diff --git a/.circleci/config.yml b/.circleci/config.yml index f6429ee..15d15c1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -51,6 +51,10 @@ jobs: docker: - image: 'cimg/php:8.0' steps: *php_unit_test_steps + php_81_tests: + docker: + - image: 'cimg/php:8.1' + steps: *php_unit_test_steps workflows: version: 2 commit: @@ -68,3 +72,6 @@ workflows: - php_80_tests: requires: - php_setup + - php_81_tests: + requires: + - php_setup diff --git a/src/BBCode.php b/src/BBCode.php index 219c721..4097d27 100644 --- a/src/BBCode.php +++ b/src/BBCode.php @@ -762,7 +762,7 @@ public function nl2br($string) { // and performs exactly the same function. Unlike html_entity_decode, it // works on older versions of PHP (prior to 4.3.0). public function unHTMLEncode($string) { - return html_entity_decode($string); + return html_entity_decode($string, ENT_QUOTES); } @@ -903,7 +903,7 @@ public function isValidEmail($string) { public function htmlEncode($string) { if ($this->escape_content) { if (!$this->allow_ampersand) { - return htmlspecialchars($string); + return htmlspecialchars($string, ENT_QUOTES); } else { return str_replace(['<', '>', '"'], ['<', '>', '"'], $string); } @@ -995,9 +995,9 @@ protected function processSmileys($string) { // For non-smiley text, we just pass it through htmlspecialchars. $output .= $this->htmlEncode($token); } else { - $alt = htmlspecialchars($token); + $alt = htmlspecialchars($token, ENT_QUOTES); if ($smiley_count < $this->max_smileys || $this->max_smileys < 0) { - $output .= "smiley_url.'/'.$this->smileys[$token]).'"' + $output .= "smiley_url.'/'.$this->smileys[$token], ENT_QUOTES).'"' // . "\" width=\"{$info[ 0 ]}\" height=\"{$info[ 1 ]}\"" ." alt=\"$alt\" title=\"$alt\" class=\"bbcode_smiley\" />"; } else { @@ -1310,7 +1310,7 @@ public function fillTemplate($template, $insert_array, $default_array = []) { } elseif (isset($flags['k'])) { $value = $this->wikify($value); } elseif (isset($flags['h'])) { - $value = htmlspecialchars($value); + $value = htmlspecialchars($value, ENT_QUOTES); } elseif (isset($flags['u'])) { $value = urlencode($value); } @@ -1658,7 +1658,7 @@ protected function dumpStack($array = false, $raw = false) { switch ($item[self::BBCODE_STACK_TOKEN]) { case self::BBCODE_TEXT: - $string .= "\"".htmlspecialchars($item[self::BBCODE_STACK_TEXT])."\" "; + $string .= "\"".htmlspecialchars($item[self::BBCODE_STACK_TEXT], ENT_QUOTES)."\" "; break; case self::BBCODE_WS: $string .= "WS "; @@ -1667,7 +1667,7 @@ protected function dumpStack($array = false, $raw = false) { $string .= "NL "; break; case self::BBCODE_TAG: - $string .= "[".htmlspecialchars($item[self::BBCODE_STACK_TAG]['_name'])."] "; + $string .= "[".htmlspecialchars($item[self::BBCODE_STACK_TAG]['_name'], ENT_QUOTES)."] "; break; default: $string .= "unknown "; @@ -1853,7 +1853,7 @@ protected function doLimit() { // // $params is an array of key => value parameters associated with the tag; for example, // in [smiley src=smile alt=:-)], it's Array('src' => "smile", 'alt' => ":-)"). - // These keys and values have NOT beel passed through htmlspecialchars(). + // These keys and values have NOT been passed through htmlspecialchars(). // // $contents is the body of the tag during BBCODE_OUTPUT. For example, in // [b]Hello[/b], it's "Hello". THIS VALUE IS ALWAYS HTML, not BBCode. @@ -1970,7 +1970,7 @@ public function doTag($action, $tag_name, $default_value, $params, $contents) { break; } if (isset($params[$possible_content]) && strlen($params[$possible_content]) > 0) { - $result = htmlspecialchars($params[$possible_content]); + $result = htmlspecialchars($params[$possible_content], ENT_QUOTES); break; } } @@ -2211,7 +2211,7 @@ protected function processVerbatimTag($tag_name, $tag_params, $tag_rule) { $this->stack[] = Array( self::BBCODE_STACK_TOKEN => $token_type, - self::BBCODE_STACK_TEXT => htmlspecialchars($this->lexer->text), + self::BBCODE_STACK_TEXT => htmlspecialchars($this->lexer->text, ENT_QUOTES), self::BBCODE_STACK_TAG => $this->lexer->tag, self::BBCODE_STACK_CLASS => $this->current_class, ); diff --git a/src/BBCodeLibrary.php b/src/BBCodeLibrary.php index cf068be..e206e61 100644 --- a/src/BBCodeLibrary.php +++ b/src/BBCodeLibrary.php @@ -450,13 +450,13 @@ public function doURL(BBCode $bbcode, $action, $name, $default, $params, $conten } if ($bbcode->getURLTargetable() !== false && isset($params['target'])) { - $target = ' target="'.htmlspecialchars($params['target']).'"'; + $target = ' target="'.htmlspecialchars($params['target'], ENT_QUOTES).'"'; } else { $target = ''; } if ($bbcode->getURLTarget() !== false && empty($target)) { - $target = ' target="'.htmlspecialchars($bbcode->getURLTarget()).'"'; + $target = ' target="'.htmlspecialchars($bbcode->getURLTarget(), ENT_QUOTES).'"'; } // If $detect_urls is on, it's possble the $content is already @@ -465,7 +465,7 @@ public function doURL(BBCode $bbcode, $action, $name, $default, $params, $conten return $bbcode->fillTemplate($bbcode->getURLTemplate(), array("url" => $url, "target" => $target, "content" => $content)); } else { - return htmlspecialchars($params['_tag']).$content.htmlspecialchars($params['_endtag']); + return htmlspecialchars($params['_tag'], ENT_QUOTES).$content.htmlspecialchars($params['_endtag'], ENT_QUOTES); } } @@ -498,7 +498,7 @@ public function doEmail(BBCode $bbcode, $action, $name, $default, $params, $cont if ($bbcode->isValidEmail($email)) { return $bbcode->fillTemplate($bbcode->getEmailTemplate(), array("email" => $email, "content" => $content)); } else { - return htmlspecialchars($params['_tag']).$content.htmlspecialchars($params['_endtag']); + return htmlspecialchars($params['_tag'], ENT_QUOTES).$content.htmlspecialchars($params['_endtag'], ENT_QUOTES); } } @@ -662,16 +662,16 @@ public function doImage(BBCode $bbcode, $action, $name, $default, $params, $cont $localImgURL = $bbcode->getLocalImgURL(); return "'
-                .htmlspecialchars(basename($content)).''; + .htmlspecialchars((empty($localImgURL) ? '' : $localImgURL.'/').ltrim($urlParts['path'], '/'), ENT_QUOTES).'" alt="' + .htmlspecialchars(basename($content), ENT_QUOTES).'" class="bbcode_img" />'; } elseif ($bbcode->isValidURL($content, false)) { // Remote URL, or at least we don't know where it is. - return ''
-                .htmlspecialchars(basename($content)).''; + return ''
+                .htmlspecialchars(basename($content), ENT_QUOTES).''; } } - return htmlspecialchars($params['_tag']).htmlspecialchars($content).htmlspecialchars($params['_endtag']); + return htmlspecialchars($params['_tag'], ENT_QUOTES).htmlspecialchars($content, ENT_QUOTES).htmlspecialchars($params['_endtag'], ENT_QUOTES); } /** @@ -730,21 +730,21 @@ public function doQuote(BBCode $bbcode, $action, $name, $default, $params, $cont } if (isset($params['name'])) { - $title = htmlspecialchars(trim($params['name']))." wrote"; + $title = htmlspecialchars(trim($params['name']), ENT_QUOTES)." wrote"; if (isset($params['date'])) { - $title .= " on ".htmlspecialchars(trim($params['date'])); + $title .= " on ".htmlspecialchars(trim($params['date']), ENT_QUOTES); } $title .= ":"; if (isset($params['url'])) { $url = trim($params['url']); if ($bbcode->isValidURL($url)) { - $title = "".$title.""; + $title = "".$title.""; } } } elseif (!is_string($default)) { $title = "Quote:"; } else { - $title = htmlspecialchars(trim($default))." wrote:"; + $title = htmlspecialchars(trim($default), ENT_QUOTES)." wrote:"; } return $bbcode->fillTemplate($bbcode->getQuoteTemplate(), array("title" => $title, "content" => $content)); diff --git a/tests/ConformanceTest.php b/tests/ConformanceTest.php index 0f8d891..8a46569 100644 --- a/tests/ConformanceTest.php +++ b/tests/ConformanceTest.php @@ -52,14 +52,14 @@ public function provideInputValidationTests() { 'html' => "This is [/ a tag.", ]], [[ - 'descr' => "Broken [ tags before [b]real tags[/b] don't break the real tags.", - 'bbcode' => "Broken [ tags before [b]real tags[/b] don't break the real tags.", - 'html' => "Broken [ tags before real tags don't break the real tags.", + 'descr' => "Broken [ tags before [b]real tags[/b] do not break the real tags.", + 'bbcode' => "Broken [ tags before [b]real tags[/b] do not break the real tags.", + 'html' => "Broken [ tags before real tags do not break the real tags.", ]], [[ - 'descr' => "Broken [tags before [b]real tags[/b] don't break the real tags.", - 'bbcode' => "Broken [tags before [b]real tags[/b] don't break the real tags.", - 'html' => "Broken [tags before real tags don't break the real tags.", + 'descr' => "Broken [tags before [b]real tags[/b] do not break the real tags.", + 'bbcode' => "Broken [tags before [b]real tags[/b] do not break the real tags.", + 'html' => "Broken [tags before real tags do not break the real tags.", ]], [[ 'descr' => "[i][b]Mis-ordered nesting[/i][/b] gets fixed.", @@ -119,9 +119,25 @@ public function provideInputValidationTests() { public function provideSpecialCharacterTests() { $result = [ [[ - 'descr' => "& and < and > and \" get replaced with HTML-safe equivalents.", + 'descr' => "& and < and > and \" and ' get replaced with HTML-safe equivalents.", 'bbcode' => "This &\"yeah!\" 'sizzle'", - 'html' => "This <woo!> &"yeah!" 'sizzle'", + 'html' => "This <woo!> &"yeah!" 'sizzle'", + ]], + [[ + 'descr' => "& and < and > and \" and ' do NOT get replaced with HTML-safe equivalents if setEscapeContent(false).", + 'bbcode' => "This &\"yeah!\" 'sizzle'", + 'html' => "This &\"yeah!\" 'sizzle'", + 'escape_content' => false, + ]], + [[ + 'descr' => "Single quotes in tags are NOT considered special characters.", + 'bbcode' => "[wiki='foo' title='bar']", + 'html' => "bar", + ]], + [[ + 'descr' => "Double quotes in tags are NOT considered special characters.", + 'bbcode' => "[wiki=\"foo\" title=\"bar\"]", + 'html' => "bar", ]], [[ 'descr' => ":-) produces a smiley element.", @@ -189,7 +205,7 @@ public function provideSpecialCharacterTests() { [[ 'descr' => "['] comments may *not* contain newlines.", 'bbcode' => "This is a test of the [' emergency\n\rbroadcasting] system.", - 'html' => "This is a test of the [' emergency
\nbroadcasting] system.", + 'html' => "This is a test of the [' emergency
\nbroadcasting] system.", ]], [[ 'descr' => "[!-- --] produces a comment.", @@ -378,10 +394,15 @@ public function provideInlineConversionTests() { 'html' => "This is a test of the emergency broadcasting system.", ]], [[ - 'descr' => "[font=\"Courier New\"] gets correctly converted (quoted default value).", + 'descr' => "[font=\"Courier New\"] gets correctly converted (double quoted default value).", 'bbcode' => "This is a test of the [font=\"Courier New\"]emergency broadcasting system[/font].", 'html' => "This is a test of the emergency broadcasting system.", ]], + [[ + 'descr' => "[font='Courier New'] gets correctly converted (single quoted default value).", + 'bbcode' => "This is a test of the [font='Courier New']emergency broadcasting system[/font].", + 'html' => "This is a test of the emergency broadcasting system.", + ]], [[ 'descr' => "[font=\"Courier New\" blarg size=1] gets converted (floating parameter ignored).", 'bbcode' => "This is a test of the [font=\"Courier New\" blarg size=1]emergency broadcasting system[/font].", @@ -425,7 +446,7 @@ public function provideInlineConversionTests() { [[ 'descr' => "[spoiler] gets converted.", 'bbcode' => "Ssh, don't tell, but [spoiler]Darth is Luke's father[/spoiler]!", - 'html' => "Ssh, don't tell, but Darth is Luke's father!", + 'html' => "Ssh, don't tell, but Darth is Luke's father!", ]], [[ 'descr' => "[acronym] gets converted.", @@ -721,7 +742,7 @@ public function provideUrlLikeTagTests() { [[ 'descr' => "The [[wiki]] special tag does not convert [a-zA-Z0-9'\".:_-].", 'bbcode' => "This is a test of the [[\"Ab1cd'Ef2gh_Ij3kl.,Mn4op:Qr9st-Uv0wx\"]] tag.", - 'html' => "This is a test of the "Ab1cd'Ef2gh_Ij3kl.,Mn4op:Qr9st-Uv0wx" tag.", + 'html' => "This is a test of the "Ab1cd'Ef2gh_Ij3kl.,Mn4op:Qr9st-Uv0wx" tag.", ]], [[ 'descr' => "The [[wiki]] special tag can contain spaces.", @@ -761,23 +782,23 @@ public function provideImageTests() { $result = [ [[ 'descr' => "[img] produces an image.", - 'bbcode' => "This is Google's logo: [img]http://www.google.com/intl/en_ALL/images/logo.gif[/img].", - 'html' => "This is Google's logo: \"logo.gif\".", + 'bbcode' => "This is the Google logo: [img]http://www.google.com/intl/en_ALL/images/logo.gif[/img].", + 'html' => "This is the Google logo: \"logo.gif\".", ]], [[ 'descr' => "[img] disallows a javascript: URL.", - 'bbcode' => "This is Google's logo: [img]javascript:alert()[/img].", - 'html' => "This is Google's logo: [img]javascript:alert()[/img].", + 'bbcode' => "This is the Google logo: [img]javascript:alert()[/img].", + 'html' => "This is the Google logo: [img]javascript:alert()[/img].", ]], [[ 'descr' => "[img] disallows a URL with an unknown protocol type.", - 'bbcode' => "This is Google's logo: [img]foobar:bar.jpg[/img].", - 'html' => "This is Google's logo: [img]foobar:bar.jpg[/img].", + 'bbcode' => "This is the Google logo: [img]foobar:bar.jpg[/img].", + 'html' => "This is the Google logo: [img]foobar:bar.jpg[/img].", ]], [[ 'descr' => "[img] disallows HTML content.", - 'bbcode' => "This is Google's logo: [img]click me[/img].", - 'html' => "This is Google's logo: [img]<a href='javascript:alert("foo")'>click me</a>[/img].", + 'bbcode' => "This is the Google logo: [img]click me[/img].", + 'html' => "This is the Google logo: [img]<a href='javascript:alert("foo")'>click me</a>[/img].", ]], [[ 'descr' => "[img] can produce a local image.", @@ -865,7 +886,7 @@ public function provideBlockTagConversionTests() { . "\n
\n" . "
Code:
\n" . "
A [b]and[/b] & <woo>!\n" - . "\tAnd a ['hey'] and a [/nonny] and a ho ho ho!
\n" + . "\tAnd a ['hey'] and a [/nonny] and a ho ho ho!
\n" . "\n" . "Also not code.", ]], @@ -880,7 +901,7 @@ public function provideBlockTagConversionTests() { 'html' => "Not code." . "\n
\n" . "
Code:
\n" - . "
\$foo['bar'] = 42;\n" + . "
\$foo['bar'] = 42;\n" . "if (\$foo["bar"] < 42) \$foo[] = 0;
\n" . "
\n" . "Also not code.
\n", @@ -1039,8 +1060,8 @@ public function provideBlockTagConversionTests() { ]], [[ 'descr' => "[nextcol] doesn't do anything outside a [columns] block.", - 'bbcode' => "Here's some text.[nextcol]\nHere's some more.\n", - 'html' => "Here's some text.[nextcol]
\nHere's some more.
\n", + 'bbcode' => "Here is some text.[nextcol]\nHere is some more.\n", + 'html' => "Here is some text.[nextcol]
\nHere is some more.
\n", ]], [[ 'descr' => "Bad column misuse doesn't break layouts.", @@ -1175,6 +1196,7 @@ protected function performTest($test) { $bbcode->setURLTargetable($test['urltarget'] == true); $bbcode->setURLTarget($test['urlforcetarget']); $bbcode->setPlainMode($test['plainmode']); + $bbcode->setEscapeContent($test['escape_content'] ?? true); if ($test['tag_marker'] === '<') { $bbcode->setTagMarker('<');