##// END OF EJS Templates
Merged r9822 from trunk....
Jean-Philippe Lang -
r9657:081ee54bee62
parent child
Show More
@@ -1,134 +1,134
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 include Redmine::WikiFormatting::LinksHelper
27 27
28 28 alias :inline_auto_link :auto_link!
29 29 alias :inline_auto_mailto :auto_mailto!
30 30
31 31 # auto_link rule after textile rules so that it doesn't break !image_url! tags
32 32 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
33 33
34 34 def initialize(*args)
35 35 super
36 36 self.hard_breaks=true
37 37 self.no_span_caps=true
38 38 self.filter_styles=false
39 39 end
40 40
41 41 def to_html(*rules)
42 42 @toc = []
43 43 super(*RULES).to_s
44 44 end
45 45
46 46 def get_section(index)
47 47 section = extract_sections(index)[1]
48 48 hash = Digest::MD5.hexdigest(section)
49 49 return section, hash
50 50 end
51 51
52 52 def update_section(index, update, hash=nil)
53 53 t = extract_sections(index)
54 54 if hash.present? && hash != Digest::MD5.hexdigest(t[1])
55 55 raise Redmine::WikiFormatting::StaleSectionError
56 56 end
57 57 t[1] = update unless t[1].blank?
58 58 t.reject(&:blank?).join "\n\n"
59 59 end
60 60
61 61 def extract_sections(index)
62 62 @pre_list = []
63 63 text = self.dup
64 64 rip_offtags text, false, false
65 65 before = ''
66 66 s = ''
67 67 after = ''
68 68 i = 0
69 69 l = 1
70 70 started = false
71 71 ended = false
72 text.scan(/(((?:.*?)(\A|\r?\n\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level|
72 text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level|
73 73 if heading.nil?
74 74 if ended
75 75 after << all
76 76 elsif started
77 77 s << all
78 78 else
79 79 before << all
80 80 end
81 81 break
82 82 end
83 83 i += 1
84 84 if ended
85 85 after << all
86 86 elsif i == index
87 87 l = level.to_i
88 88 before << content
89 89 s << heading
90 90 started = true
91 91 elsif i > index
92 92 s << content
93 93 if level.to_i > l
94 94 s << heading
95 95 else
96 96 after << heading
97 97 ended = true
98 98 end
99 99 else
100 100 before << all
101 101 end
102 102 end
103 103 sections = [before.strip, s.strip, after.strip]
104 104 sections.each {|section| smooth_offtags_without_code_highlighting section}
105 105 sections
106 106 end
107 107
108 108 private
109 109
110 110 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
111 111 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
112 112 def hard_break( text )
113 113 text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
114 114 end
115 115
116 116 alias :smooth_offtags_without_code_highlighting :smooth_offtags
117 117 # Patch to add code highlighting support to RedCloth
118 118 def smooth_offtags( text )
119 119 unless @pre_list.empty?
120 120 ## replace <pre> content
121 121 text.gsub!(/<redpre#(\d+)>/) do
122 122 content = @pre_list[$1.to_i]
123 123 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
124 124 content = "<code class=\"#{$1} syntaxhl\">" +
125 125 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
126 126 end
127 127 content
128 128 end
129 129 end
130 130 end
131 131 end
132 132 end
133 133 end
134 134 end
@@ -1,417 +1,442
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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_styles
63 63 # single style
64 64 assert_html_output({
65 65 'p{color:red}. text' => '<p style="color:red;">text</p>',
66 66 'p{color:red;}. text' => '<p style="color:red;">text</p>',
67 67 'p{color: red}. text' => '<p style="color: red;">text</p>',
68 68 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>',
69 69 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>',
70 70 'p{border:10px}. text' => '<p style="border:10px;">text</p>',
71 71 'p{border:10}. text' => '<p style="border:10;">text</p>',
72 72 'p{border:10%}. text' => '<p style="border:10%;">text</p>',
73 73 'p{border:10em}. text' => '<p style="border:10em;">text</p>',
74 74 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>',
75 75 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>',
76 76 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>',
77 77 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>',
78 78 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>',
79 79 }, false)
80 80
81 81 # multiple styles
82 82 assert_html_output({
83 83 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
84 84 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
85 85 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
86 86 }, false)
87 87
88 88 # styles with multiple values
89 89 assert_html_output({
90 90 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>',
91 91 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>',
92 92 }, false)
93 93 end
94 94
95 95 def test_invalid_styles_should_be_filtered
96 96 assert_html_output({
97 97 'p{invalid}. text' => '<p>text</p>',
98 98 'p{invalid:red}. text' => '<p>text</p>',
99 99 'p{color:(red)}. text' => '<p>text</p>',
100 100 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>',
101 101 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>',
102 102 'p{color:"}. text' => '<p>p{color:"}. text</p>',
103 103 }, false)
104 104 end
105 105
106 106 def test_inline_code
107 107 assert_html_output(
108 108 'this is @some code@' => 'this is <code>some code</code>',
109 109 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
110 110 )
111 111 end
112 112
113 113 def test_nested_lists
114 114 raw = <<-RAW
115 115 # Item 1
116 116 # Item 2
117 117 ** Item 2a
118 118 ** Item 2b
119 119 # Item 3
120 120 ** Item 3a
121 121 RAW
122 122
123 123 expected = <<-EXPECTED
124 124 <ol>
125 125 <li>Item 1</li>
126 126 <li>Item 2
127 127 <ul>
128 128 <li>Item 2a</li>
129 129 <li>Item 2b</li>
130 130 </ul>
131 131 </li>
132 132 <li>Item 3
133 133 <ul>
134 134 <li>Item 3a</li>
135 135 </ul>
136 136 </li>
137 137 </ol>
138 138 EXPECTED
139 139
140 140 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
141 141 end
142 142
143 143 def test_escaping
144 144 assert_html_output(
145 145 'this is a <script>' => 'this is a &lt;script&gt;'
146 146 )
147 147 end
148 148
149 149 def test_use_of_backslashes_followed_by_numbers_in_headers
150 150 assert_html_output({
151 151 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
152 152 }, false)
153 153 end
154 154
155 155 def test_double_dashes_should_not_strikethrough
156 156 assert_html_output(
157 157 'double -- dashes -- test' => 'double -- dashes -- test',
158 158 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
159 159 )
160 160 end
161 161
162 162 def test_acronyms
163 163 assert_html_output(
164 164 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
165 165 '2 letters JP(Jean-Philippe) acronym' => '2 letters <acronym title="Jean-Philippe">JP</acronym> acronym',
166 166 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>'
167 167 )
168 168 end
169 169
170 170 def test_blockquote
171 171 # orig raw text
172 172 raw = <<-RAW
173 173 John said:
174 174 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
175 175 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
176 176 > * Donec odio lorem,
177 177 > * sagittis ac,
178 178 > * malesuada in,
179 179 > * adipiscing eu, dolor.
180 180 >
181 181 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
182 182 > Proin a tellus. Nam vel neque.
183 183
184 184 He's right.
185 185 RAW
186 186
187 187 # expected html
188 188 expected = <<-EXPECTED
189 189 <p>John said:</p>
190 190 <blockquote>
191 191 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
192 192 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
193 193 <ul>
194 194 <li>Donec odio lorem,</li>
195 195 <li>sagittis ac,</li>
196 196 <li>malesuada in,</li>
197 197 <li>adipiscing eu, dolor.</li>
198 198 </ul>
199 199 <blockquote>
200 200 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
201 201 </blockquote>
202 202 <p>Proin a tellus. Nam vel neque.</p>
203 203 </blockquote>
204 204 <p>He's right.</p>
205 205 EXPECTED
206 206
207 207 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
208 208 end
209 209
210 210 def test_table
211 211 raw = <<-RAW
212 212 This is a table with empty cells:
213 213
214 214 |cell11|cell12||
215 215 |cell21||cell23|
216 216 |cell31|cell32|cell33|
217 217 RAW
218 218
219 219 expected = <<-EXPECTED
220 220 <p>This is a table with empty cells:</p>
221 221
222 222 <table>
223 223 <tr><td>cell11</td><td>cell12</td><td></td></tr>
224 224 <tr><td>cell21</td><td></td><td>cell23</td></tr>
225 225 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
226 226 </table>
227 227 EXPECTED
228 228
229 229 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
230 230 end
231 231
232 232 def test_table_with_line_breaks
233 233 raw = <<-RAW
234 234 This is a table with line breaks:
235 235
236 236 |cell11
237 237 continued|cell12||
238 238 |-cell21-||cell23
239 239 cell23 line2
240 240 cell23 *line3*|
241 241 |cell31|cell32
242 242 cell32 line2|cell33|
243 243
244 244 RAW
245 245
246 246 expected = <<-EXPECTED
247 247 <p>This is a table with line breaks:</p>
248 248
249 249 <table>
250 250 <tr>
251 251 <td>cell11<br />continued</td>
252 252 <td>cell12</td>
253 253 <td></td>
254 254 </tr>
255 255 <tr>
256 256 <td><del>cell21</del></td>
257 257 <td></td>
258 258 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
259 259 </tr>
260 260 <tr>
261 261 <td>cell31</td>
262 262 <td>cell32<br/>cell32 line2</td>
263 263 <td>cell33</td>
264 264 </tr>
265 265 </table>
266 266 EXPECTED
267 267
268 268 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
269 269 end
270 270
271 271 def test_textile_should_not_mangle_brackets
272 272 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
273 273 end
274 274
275 275 def test_textile_should_escape_image_urls
276 276 # this is onclick="alert('XSS');" in encoded form
277 277 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
278 278 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>'
279 279 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
280 280 end
281 281
282 282
283 283 STR_WITHOUT_PRE = [
284 284 # 0
285 285 "h1. Title
286 286
287 287 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
288 288 # 1
289 289 "h2. Heading 2
290 290
291 291 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
292 292
293 293 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
294 294 # 2
295 295 "h2. Heading 2
296 296
297 297 Morbi facilisis accumsan orci non pharetra.
298 298
299 299 h3. Heading 3
300 300
301 301 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
302 302 # 3
303 303 "h3. Heading 3
304 304
305 305 Praesent eget turpis nibh, a lacinia nulla.",
306 306 # 4
307 307 "h2. Heading 2
308 308
309 309 Ut rhoncus elementum adipiscing."]
310 310
311 311 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
312 312
313 313 def test_get_section_should_return_the_requested_section_and_its_hash
314 314 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
315 315 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
316 316 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
317 317 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
318 318
319 319 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
320 320 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
321 321 end
322 322
323 323 def test_update_section_should_update_the_requested_section
324 324 replacement = "New text"
325 325
326 326 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)
327 327 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)
328 328 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)
329 329 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
330 330
331 331 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
332 332 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
333 333 end
334 334
335 335 def test_update_section_with_hash_should_update_the_requested_section
336 336 replacement = "New text"
337 337
338 338 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
339 339 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
340 340 end
341 341
342 342 def test_update_section_with_wrong_hash_should_raise_an_error
343 343 assert_raise Redmine::WikiFormatting::StaleSectionError do
344 344 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
345 345 end
346 346 end
347 347
348 348 STR_WITH_PRE = [
349 349 # 0
350 350 "h1. Title
351 351
352 352 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
353 353 # 1
354 354 "h2. Heading 2
355 355
356 356 <pre><code class=\"ruby\">
357 357 def foo
358 358 end
359 359 </code></pre>
360 360
361 361 <pre><code><pre><code class=\"ruby\">
362 362 Place your code here.
363 363 </code></pre>
364 364 </code></pre>
365 365
366 366 Morbi facilisis accumsan orci non pharetra.
367 367
368 368 <pre>
369 369 Pre Content:
370 370
371 371 h2. Inside pre
372 372
373 373 <tag> inside pre block
374 374
375 375 Morbi facilisis accumsan orci non pharetra.
376 376 </pre>",
377 377 # 2
378 378 "h3. Heading 3
379 379
380 380 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
381 381
382 382 def test_get_section_should_ignore_pre_content
383 383 text = STR_WITH_PRE.join("\n\n")
384 384
385 385 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
386 386 assert_section_with_hash STR_WITH_PRE[2], text, 3
387 387 end
388 388
389 389 def test_update_section_should_not_escape_pre_content_outside_section
390 390 text = STR_WITH_PRE.join("\n\n")
391 391 replacement = "New text"
392 392
393 393 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
394 394 @formatter.new(text).update_section(3, replacement)
395 395 end
396 396
397 def test_get_section_should_support_lines_with_spaces_before_heading
398 # the lines after Content 2 and Heading 4 contain a space
399 text = <<-STR
400 h1. Heading 1
401
402 Content 1
403
404 h1. Heading 2
405
406 Content 2
407
408 h1. Heading 3
409
410 Content 3
411
412 h1. Heading 4
413
414 Content 4
415 STR
416
417 [1, 2, 3, 4].each do |index|
418 assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first
419 end
420 end
421
397 422 private
398 423
399 424 def assert_html_output(to_test, expect_paragraph = true)
400 425 to_test.each do |text, expected|
401 426 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
402 427 end
403 428 end
404 429
405 430 def to_html(text)
406 431 @formatter.new(text).to_html
407 432 end
408 433
409 434 def assert_section_with_hash(expected, text, index)
410 435 result = @formatter.new(text).get_section(index)
411 436
412 437 assert_kind_of Array, result
413 438 assert_equal 2, result.size
414 439 assert_equal expected, result.first, "section content did not match"
415 440 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
416 441 end
417 442 end
General Comments 0
You need to be logged in to leave comments. Login now