From fad3ab3e2e111eb0ec46d6c8372d64c4a057cf10 Mon Sep 17 00:00:00 2001
From: Paul Duncan
Date: Fri, 29 Jul 2016 02:51:12 -0400
Subject: add views/post-exporter
---
data/assets/js/admin/tabs/import.js | 2 +-
src/guff/disposition.cr | 12 +++
src/guff/handlers.cr | 55 ++++++++++++-
src/guff/views/post-exporter.cr | 151 ++++++++++++++++++++++++++++++++++++
src/views/panes/settings/import.ecr | 67 +++++++++-------
5 files changed, 254 insertions(+), 33 deletions(-)
create mode 100644 src/guff/disposition.cr
create mode 100644 src/guff/views/post-exporter.cr
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,
@@ -786,6 +830,11 @@ module Guff::Handlers
# before api handler
: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" => %{
+
+
+
+
+
+ %{name | h}
+ %{link | h}
+ %{text | h}
+ %{date_rfc2822 | h}
+ https://guff.pablotron.org/?v=%{version | h}
+ %{lang | h}
+ 1.0
+ %{link | h}
+ %{link | h}
+ }.strip,
+
+ "head_tag" => %{
+
+ %{tag | h}
+ %{tag | h}
+
+ }.strip,
+
+ "item_tag" => %{
+ %{tag | h}
+ }.strip,
+
+ "item" => %{
+ -
+ %{tags}
+ %{name | h}
+ %{link | h}
+ %{link | h}
+
+ %{body | h}
+
+ %{post_id | h}
+ %{posted_at_text | h}
+ %{posted_at | h}
+ closed
+ closed
+ %{slug | h}
+ publish
+ 0
+ 0
+ post
+
+
+ },
+
+ "foot" => %{
+
+
+ }.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 format.
-
-
-
+
--
cgit v1.2.3