##// END OF EJS Templates
Fixed: Wiki section edit escapes code tags inside pre blocks (#9673)....
Jean-Philippe Lang -
r7855:c5cabfe106c9
parent child
Show More
@@ -1,181 +1,182
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redcloth3'
19 19 require 'digest/md5'
20 20
21 21 module Redmine
22 22 module WikiFormatting
23 23 module Textile
24 24 class Formatter < RedCloth3
25 25 include ActionView::Helpers::TagHelper
26 26
27 27 # auto_link rule after textile rules so that it doesn't break !image_url! tags
28 28 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
29 29
30 30 def initialize(*args)
31 31 super
32 32 self.hard_breaks=true
33 33 self.no_span_caps=true
34 34 self.filter_styles=true
35 35 end
36 36
37 37 def to_html(*rules)
38 38 @toc = []
39 39 super(*RULES).to_s
40 40 end
41 41
42 42 def get_section(index)
43 43 section = extract_sections(index)[1]
44 44 hash = Digest::MD5.hexdigest(section)
45 45 return section, hash
46 46 end
47 47
48 48 def update_section(index, update, hash=nil)
49 49 t = extract_sections(index)
50 50 if hash.present? && hash != Digest::MD5.hexdigest(t[1])
51 51 raise Redmine::WikiFormatting::StaleSectionError
52 52 end
53 53 t[1] = update unless t[1].blank?
54 54 t.reject(&:blank?).join "\n\n"
55 55 end
56 56
57 57 def extract_sections(index)
58 58 @pre_list = []
59 59 text = self.dup
60 60 rip_offtags text, false
61 61 before = ''
62 62 s = ''
63 63 after = ''
64 64 i = 0
65 65 l = 1
66 66 started = false
67 67 ended = false
68 68 text.scan(/(((?:.*?)(\A|\r?\n\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level|
69 69 if heading.nil?
70 70 if ended
71 71 after << all
72 72 elsif started
73 73 s << all
74 74 else
75 75 before << all
76 76 end
77 77 break
78 78 end
79 79 i += 1
80 80 if ended
81 81 after << all
82 82 elsif i == index
83 83 l = level.to_i
84 84 before << content
85 85 s << heading
86 86 started = true
87 87 elsif i > index
88 88 s << content
89 89 if level.to_i > l
90 90 s << heading
91 91 else
92 92 after << heading
93 93 ended = true
94 94 end
95 95 else
96 96 before << all
97 97 end
98 98 end
99 99 sections = [before.strip, s.strip, after.strip]
100 sections.each {|section| smooth_offtags section}
100 sections.each {|section| smooth_offtags_without_code_highlighting section}
101 101 sections
102 102 end
103 103
104 104 private
105 105
106 106 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
107 107 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
108 108 def hard_break( text )
109 109 text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
110 110 end
111 111
112 alias :smooth_offtags_without_code_highlighting :smooth_offtags
112 113 # Patch to add code highlighting support to RedCloth
113 114 def smooth_offtags( text )
114 115 unless @pre_list.empty?
115 116 ## replace <pre> content
116 117 text.gsub!(/<redpre#(\d+)>/) do
117 118 content = @pre_list[$1.to_i]
118 119 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
119 120 content = "<code class=\"#{$1} syntaxhl\">" +
120 121 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
121 122 end
122 123 content
123 124 end
124 125 end
125 126 end
126 127
127 128 AUTO_LINK_RE = %r{
128 129 ( # leading text
129 130 <\w+.*?>| # leading HTML tag, or
130 131 [^=<>!:'"/]| # leading punctuation, or
131 132 ^ # beginning of line
132 133 )
133 134 (
134 135 (?:https?://)| # protocol spec, or
135 136 (?:s?ftps?://)|
136 137 (?:www\.) # www.*
137 138 )
138 139 (
139 140 (\S+?) # url
140 141 (\/)? # slash
141 142 )
142 143 ((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
143 144 (?=<|\s|$)
144 145 }x unless const_defined?(:AUTO_LINK_RE)
145 146
146 147 # Turns all urls into clickable links (code from Rails).
147 148 def inline_auto_link(text)
148 149 text.gsub!(AUTO_LINK_RE) do
149 150 all, leading, proto, url, post = $&, $1, $2, $3, $6
150 151 if leading =~ /<a\s/i || leading =~ /![<>=]?/
151 152 # don't replace URL's that are already linked
152 153 # and URL's prefixed with ! !> !< != (textile images)
153 154 all
154 155 else
155 156 # Idea below : an URL with unbalanced parethesis and
156 157 # ending by ')' is put into external parenthesis
157 158 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
158 159 url=url[0..-2] # discard closing parenth from url
159 160 post = ")"+post # add closing parenth to post
160 161 end
161 162 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
162 163 %(#{leading}#{tag}#{post})
163 164 end
164 165 end
165 166 end
166 167
167 168 # Turns all email addresses into clickable links (code from Rails).
168 169 def inline_auto_mailto(text)
169 170 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
170 171 mail = $1
171 172 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
172 173 mail
173 174 else
174 175 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
175 176 end
176 177 end
177 178 end
178 179 end
179 180 end
180 181 end
181 182 end
@@ -1,333 +1,338
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../../../test_helper', __FILE__)
19 19 require 'digest/md5'
20 20
21 21 class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
22 22
23 23 def setup
24 24 @formatter = Redmine::WikiFormatting::Textile::Formatter
25 25 end
26 26
27 27 MODIFIERS = {
28 28 "*" => 'strong', # bold
29 29 "_" => 'em', # italic
30 30 "+" => 'ins', # underline
31 31 "-" => 'del', # deleted
32 32 "^" => 'sup', # superscript
33 33 "~" => 'sub' # subscript
34 34 }
35 35
36 36 def test_modifiers
37 37 assert_html_output(
38 38 '*bold*' => '<strong>bold</strong>',
39 39 'before *bold*' => 'before <strong>bold</strong>',
40 40 '*bold* after' => '<strong>bold</strong> after',
41 41 '*two words*' => '<strong>two words</strong>',
42 42 '*two*words*' => '<strong>two*words</strong>',
43 43 '*two * words*' => '<strong>two * words</strong>',
44 44 '*two* *words*' => '<strong>two</strong> <strong>words</strong>',
45 45 '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>',
46 46 # with class
47 47 '*(foo)two words*' => '<strong class="foo">two words</strong>'
48 48 )
49 49 end
50 50
51 51 def test_modifiers_combination
52 52 MODIFIERS.each do |m1, tag1|
53 53 MODIFIERS.each do |m2, tag2|
54 54 next if m1 == m2
55 55 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}"
56 56 html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>"
57 57 assert_html_output text => html
58 58 end
59 59 end
60 60 end
61 61
62 62 def test_inline_code
63 63 assert_html_output(
64 64 'this is @some code@' => 'this is <code>some code</code>',
65 65 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
66 66 )
67 67 end
68 68
69 69 def test_escaping
70 70 assert_html_output(
71 71 'this is a <script>' => 'this is a &lt;script&gt;'
72 72 )
73 73 end
74 74
75 75 def test_use_of_backslashes_followed_by_numbers_in_headers
76 76 assert_html_output({
77 77 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
78 78 }, false)
79 79 end
80 80
81 81 def test_double_dashes_should_not_strikethrough
82 82 assert_html_output(
83 83 'double -- dashes -- test' => 'double -- dashes -- test',
84 84 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
85 85 )
86 86 end
87 87
88 88 def test_acronyms
89 89 assert_html_output(
90 90 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
91 91 '2 letters JP(Jean-Philippe) acronym' => '2 letters <acronym title="Jean-Philippe">JP</acronym> acronym',
92 92 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>'
93 93 )
94 94 end
95 95
96 96 def test_blockquote
97 97 # orig raw text
98 98 raw = <<-RAW
99 99 John said:
100 100 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
101 101 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
102 102 > * Donec odio lorem,
103 103 > * sagittis ac,
104 104 > * malesuada in,
105 105 > * adipiscing eu, dolor.
106 106 >
107 107 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
108 108 > Proin a tellus. Nam vel neque.
109 109
110 110 He's right.
111 111 RAW
112 112
113 113 # expected html
114 114 expected = <<-EXPECTED
115 115 <p>John said:</p>
116 116 <blockquote>
117 117 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
118 118 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
119 119 <ul>
120 120 <li>Donec odio lorem,</li>
121 121 <li>sagittis ac,</li>
122 122 <li>malesuada in,</li>
123 123 <li>adipiscing eu, dolor.</li>
124 124 </ul>
125 125 <blockquote>
126 126 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
127 127 </blockquote>
128 128 <p>Proin a tellus. Nam vel neque.</p>
129 129 </blockquote>
130 130 <p>He's right.</p>
131 131 EXPECTED
132 132
133 133 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
134 134 end
135 135
136 136 def test_table
137 137 raw = <<-RAW
138 138 This is a table with empty cells:
139 139
140 140 |cell11|cell12||
141 141 |cell21||cell23|
142 142 |cell31|cell32|cell33|
143 143 RAW
144 144
145 145 expected = <<-EXPECTED
146 146 <p>This is a table with empty cells:</p>
147 147
148 148 <table>
149 149 <tr><td>cell11</td><td>cell12</td><td></td></tr>
150 150 <tr><td>cell21</td><td></td><td>cell23</td></tr>
151 151 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
152 152 </table>
153 153 EXPECTED
154 154
155 155 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
156 156 end
157 157
158 158 def test_table_with_line_breaks
159 159 raw = <<-RAW
160 160 This is a table with line breaks:
161 161
162 162 |cell11
163 163 continued|cell12||
164 164 |-cell21-||cell23
165 165 cell23 line2
166 166 cell23 *line3*|
167 167 |cell31|cell32
168 168 cell32 line2|cell33|
169 169
170 170 RAW
171 171
172 172 expected = <<-EXPECTED
173 173 <p>This is a table with line breaks:</p>
174 174
175 175 <table>
176 176 <tr>
177 177 <td>cell11<br />continued</td>
178 178 <td>cell12</td>
179 179 <td></td>
180 180 </tr>
181 181 <tr>
182 182 <td><del>cell21</del></td>
183 183 <td></td>
184 184 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
185 185 </tr>
186 186 <tr>
187 187 <td>cell31</td>
188 188 <td>cell32<br/>cell32 line2</td>
189 189 <td>cell33</td>
190 190 </tr>
191 191 </table>
192 192 EXPECTED
193 193
194 194 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
195 195 end
196 196
197 197 def test_textile_should_not_mangle_brackets
198 198 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
199 199 end
200 200
201 201 def test_textile_should_escape_image_urls
202 202 # this is onclick="alert('XSS');" in encoded form
203 203 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
204 204 expected = '<p><img src="/images/comment.png&quot;onclick=&amp;#x61;&amp;#x6c;&amp;#x65;&amp;#x72;&amp;#x74;&amp;#x28;&amp;#x27;&amp;#x58;&amp;#x53;&amp;#x53;&amp;#x27;&amp;#x29;;&amp;#x22;" alt="" /></p>'
205 205 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
206 206 end
207 207
208 208
209 209 STR_WITHOUT_PRE = [
210 210 # 0
211 211 "h1. Title
212 212
213 213 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
214 214 # 1
215 215 "h2. Heading 2
216 216
217 217 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
218 218
219 219 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
220 220 # 2
221 221 "h2. Heading 2
222 222
223 223 Morbi facilisis accumsan orci non pharetra.
224 224
225 225 h3. Heading 3
226 226
227 227 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
228 228 # 3
229 229 "h3. Heading 3
230 230
231 231 Praesent eget turpis nibh, a lacinia nulla.",
232 232 # 4
233 233 "h2. Heading 2
234 234
235 235 Ut rhoncus elementum adipiscing."]
236 236
237 237 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
238 238
239 239 def test_get_section_should_return_the_requested_section_and_its_hash
240 240 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
241 241 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
242 242 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
243 243 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
244 244
245 245 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
246 246 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
247 247 end
248 248
249 249 def test_update_section_should_update_the_requested_section
250 250 replacement = "New text"
251 251
252 252 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
253 253 assert_equal [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
254 254 assert_equal [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
255 255 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
256 256
257 257 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
258 258 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
259 259 end
260 260
261 261 def test_update_section_with_hash_should_update_the_requested_section
262 262 replacement = "New text"
263 263
264 264 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
265 265 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
266 266 end
267 267
268 268 def test_update_section_with_wrong_hash_should_raise_an_error
269 269 assert_raise Redmine::WikiFormatting::StaleSectionError do
270 270 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
271 271 end
272 272 end
273 273
274 274 STR_WITH_PRE = [
275 275 # 0
276 276 "h1. Title
277 277
278 278 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
279 279 # 1
280 280 "h2. Heading 2
281 281
282 <pre><code class=\"ruby\">
283 def foo
284 end
285 </code></pre>
286
282 287 Morbi facilisis accumsan orci non pharetra.
283 288
284 289 <pre>
285 290 Pre Content:
286 291
287 292 h2. Inside pre
288 293
289 294 <tag> inside pre block
290 295
291 296 Morbi facilisis accumsan orci non pharetra.
292 297 </pre>",
293 298 # 2
294 299 "h3. Heading 3
295 300
296 301 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
297 302
298 303 def test_get_section_should_ignore_pre_content
299 304 text = STR_WITH_PRE.join("\n\n")
300 305
301 306 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
302 307 assert_section_with_hash STR_WITH_PRE[2], text, 3
303 308 end
304 309
305 310 def test_update_section_should_not_escape_pre_content_outside_section
306 311 text = STR_WITH_PRE.join("\n\n")
307 312 replacement = "New text"
308 313
309 314 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
310 315 @formatter.new(text).update_section(3, replacement)
311 316 end
312 317
313 318 private
314 319
315 320 def assert_html_output(to_test, expect_paragraph = true)
316 321 to_test.each do |text, expected|
317 322 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
318 323 end
319 324 end
320 325
321 326 def to_html(text)
322 327 @formatter.new(text).to_html
323 328 end
324 329
325 330 def assert_section_with_hash(expected, text, index)
326 331 result = @formatter.new(text).get_section(index)
327 332
328 333 assert_kind_of Array, result
329 334 assert_equal 2, result.size
330 335 assert_equal expected, result.first, "section content did not match"
331 336 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
332 337 end
333 338 end
General Comments 0
You need to be logged in to leave comments. Login now