Feature #22923 » 0001-Add-ODT-export-for-wiki-pages.patch
| Gemfile | ||
|---|---|---|
| 15 | 15 |
gem "actionpack-xml_parser" |
| 16 | 16 |
gem "roadie-rails" |
| 17 | 17 |
gem "mimemagic" |
| 18 |
gem "html2odt", "~> 0.3.3" |
|
| 18 | 19 | |
| 19 | 20 |
# Request at least nokogiri 1.6.7.2 because of security advisories |
| 20 | 21 |
gem "nokogiri", ">= 1.6.7.2" |
| 21 | 22 | |
| 22 |
# Request at least rails-html-sanitizer 1.0.3 because of security advisories
|
|
| 23 |
# Request at least rails-html-sanitizer 1.0.3 because of security advisories |
|
| 23 | 24 |
gem "rails-html-sanitizer", ">= 1.0.3" |
| 24 | 25 | |
| 25 | 26 |
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem |
| app/controllers/wiki_controller.rb | ||
|---|---|---|
| 101 | 101 |
export = render_to_string :action => 'export', :layout => false |
| 102 | 102 |
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
|
| 103 | 103 |
return |
| 104 |
elsif params[:format] == 'odt' |
|
| 105 |
send_file_headers! :type => 'application/vnd.oasis.opendocument.text', |
|
| 106 |
:filename => "#{@page.title}.odt"
|
|
| 107 |
render :inline => "<%= raw wiki_page_to_odt(@page, @project) %>" |
|
| 108 |
return |
|
| 104 | 109 |
elsif params[:format] == 'txt' |
| 105 | 110 |
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
|
| 106 | 111 |
return |
| ... | ... | |
| 305 | 310 |
format.pdf {
|
| 306 | 311 |
send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}.pdf"
|
| 307 | 312 |
} |
| 313 |
format.odt {
|
|
| 314 |
send_file_headers! :type => 'application/vnd.oasis.opendocument.text', |
|
| 315 |
:filename => "#{@project.identifier}.odt"
|
|
| 316 |
render :inline => "<%= raw wiki_pages_to_odt(@pages, @project) %>" |
|
| 317 |
} |
|
| 308 | 318 |
end |
| 309 | 319 |
end |
| 310 | 320 | |
| app/helpers/wiki_helper.rb | ||
|---|---|---|
| 19 | 19 | |
| 20 | 20 |
module WikiHelper |
| 21 | 21 |
include Redmine::Export::PDF::WikiPdfHelper |
| 22 |
include Redmine::Export::ODT::WikiOdtHelper |
|
| 22 | 23 | |
| 23 | 24 |
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0) |
| 24 | 25 |
pages = pages.group_by(&:parent) unless pages.is_a?(Hash) |
| app/views/wiki/index.html.erb | ||
|---|---|---|
| 26 | 26 |
<% if User.current.allowed_to?(:export_wiki_pages, @project) %> |
| 27 | 27 |
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
|
| 28 | 28 |
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
|
| 29 |
<%= f.link_to('ODT', :url => {:action => 'export', :format => 'odt'}) %>
|
|
| 29 | 30 |
<% end %> |
| 30 | 31 |
<% end %> |
| 31 | 32 |
<% end %> |
| app/views/wiki/show.html.erb | ||
|---|---|---|
| 31 | 31 |
<%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
|
| 32 | 32 |
<%= '('.html_safe + link_to(l(:label_diff), :controller => 'wiki', :action => 'diff',
|
| 33 | 33 |
:id => @page.title, :project_id => @page.project, |
| 34 |
:version => @content.version) + ')'.html_safe if @content.previous %> -
|
|
| 34 |
:version => @content.version) + ')'.html_safe if @content.previous %> - |
|
| 35 | 35 |
<%= link_to((l(:label_next) + " \xc2\xbb"), :action => 'show', |
| 36 | 36 |
:id => @page.title, :project_id => @page.project, |
| 37 | 37 |
:version => @content.next.version) + " - " if @content.next %> |
| ... | ... | |
| 68 | 68 |
<% other_formats_links do |f| %> |
| 69 | 69 |
<%= f.link_to 'PDF', :url => {:id => @page.title, :version => params[:version]} %>
|
| 70 | 70 |
<%= f.link_to 'HTML', :url => {:id => @page.title, :version => params[:version]} %>
|
| 71 |
<%= f.link_to 'ODT', :url => {:id => @page.title, :version => params[:version]} %>
|
|
| 71 | 72 |
<%= f.link_to 'TXT', :url => {:id => @page.title, :version => params[:version]} %>
|
| 72 | 73 |
<% end if User.current.allowed_to?(:export_wiki_pages, @project) %> |
| 73 | 74 | |
| config/initializers/20-mime_types.rb | ||
|---|---|---|
| 2 | 2 | |
| 3 | 3 |
Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV) |
| 4 | 4 | |
| 5 |
Mime::Type.register "application/vnd.oasis.opendocument.text", :odt |
|
| lib/redmine/export/odt/wiki_odt_helper.rb | ||
|---|---|---|
| 1 |
module Redmine |
|
| 2 |
module Export |
|
| 3 |
module ODT |
|
| 4 |
module WikiOdtHelper |
|
| 5 |
def wiki_pages_to_odt(pages, project) |
|
| 6 |
doc = Html2Odt::Document.new |
|
| 7 | ||
| 8 |
doc.image_location_mapping = image_location_mapping_proc(pages.map(&:attachments).flatten) |
|
| 9 | ||
| 10 |
doc.html = cleanup_html(html_for_page_hierarchy(pages.group_by(&:parent_id))) |
|
| 11 |
doc.base_uri = project_wiki_index_url(project) |
|
| 12 | ||
| 13 |
doc.title = project.name |
|
| 14 |
doc.author = User.current.name |
|
| 15 | ||
| 16 |
doc.data |
|
| 17 |
end |
|
| 18 | ||
| 19 |
def wiki_page_to_odt(page, project) |
|
| 20 |
doc = Html2Odt::Document.new |
|
| 21 | ||
| 22 |
doc.image_location_mapping = image_location_mapping_proc(page.attachments) |
|
| 23 |
doc.base_uri = project_wiki_page_url(project, page) |
|
| 24 | ||
| 25 |
doc.author = User.current.name |
|
| 26 |
doc.title = "#{project.name} - #{page.title}"
|
|
| 27 | ||
| 28 |
doc.html = cleanup_html(html_for_page_hierarchy(nil => [page])) |
|
| 29 | ||
| 30 |
doc.data |
|
| 31 |
end |
|
| 32 | ||
| 33 |
protected |
|
| 34 | ||
| 35 |
def html_for_page_hierarchy(pages, node = nil, level = 0) |
|
| 36 |
return "" if pages[node].blank? |
|
| 37 | ||
| 38 |
html = "" |
|
| 39 | ||
| 40 |
pages[node].each do |page| |
|
| 41 |
html += "<hr/>\n" unless level == 0 && page == pages[node].first |
|
| 42 | ||
| 43 |
html += textilizable(page.content, :text, |
|
| 44 |
:only_path => false, |
|
| 45 |
:edit_section_links => false, |
|
| 46 |
:headings => false) |
|
| 47 | ||
| 48 |
html += html_for_page_hierarchy(pages, page.id, level + 1) |
|
| 49 |
end |
|
| 50 | ||
| 51 |
html |
|
| 52 |
end |
|
| 53 | ||
| 54 |
def cleanup_html(html) |
|
| 55 |
# Strip {{toc}} tags
|
|
| 56 |
# |
|
| 57 |
# The links generated within the toc-pseudo-macro will not work inside |
|
| 58 |
# the ODT, so let's remove it.. |
|
| 59 |
html = html.gsub(/<p>\{\{([<>]?)toc.*?\}\}<\/p>/i, '')
|
|
| 60 | ||
| 61 | ||
| 62 |
# Cleanup {{collapse}} macro output
|
|
| 63 |
# |
|
| 64 |
# The collapse macro is generating the following (simplified) markup: |
|
| 65 |
# |
|
| 66 |
# <p> |
|
| 67 |
# <a class="collapsible collapsed">Open link</a> |
|
| 68 |
# <a class="collapsible">Close link</a> |
|
| 69 |
# <div class="collapsed-text">Content</div> |
|
| 70 |
# </p> |
|
| 71 |
# |
|
| 72 |
# An HTML parser (like Nokogiri or any browser) will create the |
|
| 73 |
# following DOM |
|
| 74 |
# |
|
| 75 |
# <p> |
|
| 76 |
# <a class="collapsible collapsed">Open link</a> |
|
| 77 |
# <a class="collapsible">Close link</a> |
|
| 78 |
# </p> |
|
| 79 |
# <div class="collapsed-text">Content</div> |
|
| 80 |
# <p/> |
|
| 81 |
# |
|
| 82 |
# So we're trying to remove the first p, containing the links, and |
|
| 83 |
# we're replacing the div.collapsed-text with its content. The |
|
| 84 |
# remaining p is difficult to target (or does not seem to be created |
|
| 85 |
# in Nokogiri), so we're leaving that one alone. |
|
| 86 |
# |
|
| 87 |
# In a previous version, we were only removing the links themselves, |
|
| 88 |
# but this lead to errors in html2odt's own HTML cleanup (elements, |
|
| 89 |
# that needed fixing were not found). |
|
| 90 |
doc = Nokogiri::HTML::DocumentFragment.parse(html) |
|
| 91 | ||
| 92 |
doc.css(".collapsible.collapsed").each do |collapsed_links|
|
|
| 93 |
collapsed_links.parent.remove |
|
| 94 |
end |
|
| 95 | ||
| 96 |
doc.css(".collapsed-text").each do |collapsed_text|
|
|
| 97 |
collapsed_text.replace collapsed_text.children |
|
| 98 |
end |
|
| 99 | ||
| 100 |
html = doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML) |
|
| 101 | ||
| 102 |
html |
|
| 103 |
end |
|
| 104 | ||
| 105 |
def image_location_mapping_proc(attachments) |
|
| 106 |
lambda do |src| |
|
| 107 |
# See if src maps to local URL which happens to be an attachment of |
|
| 108 |
# this wiki page |
|
| 109 | ||
| 110 |
if src =~ /\Ahttps?:\/\// |
|
| 111 |
# Ignore URLs pointing to other hosts |
|
| 112 | ||
| 113 |
src_uri = URI.parse(src) rescue nil |
|
| 114 | ||
| 115 |
next unless src_uri |
|
| 116 | ||
| 117 |
src_base_url = "#{src_uri.scheme}://#{src_uri.host}"
|
|
| 118 |
if src_uri.default_port != src_uri.port |
|
| 119 |
src_base_url += ":#{src_uri.port}"
|
|
| 120 |
end |
|
| 121 | ||
| 122 |
next if request.base_url != src_base_url |
|
| 123 |
else |
|
| 124 |
# Ignore URLs with protocols != http(s) |
|
| 125 |
next if src.include? "://" |
|
| 126 |
end |
|
| 127 | ||
| 128 |
# Include public images |
|
| 129 |
# |
|
| 130 |
public_path = File.join(Rails.public_path, src) |
|
| 131 | ||
| 132 |
# but make sure, that we're not vulnerable to directory traversal attacks |
|
| 133 |
# |
|
| 134 |
# File.realpath accesses file system and raises Errno::ENOENT if file |
|
| 135 |
# does not exist |
|
| 136 |
valid_path = File.realpath(public_path).starts_with?(Rails.public_path.to_s) rescue false |
|
| 137 |
next public_path if valid_path and File.readable?(public_path) |
|
| 138 | ||
| 139 | ||
| 140 |
# Include attached images |
|
| 141 |
# |
|
| 142 |
path = Rails.application.routes.recognize_path(src) rescue nil |
|
| 143 | ||
| 144 |
next if path.blank? |
|
| 145 |
next if path[:controller] != "attachments" |
|
| 146 |
next if path[:id].blank? |
|
| 147 | ||
| 148 |
attachment = attachments.find { |a| a.to_param == path[:id] }
|
|
| 149 | ||
| 150 |
if path[:action] == "thumbnail" and path[:size].present? |
|
| 151 |
return attachment.thumbnail(size: path[:size]) |
|
| 152 |
end |
|
| 153 | ||
| 154 |
if path[:action] == "download" |
|
| 155 |
return attachment.diskfile |
|
| 156 |
end |
|
| 157 |
end |
|
| 158 |
end |
|
| 159 |
end |
|
| 160 |
end |
|
| 161 |
end |
|
| 162 |
end |
|
- « Previous
- 1
- …
- 3
- 4
- 5
- Next »