##// END OF EJS Templates
Fixed: Angle brackets within 'pre' blocks are silently replaced by HTML entities during wiki section-edit (#9673)....
Jean-Philippe Lang -
r7849:9f15517095c4
parent child
Show More
@@ -1,181 +1,181
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 rip_offtags text
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 100 sections.each {|section| smooth_offtags 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 112 # Patch to add code highlighting support to RedCloth
113 113 def smooth_offtags( text )
114 114 unless @pre_list.empty?
115 115 ## replace <pre> content
116 116 text.gsub!(/<redpre#(\d+)>/) do
117 117 content = @pre_list[$1.to_i]
118 118 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
119 119 content = "<code class=\"#{$1} syntaxhl\">" +
120 120 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
121 121 end
122 122 content
123 123 end
124 124 end
125 125 end
126 126
127 127 AUTO_LINK_RE = %r{
128 128 ( # leading text
129 129 <\w+.*?>| # leading HTML tag, or
130 130 [^=<>!:'"/]| # leading punctuation, or
131 131 ^ # beginning of line
132 132 )
133 133 (
134 134 (?:https?://)| # protocol spec, or
135 135 (?:s?ftps?://)|
136 136 (?:www\.) # www.*
137 137 )
138 138 (
139 139 (\S+?) # url
140 140 (\/)? # slash
141 141 )
142 142 ((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
143 143 (?=<|\s|$)
144 144 }x unless const_defined?(:AUTO_LINK_RE)
145 145
146 146 # Turns all urls into clickable links (code from Rails).
147 147 def inline_auto_link(text)
148 148 text.gsub!(AUTO_LINK_RE) do
149 149 all, leading, proto, url, post = $&, $1, $2, $3, $6
150 150 if leading =~ /<a\s/i || leading =~ /![<>=]?/
151 151 # don't replace URL's that are already linked
152 152 # and URL's prefixed with ! !> !< != (textile images)
153 153 all
154 154 else
155 155 # Idea below : an URL with unbalanced parethesis and
156 156 # ending by ')' is put into external parenthesis
157 157 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
158 158 url=url[0..-2] # discard closing parenth from url
159 159 post = ")"+post # add closing parenth to post
160 160 end
161 161 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
162 162 %(#{leading}#{tag}#{post})
163 163 end
164 164 end
165 165 end
166 166
167 167 # Turns all email addresses into clickable links (code from Rails).
168 168 def inline_auto_mailto(text)
169 169 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
170 170 mail = $1
171 171 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
172 172 mail
173 173 else
174 174 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
175 175 end
176 176 end
177 177 end
178 178 end
179 179 end
180 180 end
181 181 end
@@ -1,323 +1,333
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 282 Morbi facilisis accumsan orci non pharetra.
283 283
284 284 <pre>
285 285 Pre Content:
286 286
287 287 h2. Inside pre
288 288
289 <tag> inside pre block
290
289 291 Morbi facilisis accumsan orci non pharetra.
290 292 </pre>",
291 293 # 2
292 294 "h3. Heading 3
293 295
294 296 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
295 297
296 298 def test_get_section_should_ignore_pre_content
297 299 text = STR_WITH_PRE.join("\n\n")
298 300
299 301 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
300 302 assert_section_with_hash STR_WITH_PRE[2], text, 3
301 303 end
302 304
305 def test_update_section_should_not_escape_pre_content_outside_section
306 text = STR_WITH_PRE.join("\n\n")
307 replacement = "New text"
308
309 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
310 @formatter.new(text).update_section(3, replacement)
311 end
312
303 313 private
304 314
305 315 def assert_html_output(to_test, expect_paragraph = true)
306 316 to_test.each do |text, expected|
307 317 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
308 318 end
309 319 end
310 320
311 321 def to_html(text)
312 322 @formatter.new(text).to_html
313 323 end
314 324
315 325 def assert_section_with_hash(expected, text, index)
316 326 result = @formatter.new(text).get_section(index)
317 327
318 328 assert_kind_of Array, result
319 329 assert_equal 2, result.size
320 330 assert_equal expected, result.first, "section content did not match"
321 331 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
322 332 end
323 333 end
General Comments 0
You need to be logged in to leave comments. Login now