Project

General

Profile

Feature #22923 » 0001-Add-ODT-export-for-wiki-pages.patch

Gregor Schmidt, 2016-06-24 14:20

View differences:

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
(5-5/5)