From 0bd5dcfe69ee5ab11909408ec21a17e2e0e6733c Mon Sep 17 00:00:00 2001
From: Gregor Schmidt
Date: Mon, 30 May 2016 12:01:21 +0200
Subject: [PATCH] Add ODT export for wiki pages
---
Gemfile | 3 +-
app/controllers/wiki_controller.rb | 10 ++
app/helpers/wiki_helper.rb | 1 +
app/views/wiki/index.html.erb | 1 +
app/views/wiki/show.html.erb | 3 +-
config/initializers/20-mime_types.rb | 1 +
lib/redmine/export/odt/wiki_odt_helper.rb | 162 ++++++++++++++++++++++++++++++
7 files changed, 179 insertions(+), 2 deletions(-)
create mode 100644 lib/redmine/export/odt/wiki_odt_helper.rb
diff --git a/Gemfile b/Gemfile
index 2ddfd92..d68127b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -15,11 +15,12 @@ gem "actionpack-action_caching"
gem "actionpack-xml_parser"
gem "roadie-rails"
gem "mimemagic"
+gem "html2odt", "~> 0.3.3"
# Request at least nokogiri 1.6.7.2 because of security advisories
gem "nokogiri", ">= 1.6.7.2"
-# Request at least rails-html-sanitizer 1.0.3 because of security advisories
+# Request at least rails-html-sanitizer 1.0.3 because of security advisories
gem "rails-html-sanitizer", ">= 1.0.3"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb
index 10fd099..353b73d 100644
--- a/app/controllers/wiki_controller.rb
+++ b/app/controllers/wiki_controller.rb
@@ -101,6 +101,11 @@ class WikiController < ApplicationController
export = render_to_string :action => 'export', :layout => false
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
return
+ elsif params[:format] == 'odt'
+ send_file_headers! :type => 'application/vnd.oasis.opendocument.text',
+ :filename => "#{@page.title}.odt"
+ render :inline => "<%= raw wiki_page_to_odt(@page, @project) %>"
+ return
elsif params[:format] == 'txt'
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
@@ -305,6 +310,11 @@ class WikiController < ApplicationController
format.pdf {
send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}.pdf"
}
+ format.odt {
+ send_file_headers! :type => 'application/vnd.oasis.opendocument.text',
+ :filename => "#{@project.identifier}.odt"
+ render :inline => "<%= raw wiki_pages_to_odt(@pages, @project) %>"
+ }
end
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 89da548..a552ac9 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -19,6 +19,7 @@
module WikiHelper
include Redmine::Export::PDF::WikiPdfHelper
+ include Redmine::Export::ODT::WikiOdtHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
diff --git a/app/views/wiki/index.html.erb b/app/views/wiki/index.html.erb
index 0d6955d..804af24 100644
--- a/app/views/wiki/index.html.erb
+++ b/app/views/wiki/index.html.erb
@@ -26,6 +26,7 @@
<% if User.current.allowed_to?(:export_wiki_pages, @project) %>
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
+ <%= f.link_to('ODT', :url => {:action => 'export', :format => 'odt'}) %>
<% end %>
<% end %>
<% end %>
diff --git a/app/views/wiki/show.html.erb b/app/views/wiki/show.html.erb
index 41dd12d..a481c4e 100644
--- a/app/views/wiki/show.html.erb
+++ b/app/views/wiki/show.html.erb
@@ -31,7 +31,7 @@
<%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
<%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
:id => @page.title, :project_id => @page.project,
- :version => @content.version) + ')'.html_safe if @content.previous %> -
+ :version => @content.version) + ')'.html_safe if @content.previous %> -
<%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show',
:id => @page.title, :project_id => @page.project,
:version => @content.next.version) + " - " if @content.next %>
@@ -68,6 +68,7 @@
<% other_formats_links do |f| %>
<%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %>
<%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %>
+ <%= f.link_to 'ODT', :url => {:id => @page.title, :version => params[:version]} %>
<%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %>
<% end if User.current.allowed_to?(:export_wiki_pages, @project) %>
diff --git a/config/initializers/20-mime_types.rb b/config/initializers/20-mime_types.rb
index cfd35a3..95318b0 100644
--- a/config/initializers/20-mime_types.rb
+++ b/config/initializers/20-mime_types.rb
@@ -2,3 +2,4 @@
Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
+Mime::Type.register "application/vnd.oasis.opendocument.text", :odt
diff --git a/lib/redmine/export/odt/wiki_odt_helper.rb b/lib/redmine/export/odt/wiki_odt_helper.rb
new file mode 100644
index 0000000..6b3cfa2
--- /dev/null
+++ b/lib/redmine/export/odt/wiki_odt_helper.rb
@@ -0,0 +1,162 @@
+module Redmine
+ module Export
+ module ODT
+ module WikiOdtHelper
+ def wiki_pages_to_odt(pages, project)
+ doc = Html2Odt::Document.new
+
+ doc.image_location_mapping = image_location_mapping_proc(pages.map(&:attachments).flatten)
+
+ doc.html = cleanup_html(html_for_page_hierarchy(pages.group_by(&:parent_id)))
+ doc.base_uri = project_wiki_index_url(project)
+
+ doc.title = project.name
+ doc.author = User.current.name
+
+ doc.data
+ end
+
+ def wiki_page_to_odt(page, project)
+ doc = Html2Odt::Document.new
+
+ doc.image_location_mapping = image_location_mapping_proc(page.attachments)
+ doc.base_uri = project_wiki_page_url(project, page)
+
+ doc.author = User.current.name
+ doc.title = "#{project.name} - #{page.title}"
+
+ doc.html = cleanup_html(html_for_page_hierarchy(nil => [page]))
+
+ doc.data
+ end
+
+ protected
+
+ def html_for_page_hierarchy(pages, node = nil, level = 0)
+ return "" if pages[node].blank?
+
+ html = ""
+
+ pages[node].each do |page|
+ html += "
\n" unless level == 0 && page == pages[node].first
+
+ html += textilizable(page.content, :text,
+ :only_path => false,
+ :edit_section_links => false,
+ :headings => false)
+
+ html += html_for_page_hierarchy(pages, page.id, level + 1)
+ end
+
+ html
+ end
+
+ def cleanup_html(html)
+ # Strip {{toc}} tags
+ #
+ # The links generated within the toc-pseudo-macro will not work inside
+ # the ODT, so let's remove it..
+ html = html.gsub(/\{\{([<>]?)toc.*?\}\}<\/p>/i, '')
+
+
+ # Cleanup {{collapse}} macro output
+ #
+ # The collapse macro is generating the following (simplified) markup:
+ #
+ #
+ # Open link
+ # Close link
+ #
Content
+ #
+ #
+ # An HTML parser (like Nokogiri or any browser) will create the
+ # following DOM
+ #
+ #
+ # Open link
+ # Close link
+ #
+ # Content
+ #
+ #
+ # So we're trying to remove the first p, containing the links, and
+ # we're replacing the div.collapsed-text with its content. The
+ # remaining p is difficult to target (or does not seem to be created
+ # in Nokogiri), so we're leaving that one alone.
+ #
+ # In a previous version, we were only removing the links themselves,
+ # but this lead to errors in html2odt's own HTML cleanup (elements,
+ # that needed fixing were not found).
+ doc = Nokogiri::HTML::DocumentFragment.parse(html)
+
+ doc.css(".collapsible.collapsed").each do |collapsed_links|
+ collapsed_links.parent.remove
+ end
+
+ doc.css(".collapsed-text").each do |collapsed_text|
+ collapsed_text.replace collapsed_text.children
+ end
+
+ html = doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
+
+ html
+ end
+
+ def image_location_mapping_proc(attachments)
+ lambda do |src|
+ # See if src maps to local URL which happens to be an attachment of
+ # this wiki page
+
+ if src =~ /\Ahttps?:\/\//
+ # Ignore URLs pointing to other hosts
+
+ src_uri = URI.parse(src) rescue nil
+
+ next unless src_uri
+
+ src_base_url = "#{src_uri.scheme}://#{src_uri.host}"
+ if src_uri.default_port != src_uri.port
+ src_base_url += ":#{src_uri.port}"
+ end
+
+ next if request.base_url != src_base_url
+ else
+ # Ignore URLs with protocols != http(s)
+ next if src.include? "://"
+ end
+
+ # Include public images
+ #
+ public_path = File.join(Rails.public_path, src)
+
+ # but make sure, that we're not vulnerable to directory traversal attacks
+ #
+ # File.realpath accesses file system and raises Errno::ENOENT if file
+ # does not exist
+ valid_path = File.realpath(public_path).starts_with?(Rails.public_path.to_s) rescue false
+ next public_path if valid_path and File.readable?(public_path)
+
+
+ # Include attached images
+ #
+ path = Rails.application.routes.recognize_path(src) rescue nil
+
+ next if path.blank?
+ next if path[:controller] != "attachments"
+ next if path[:id].blank?
+
+ attachment = attachments.find { |a| a.to_param == path[:id] }
+
+ if path[:action] == "thumbnail" and path[:size].present?
+ return attachment.thumbnail(size: path[:size])
+ end
+
+ if path[:action] == "download"
+ return attachment.diskfile
+ end
+ end
+ end
+ end
+ end
+ end
+end
--
2.9.0