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: + # + #

+ # + # Close link + #

Content
+ #

+ # + # An HTML parser (like Nokogiri or any browser) will create the + # following DOM + # + #

+ # + # 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