aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/assets/js/admin/tabs/import.js2
-rw-r--r--src/guff/disposition.cr12
-rw-r--r--src/guff/handlers.cr55
-rw-r--r--src/guff/views/post-exporter.cr151
-rw-r--r--src/views/panes/settings/import.ecr67
5 files changed, 254 insertions, 33 deletions
diff --git a/data/assets/js/admin/tabs/import.js b/data/assets/js/admin/tabs/import.js
index 2d5bd7f..8027dcd 100644
--- a/data/assets/js/admin/tabs/import.js
+++ b/data/assets/js/admin/tabs/import.js
@@ -9,7 +9,7 @@ jQuery(function($) {
});
$('#export-posts').click(function() {
- alert('TODO: export posts');
+ $('#export-posts-form').submit();
// stop event
return false;
diff --git a/src/guff/disposition.cr b/src/guff/disposition.cr
new file mode 100644
index 0000000..96354b0
--- /dev/null
+++ b/src/guff/disposition.cr
@@ -0,0 +1,12 @@
+module Guff::Disposition
+ HEADER = "content-disposition"
+ FORMAT = "attachment; filename=\"%s\"; filename*=UTF-8''%s"
+
+ def self.header(name : String) : String
+ FORMAT % [name.gsub(/[%";]+/, "_"), URI.escape(name)]
+ end
+
+ def self.set(headers : HTTP::Headers, name : String)
+ headers[HEADER] = header(name)
+ end
+end
diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr
index d6c526f..20c44ea 100644
--- a/src/guff/handlers.cr
+++ b/src/guff/handlers.cr
@@ -396,11 +396,12 @@ module Guff::Handlers
path = context.request.path.not_nil!
if /\/$/.match(path)
- # TODO: render page
+ # send headers
context.response.headers["x-frame-options"] = "SAMEORIGIN"
context.response.content_type = "text/html; charset=utf-8"
context.response.status_code = 200
+ # render page
Views::Posts::Project.new(@context, r).to_s(context.response)
else
# redirect to project
@@ -501,11 +502,12 @@ module Guff::Handlers
def call(context : HTTP::Server::Context)
site_id, r = find(context.request)
if site_id && r
- # TODO: render page
+ # send headers
context.response.headers["x-frame-options"] = "SAMEORIGIN"
context.response.content_type = "text/html; charset=utf-8"
context.response.status_code = 200
+ # render page
Views::Blog::List.new(@context, site_id, r).to_s(context.response)
else
# unknown page
@@ -742,6 +744,48 @@ module Guff::Handlers
end
end
+ class ExportPostsAPIHandler < AuthenticatedHandler
+ def initialize(context : Context)
+ super(context, %w{admin editor})
+ end
+
+ def authenticated_call(context : HTTP::Server::Context)
+ req_path = context.request.path.not_nil!
+
+ if matching_request?(context.request.method, req_path) &&
+ valid_origin_headers?(context.request.headers)
+
+ # parse request parameters and get site_id
+ params = HTTP::Params.parse(context.request.body.not_nil!)
+ site_id = params["site_id"].not_nil!.to_i64
+
+ # create exporter
+ exporter = Views::PostExporter.new(
+ context: @context,
+ site_id: site_id,
+ )
+
+ # build response
+ context.response.headers["x-frame-options"] = "SAMEORIGIN"
+ context.response.status_code = 200
+ context.response.content_type = exporter.content_type
+ Disposition.set(context.response.headers, exporter.file_name)
+
+ # send response
+ exporter.to_s(context.response)
+ else
+ # not a matching request
+ call_next(context)
+ end
+ end
+
+ PATH_RE = %r{^/guff/api/export/posts}
+
+ private def matching_request?(method, path)
+ (method == "POST") && PATH_RE.match(path)
+ end
+ end
+
HANDLERS = [{
:dev => true,
:id => :stub,
@@ -787,6 +831,11 @@ module Guff::Handlers
:dev => false,
:id => :file_api,
}, {
+ # NOTE: special handler for api/export/posts, should load
+ # before api handler
+ :dev => false,
+ :id => :export_posts_api,
+ }, {
:dev => false,
:id => :api,
}, {
@@ -822,6 +871,8 @@ module Guff::Handlers
SessionHandler.new(context)
when :file_api
FileAPIHandler.new(context)
+ when :export_posts_api
+ ExportPostsAPIHandler.new(context)
when :api
APIHandler.new(context)
when :assets
diff --git a/src/guff/views/post-exporter.cr b/src/guff/views/post-exporter.cr
new file mode 100644
index 0000000..0558c39
--- /dev/null
+++ b/src/guff/views/post-exporter.cr
@@ -0,0 +1,151 @@
+#
+# WXR Blog Post Exporter
+#
+# Based on the information at the following URLs:
+# https://devtidbits.com/2011/03/16/the-wordpress-extended-rss-wxr-exportimport-xml-document-format-decoded-and-explained/
+# http://wpcandy.com/made/the-sample-post-collection/
+#
+class Guff::Views::PostExporter
+ TEMPLATES = Template::Cache.new({
+ "head" => %{
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!-- Generated by Guff %{version | h} on %{date_iso8601 | h}. -->
+
+ <rss version="2.0"
+ xmlns:content="http://purl.org/rss/1.0/modules/content/"
+ xmlns:wfw="http://wellformedweb.org/CommentAPI/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:wp="http://wordpress.org/export/1.0/"
+ >
+ <channel>
+ <title>%{name | h}</title>
+ <link>%{link | h}</link>
+ <description>%{text | h}</description>
+ <pubDate>%{date_rfc2822 | h}</pubDate>
+ <generator>https://guff.pablotron.org/?v=%{version | h}</generator>
+ <language>%{lang | h}</language>
+ <wp:wxr_version>1.0</wp:wxr_version>
+ <wp:base_site_url>%{link | h}</wp:base_site_url>
+ <wp:base_blog_url>%{link | h}</wp:base_blog_url>
+ }.strip,
+
+ "head_tag" => %{
+ <wp:tag>
+ <wp:tag_slug>%{tag | h}</wp:tag_slug>
+ <wp:tag_name>%{tag | h}</wp:tag_name>
+ </wp:tag>
+ }.strip,
+
+ "item_tag" => %{
+ <category domain="tag">%{tag | h}</category>
+ }.strip,
+
+ "item" => %{
+ <item>
+ %{tags}
+ <title>%{name | h}</title>
+ <link>%{link | h}</link>
+ <guid isPermaLink="false">%{link | h}</guid>
+ <description></description>
+ <content:encoded>%{body | h}</content:encoded>
+ <excerpt:encoded></excerpt:encoded>
+ <wp:post_id>%{post_id | h}</wp:post_id>
+ <wp:post_date>%{posted_at_text | h}</wp:post_date>
+ <wp:post_date_gmt>%{posted_at | h}</wp:post_date_gmt>
+ <wp:comment_status>closed</wp:comment_status>
+ <wp:ping_status>closed</wp:ping_status>
+ <wp:post_name>%{slug | h}</wp:post_name>
+ <wp:status>publish</wp:status>
+ <wp:post_parent>0</wp:post_parent>
+ <wp:menu_order>0</wp:menu_order>
+ <wp:post_type>post</wp:post_type>
+ <wp:post_password></wp:post_password>
+ </item>
+ },
+
+ "foot" => %{
+ </channel>
+ </rss>
+ }.strip,
+ })
+
+ # TODO: add additional export options
+ def initialize(
+ @context : Context,
+ @site_id : Int64,
+ )
+ end
+
+ # FIXME: language should be specified on a per-site basis
+ LANG = "en-US"
+
+ # FIXME: check this
+ RFC2822 = Time::Format.new("%c %z")
+
+ EMPTY_HASH = {} of String => String
+
+ def to_s(io)
+ # cache current timestamp
+ now = Time.now
+
+ # write header
+ io << TEMPLATES["head"].run({
+ "version" => Guff::VERSION,
+ "lang" => LANG,
+
+ # timestamps
+ "date_rfc2822" => RFC2822.format(now),
+ "date_iso8601" => ISO8601.format(now),
+
+ # TODO: site name, link, and description
+ "name" => "Site Name",
+ "link" => "https://example.com/",
+ "text" => "Some description of this blog."
+ })
+
+ # TODO: write tags used by blog posts
+
+ # write posts
+ done, page = false, 0_i32
+ until done
+ page += 1
+ r = @context.models.blog.find(
+ site_id: @site_id,
+ page: page
+ )
+
+ if r.rows.size > 0
+ r.rows.each do |row|
+ io << TEMPLATES["item"].run(row.merge({
+ "tags" => "",
+ "link" => "/todo/" + row["slug"],
+ }))
+ end
+ else
+ done = true
+ end
+ end
+
+ # write footer
+ io << TEMPLATES["foot"].run(EMPTY_HASH)
+ end
+
+ CONTENT_TYPE = "text/xml; charset=utf-8"
+
+ def content_type
+ CONTENT_TYPE
+ end
+
+ # NOTE: wordpress uses name.wordpress.YYYY-MM-DD.xml
+ FILE_NAME_FORMAT = "%s-posts-%s.xml"
+
+ FILE_NAME_TIME_FORMAT = Time::Format.new("%Y%m%d-%H%M%S")
+
+ def file_name
+ site = @context.models.site.get(site_id: @site_id)
+ FILE_NAME_FORMAT % [
+ site["name"].as(String),
+ FILE_NAME_TIME_FORMAT.format(Time.now),
+ ]
+ end
+end
diff --git a/src/views/panes/settings/import.ecr b/src/views/panes/settings/import.ecr
index 5201e77..2cfa7ad 100644
--- a/src/views/panes/settings/import.ecr
+++ b/src/views/panes/settings/import.ecr
@@ -103,36 +103,43 @@
>WXR</a> format.
</p>
- <div class='form-group'>
- <label for='export-site'>
- Source Site
- </label>
-
- <select
- id='export-site'
- class='form-control'
- title='Choose source site.'
- aria-describedby='export-site-help'
- ><%=
- site_options
- %></select>
-
- <p id='export-site-help' class='help-block'>
- Source site of blog posts to export.
- </p>
- </div><!-- form-group -->
-
- <div class='form-group'>
- <a
- href='#'
- id='export-posts'
- class='btn btn-primary'
- title='Export blog posts to file in WXR format.'
- >
- <i class='fa fa-download'></i>
- Export Blog Posts
- </a>
- </div><!-- form-group -->
+ <form
+ id='export-posts-form'
+ method='post'
+ action='../guff/api/export/posts'
+ >
+ <div class='form-group'>
+ <label for='export-site'>
+ Source Site
+ </label>
+
+ <select
+ id='export-site'
+ name='site_id'
+ class='form-control'
+ title='Choose source site.'
+ aria-describedby='export-site-help'
+ ><%=
+ site_options
+ %></select>
+
+ <p id='export-site-help' class='help-block'>
+ Source site of blog posts to export.
+ </p>
+ </div><!-- form-group -->
+
+ <div class='form-group'>
+ <a
+ href='#'
+ id='export-posts'
+ class='btn btn-primary'
+ title='Export blog posts to file in WXR format.'
+ >
+ <i class='fa fa-download'></i>
+ Export Blog Posts
+ </a>
+ </div><!-- form-group -->
+ </form>
</div><!-- panel-body -->
</div><!-- panel -->
</div><!-- tab-pane -->