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.

-
- - - - -

- Source site of blog posts to export. -

-
- -
- - - Export Blog Posts - -
+
+
+ + + + +

+ Source site of blog posts to export. +

+
+ + +
-- cgit v1.2.3