????
Current Path : C:/inetpub/vhost/redmine/test/unit/lib/redmine/wiki_formatting/ |
Current File : C:/inetpub/vhost/redmine/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb |
# frozen_string_literal: true # # Redmine - project management software # Copyright (C) 2006-2023 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require File.expand_path('../../../../../test_helper', __FILE__) require 'digest/md5' class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase def setup @formatter = Redmine::WikiFormatting::Textile::Formatter end MODIFIERS = { "*" => 'strong', # bold "_" => 'em', # italic "+" => 'ins', # underline "-" => 'del', # deleted "^" => 'sup', # superscript "~" => 'sub' # subscript } def test_modifiers assert_html_output( '*bold*' => '<strong>bold</strong>', 'before *bold*' => 'before <strong>bold</strong>', '*bold* after' => '<strong>bold</strong> after', '*two words*' => '<strong>two words</strong>', '*two*words*' => '<strong>two*words</strong>', '*two * words*' => '<strong>two * words</strong>', '*two* *words*' => '<strong>two</strong> <strong>words</strong>', '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>' ) end def test_modifiers_combination MODIFIERS.each do |m1, tag1| MODIFIERS.each do |m2, tag2| next if m1 == m2 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}" html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>" assert_html_output text => html end end end def test_modifier_should_work_with_one_non_ascii_character assert_html_output "*Ä*" => "<strong>Ä</strong>" end def test_styles # single style assert_html_output( { 'p{color:red}. text' => '<p style="color:red;">text</p>', 'p{color:red;}. text' => '<p style="color:red;">text</p>', 'p{color: red}. text' => '<p style="color: red;">text</p>', 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>', 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>', 'p{border:10px}. text' => '<p style="border:10px;">text</p>', 'p{border:10}. text' => '<p style="border:10;">text</p>', 'p{border:10%}. text' => '<p style="border:10%;">text</p>', 'p{border:10em}. text' => '<p style="border:10em;">text</p>', 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>', 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>', 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>', 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>', 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>', 'p{width:50px}. text' => '<p style="width:50px;">text</p>', 'p{max-width:100px}. text' => '<p style="max-width:100px;">text</p>', 'p{height:40px}. text' => '<p style="height:40px;">text</p>', 'p{max-height:80px}. text' => '<p style="max-height:80px;">text</p>', }, false ) # multiple styles assert_html_output( { 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>', 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>', 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>', }, false ) # styles with multiple values assert_html_output( { 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>', 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>', }, false ) end def test_invalid_styles_should_be_filtered assert_html_output( { 'p{invalid}. text' => '<p>text</p>', 'p{invalid:red}. text' => '<p>text</p>', 'p{color:(red)}. text' => '<p>text</p>', 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>', 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>', 'p{color:"}. text' => '<p>p{color:"}. text</p>', }, false ) end def test_inline_code assert_html_output( 'this is @some code@' => 'this is <code>some code</code>', '@<Location /redmine>@' => '<code><Location /redmine></code>' ) end def test_lang_attribute assert_html_output( '*[fr]French*' => '<strong lang="fr">French</strong>', '*[fr-fr]French*' => '<strong lang="fr-fr">French</strong>', '*[fr_fr]French*' => '<strong lang="fr_fr">French</strong>' ) end def test_lang_attribute_should_ignore_invalid_value assert_html_output( '*[fr3]French*' => '<strong>[fr3]French</strong>' ) end def test_nested_lists raw = <<~RAW # Item 1 # Item 2 ** Item 2a ** Item 2b # Item 3 ** Item 3a RAW expected = <<~EXPECTED <ol> <li>Item 1</li> <li>Item 2 <ul> <li>Item 2a</li> <li>Item 2b</li> </ul> </li> <li>Item 3 <ul> <li>Item 3a</li> </ul> </li> </ol> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') raw = <<~RAW * Item-1 * Item-1a * Item-1b RAW expected = <<~EXPECTED <ul> <li>Item-1 <ul> <li>Item-1a</li> <li>Item-1b</li> </ul> </li> </ul> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_escaping assert_html_output( 'this is a <script>' => 'this is a <script>' ) end def test_kbd assert_html_output( { '<kbd>test</kbd>' => '<kbd>test</kbd>' }, false ) end def test_use_of_backslashes_followed_by_numbers_in_headers assert_html_output( { 'h1. 2009\02\09' => '<h1>2009\02\09</h1>' }, false ) end def test_double_dashes_should_not_strikethrough assert_html_output( 'double -- dashes -- test' => 'double -- dashes -- test', 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test' ) end def test_abbreviations assert_html_output( 'this is an abbreviation: GPL(General Public License)' => 'this is an abbreviation: <abbr title="General Public License">GPL</abbr>', '2 letters JP(Jean-Philippe) abbreviation' => '2 letters <abbr title="Jean-Philippe">JP</abbr> abbreviation', 'GPL(This is a double-quoted "title")' => '<abbr title="This is a double-quoted "title"">GPL</abbr>' ) end def test_blockquote # orig raw text raw = <<~RAW John said: > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. > * Donec odio lorem, > * sagittis ac, > * malesuada in, > * adipiscing eu, dolor. > > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus. > Proin a tellus. Nam vel neque. He's right. RAW # expected html expected = <<~EXPECTED <p>John said:</p> <blockquote> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br /> Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. <ul> <li>Donec odio lorem,</li> <li>sagittis ac,</li> <li>malesuada in,</li> <li>adipiscing eu, dolor.</li> </ul> <blockquote> <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p> </blockquote> <p>Proin a tellus. Nam vel neque.</p> </blockquote> <p>He's right.</p> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_table raw = <<~RAW This is a table with empty cells: |cell11|cell12|| |cell21||cell23| |cell31|cell32|cell33| RAW expected = <<~EXPECTED <p>This is a table with empty cells:</p> <table> <tr><td>cell11</td><td>cell12</td><td></td></tr> <tr><td>cell21</td><td></td><td>cell23</td></tr> <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr> </table> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_table_with_alignment raw = <<~RAW |>. right| |<. left| |<>. justify| RAW expected = <<~EXPECTED <table> <tr><td style="text-align:right;">right</td></tr> <tr><td style="text-align:left;">left</td></tr> <tr><td style="text-align:justify;">justify</td></tr> </table> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_table_with_trailing_whitespace raw = <<~RAW This is a table with trailing whitespace in one row: |cell11|cell12| |cell21|cell22| |cell31|cell32| RAW expected = <<~EXPECTED <p>This is a table with trailing whitespace in one row:</p> <table> <tr><td>cell11</td><td>cell12</td></tr> <tr><td>cell21</td><td>cell22</td></tr> <tr><td>cell31</td><td>cell32</td></tr> </table> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_table_with_line_breaks raw = <<~RAW This is a table with line breaks: |cell11 continued|cell12|| |-cell21-||cell23 cell23 line2 cell23 *line3*| |cell31|cell32 cell32 line2|cell33| RAW expected = <<~EXPECTED <p>This is a table with line breaks:</p> <table> <tr> <td>cell11<br />continued</td> <td>cell12</td> <td></td> </tr> <tr> <td><del>cell21</del></td> <td></td> <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td> </tr> <tr> <td>cell31</td> <td>cell32<br/>cell32 line2</td> <td>cell33</td> </tr> </table> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_tables_with_lists raw = <<~RAW This is a table with lists: |cell11|cell12| |cell21|ordered list # item # item 2| |cell31|unordered list * item * item 2| RAW expected = <<~EXPECTED <p>This is a table with lists:</p> <table> <tr> <td>cell11</td> <td>cell12</td> </tr> <tr> <td>cell21</td> <td>ordered list<br /># item<br /># item 2</td> </tr> <tr> <td>cell31</td> <td>unordered list<br />* item<br />* item 2</td> </tr> </table> EXPECTED assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end def test_textile_should_not_mangle_brackets assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]') end def test_textile_should_escape_image_urls # this is onclick="alert('XSS');" in encoded form raw = '!/images/comment.png"onclick=alert('XSS');"!' expected = '<p><img src="/images/comment.png"onclick=' \ '&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;' \ '&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;" alt="" /></p>' assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') end STR_WITHOUT_PRE = [ # 0 <<~STR.chomp, h1. Title Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. STR # 1 <<~STR.chomp, h2. Heading 2 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc. STR # 2 <<~STR.chomp, h2. Heading 2 Morbi facilisis accumsan orci non pharetra. h3. Heading 3 Nulla nunc nisi, egestas in ornare vel, posuere ac libero. STR # 3 <<~STR.chomp, h3. Heading 3 Praesent eget turpis nibh, a lacinia nulla. STR # 4 <<~STR.chomp, h2. Heading 2 Ut rhoncus elementum adipiscing. STR ] TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze def test_get_section_should_return_the_requested_section_and_its_hash assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10 end def test_update_section_should_update_the_requested_section replacement = "New text" 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) ) 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) ) 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) ) assert_equal( [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement) ) assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement) assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement) end def test_update_section_with_hash_should_update_the_requested_section replacement = "New text" 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, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1])) ) end def test_update_section_with_wrong_hash_should_raise_an_error assert_raise Redmine::WikiFormatting::StaleSectionError do @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text")) end end STR_WITH_PRE = [ # 0 <<~STR.chomp, h1. Title Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. STR # 1 <<~STR.chomp, h2. Heading 2 <pre><code class=\"ruby\"> def foo end </code></pre> <pre><code><pre><code class=\"ruby\"> Place your code here. </code></pre> </code></pre> Morbi facilisis accumsan orci non pharetra. <pre> Pre Content: h2. Inside pre <tag> inside pre block Morbi facilisis accumsan orci non pharetra. </pre> STR # 2 <<~STR.chomp, h3. Heading 3 Nulla nunc nisi, egestas in ornare vel, posuere ac libero. STR ] def test_get_section_should_ignore_pre_content text = STR_WITH_PRE.join("\n\n") assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2 assert_section_with_hash STR_WITH_PRE[2], text, 3 end def test_update_section_should_not_escape_pre_content_outside_section text = STR_WITH_PRE.join("\n\n") replacement = "New text" assert_equal( [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"), @formatter.new(text).update_section(3, replacement) ) end def test_get_section_should_support_lines_with_spaces_before_heading # the lines after Content 2 and Heading 4 contain a space text = <<~STR h1. Heading 1 Content 1 h1. Heading 2 Content 2 h1. Heading 3 Content 3 h1. Heading 4 Content 4 STR [1, 2, 3, 4].each do |index| assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first end end def test_get_section_should_support_headings_starting_with_a_tab text = <<~STR h1.\tHeading 1 Content 1 h1. Heading 2 Content 2 STR assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first end def test_should_not_allow_arbitrary_class_attribute_on_offtags { "class=\"foo\"" => "data-language=\"foo\"", "class='foo'" => "data-language=\"foo\"", "class=\"ruby foo\"" => "data-language=\"ruby foo\"", "class='ruby foo'" => "data-language=\"ruby foo\"", "class=\"ruby \"foo\" bar\"" => "data-language=\"ruby \"", }.each do |classattr, codeattr| assert_html_output({"<code #{classattr}>test</code>" => "<code #{codeattr}>test</code>"}, false) assert_html_output({"<pre #{classattr}>test</pre>" => "<pre>test</pre>"}, false) assert_html_output({"<kbd #{classattr}>test</kbd>" => "<kbd>test</kbd>"}, false) end assert_html_output({"<notextile class=\"foo\">test</notextile>" => "test"}, false) assert_html_output({"<notextile class='foo'>test</notextile>" => "test"}, false) assert_html_output({"<notextile class=\"ruby foo\">test</notextile>" => "test"}, false) assert_html_output({"<notextile class='ruby foo'>test</notextile>" => "test"}, false) assert_html_output({"<notextile class=\"ruby \"foo\" bar\">test</notextile>" => "test"}, false) end def test_should_allow_valid_language_class_attribute_on_code_tags # language name is double-quoted assert_html_output( {"<code class=\"ruby\">test</code>" => "<code class=\"ruby syntaxhl\" data-language=\"ruby\"><span class=\"nb\">test</span></code>"}, false ) # language name is single-quoted assert_html_output( {"<code class='ruby'>test</code>" => "<code class=\"ruby syntaxhl\" data-language=\"ruby\"><span class=\"nb\">test</span></code>"}, false ) end def test_should_preserve_code_language_class_attribute_in_data_language assert_html_output( { "<code class=\"foolang\">unsupported language</code>" => "<code data-language=\"foolang\">unsupported language</code>", "<code class=\"c-k&r\">special-char language</code>" => "<code data-language=\"c-k&r\">special-char language</code>", }, false ) end def test_should_not_allow_valid_language_class_attribute_on_non_code_offtags %w(pre kbd).each do |tag| assert_html_output({"<#{tag} class=\"ruby\">test</#{tag}>" => "<#{tag}>test</#{tag}>"}, false) end assert_html_output({"<notextile class=\"ruby\">test</notextile>" => "test"}, false) end def test_should_prefix_class_attribute_on_tags assert_html_output( { '!(foo)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" alt=\"\" /></p>", '%(foo)test%' => "<p><span class=\"wiki-class-foo\">test</span></p>", 'p(foo). test' => "<p class=\"wiki-class-foo\">test</p>", '|(foo). test|' => "<table>\n\t\t<tr>\n\t\t\t<td class=\"wiki-class-foo\">test</td>\n\t\t</tr>\n\t</table>", }, false ) end def test_should_prefix_id_attribute_on_tags assert_html_output( { '!(#foo)test.png!' => "<p><img src=\"test.png\" id=\"wiki-id-foo\" alt=\"\" /></p>", '%(#foo)test%' => "<p><span id=\"wiki-id-foo\">test</span></p>", 'p(#foo). test' => "<p id=\"wiki-id-foo\">test</p>", '|(#foo). test|' => "<table>\n\t\t<tr>\n\t\t\t<td id=\"wiki-id-foo\">test</td>\n\t\t</tr>\n\t</table>", }, false ) end def test_should_not_prefix_class_and_id_attributes_already_prefixed assert_html_output( { '!(wiki-class-foo#wiki-id-bar)test.png!' => "<p><img src=\"test.png\" class=\"wiki-class-foo\" id=\"wiki-id-bar\" alt=\"\" /></p>", }, false ) end def test_footnotes text = <<~STR This is some text[1]. fn1. This is the foot note STR expected = <<~EXPECTED <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p> <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p> EXPECTED assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '') end # TODO: Remove this test after migrating to RedCloth 4 def test_should_not_crash_with_special_input assert_nothing_raised {to_html(" \f")} assert_nothing_raised {to_html(" \v")} end def test_should_not_handle_as_preformatted_text_tags_that_starts_with_pre text = <<~STR <pree> This is some text </pree> STR expected = <<~EXPECTED <p><pree><br /> This is some text<br /> </pree></p> EXPECTED assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '') end def test_should_escape_tags_that_start_with_pre text = <<~STR <preä demo>Text STR expected = <<~EXPECTED <p><preä demo>Text</p> EXPECTED assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '') end def test_should_remove_html_comments text = <<~STR <!-- begin --> Hello <!-- comment between words -->world. <!-- multi-line comment -->Foo <pre> This is a code block. <p> <!-- comments in a code block should be preserved --> </p> </pre> STR expected = <<~EXPECTED <p>Hello world.</p> <p>Foo</p> <pre> This is a code block. <p> <!-- comments in a code block should be preserved --> </p> </pre> EXPECTED assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '') end def test_should_escape_bq_citations assert_html_output( { %{bq.:http://x/"onmouseover="alert(document.domain) Hover me} => %{<blockquote cite="http://x/"onmouseover="alert(document.domain)">\n\t\t<p>Hover me</p>\n\t</blockquote>} }, false) end private def assert_html_output(to_test, expect_paragraph = true) to_test.each do |text, expected| assert_equal( (expect_paragraph ? "<p>#{expected}</p>" : expected), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n" ) end end def to_html(text) @formatter.new(text).to_html end def assert_section_with_hash(expected, text, index) result = @formatter.new(text).get_section(index) assert_kind_of Array, result assert_equal 2, result.size assert_equal expected, result.first, "section content did not match" assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match" end end