Upgrade ezyang/htmlpurifier library - closes #5529
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/freescout-help-desk/laravel-selfupdater"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/freescout-help-desk/Purifier"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
@@ -23,7 +27,7 @@
|
||||
"fideloper/proxy": "3.3.4",
|
||||
"laravel/framework": "v5.5.40",
|
||||
"laravel/tinker": "v1.0.7",
|
||||
"mews/purifier": "3.2.2",
|
||||
"mews/purifier": "dev-customized",
|
||||
"spatie/laravel-activitylog": "2.7.0",
|
||||
"lord/laroute": "2.4.7",
|
||||
"axn/laravel-laroute": "1.3.0",
|
||||
@@ -91,8 +95,7 @@
|
||||
"overrides/erusev/parsedown/"
|
||||
],
|
||||
"psr-0": {
|
||||
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
|
||||
"HTMLPurifier_": "overrides/ezyang/htmlpurifier/library"
|
||||
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes"
|
||||
},
|
||||
"files": [
|
||||
"app/Misc/Functions.php"
|
||||
@@ -210,7 +213,6 @@
|
||||
"Whoops\\": "overrides/filp/whoops/src/Whoops/",
|
||||
"Whoops\\Handler\\": "overrides/filp/whoops/src/Whoops/Handler/",
|
||||
"Whoops\\Util\\": "overrides/filp/whoops/src/Whoops/Util/",
|
||||
"Mews\\Purifier\\": "overrides/mews/purifier/src/",
|
||||
"Cron\\": "overrides/mtdowling/cron-expression/src/Cron/",
|
||||
"Psy\\": "overrides/psy/psysh/src/",
|
||||
"Psy\\Input\\": "overrides/psy/psysh/src/Input/",
|
||||
@@ -277,8 +279,6 @@
|
||||
"vendor/symfony/http-foundation/File/MimeType/FileBinaryMimeTypeGuesser.php",
|
||||
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
|
||||
"vendor/fzaninotto/faker/src/Faker/Provider/Base.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/DetectsLostConnections.php",
|
||||
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
|
||||
@@ -404,13 +404,6 @@
|
||||
"vendor/symfony/translation/Loader/ArrayLoader.php",
|
||||
"vendor/symfony/translation/Formatter/MessageFormatter.php",
|
||||
"vendor/symfony/translation/Translator.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php",
|
||||
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php",
|
||||
"vendor/javoscript/laravel-macroable-models/src/MacroableModels.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Attachment.php",
|
||||
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/EmbeddedFile.php",
|
||||
@@ -473,7 +466,6 @@
|
||||
"vendor/league/flysystem/src/Filesystem.php",
|
||||
"vendor/league/flysystem/src/FilesystemInterface.php",
|
||||
"vendor/league/flysystem/src/FileNotFoundException.php",
|
||||
"vendor/mews/purifier/src/Purifier.php",
|
||||
"vendor/mtdowling/cron-expression/src/Cron/CronExpression.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Mail/Mailer.php",
|
||||
"vendor/filp/whoops/src/Whoops/Run.php",
|
||||
|
||||
97
composer.lock
generated
97
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "da1068393c4ab3db8b261fd14ab64f03",
|
||||
"content-hash": "e597d601a6307036f019cd690319c679",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anahkiasen/underscore-php",
|
||||
@@ -998,23 +998,30 @@
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
"version": "v4.12.0",
|
||||
"version": "v4.19.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||
"reference": "a617e55bc62a87eec73bd456d146d134ad716f03"
|
||||
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/a617e55bc62a87eec73bd456d146d134ad716f03",
|
||||
"reference": "a617e55bc62a87eec73bd456d146d134ad716f03",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.2"
|
||||
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
|
||||
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||
"simpletest/simpletest": "dev-master"
|
||||
},
|
||||
"suggest": {
|
||||
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||
"ext-tidy": "Used for pretty-printing HTML"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@@ -1023,7 +1030,10 @@
|
||||
],
|
||||
"psr-0": {
|
||||
"HTMLPurifier": "library/"
|
||||
}
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/library/HTMLPurifier/Language/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
@@ -1043,9 +1053,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/master"
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
|
||||
},
|
||||
"time": "2019-10-28T03:44:26+00:00"
|
||||
"time": "2025-10-17T16:34:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fideloper/proxy",
|
||||
@@ -2038,30 +2048,24 @@
|
||||
},
|
||||
{
|
||||
"name": "mews/purifier",
|
||||
"version": "3.2.2",
|
||||
"version": "dev-customized",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mewebstudio/Purifier.git",
|
||||
"reference": "75e4d9a0553b31c1fd31aef65f9561c30dbe5e5e"
|
||||
"url": "https://github.com/freescout-help-desk/Purifier.git",
|
||||
"reference": "6ef282797274c62687aa31b5fc74665135bb8e7d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/mewebstudio/Purifier/zipball/75e4d9a0553b31c1fd31aef65f9561c30dbe5e5e",
|
||||
"reference": "75e4d9a0553b31c1fd31aef65f9561c30dbe5e5e",
|
||||
"url": "https://api.github.com/repos/freescout-help-desk/Purifier/zipball/6ef282797274c62687aa31b5fc74665135bb8e7d",
|
||||
"reference": "6ef282797274c62687aa31b5fc74665135bb8e7d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ezyang/htmlpurifier": "4.12.*",
|
||||
"illuminate/config": "^5.1|^6.0|^7.0",
|
||||
"illuminate/filesystem": "^5.1|^6.0|^7.0",
|
||||
"illuminate/support": "^5.1|^6.0|^7.0",
|
||||
"php": "^7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"graham-campbell/testbench": "^3.2",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "^4.8|^5.0",
|
||||
"scrutinizer/ocular": "^1.3"
|
||||
"ezyang/htmlpurifier": "^4.19.0",
|
||||
"illuminate/config": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/filesystem": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^7.1|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"laravel/framework": "To test the Laravel bindings",
|
||||
@@ -2070,23 +2074,27 @@
|
||||
"type": "package",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Purifier": "Mews\\Purifier\\Facades\\Purifier"
|
||||
},
|
||||
"providers": [
|
||||
"Mews\\Purifier\\PurifierServiceProvider"
|
||||
]
|
||||
],
|
||||
"aliases": {
|
||||
"Purifier": "Mews\\Purifier\\Facades\\Purifier"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Mews\\Purifier\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Mews\\Tests\\Purifier\\": "tests/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
@@ -2098,25 +2106,21 @@
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Laravel 5/6/7 HtmlPurifier Package",
|
||||
"description": "Laravel 5/6/7/8/9/10 HtmlPurifier Package",
|
||||
"homepage": "https://github.com/mewebstudio/purifier",
|
||||
"keywords": [
|
||||
"HtmlPurifier",
|
||||
"Laravel HtmlPurifier",
|
||||
"Laravel Purifier",
|
||||
"Laravel Security",
|
||||
"Purifier",
|
||||
"htmlpurifier",
|
||||
"laravel5 HtmlPurifier",
|
||||
"laravel5 Purifier",
|
||||
"laravel5 Security",
|
||||
"laravel6 HtmlPurifier",
|
||||
"laravel6 Purifier",
|
||||
"laravel6 Security",
|
||||
"security",
|
||||
"xss"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/mewebstudio/Purifier/issues",
|
||||
"source": "https://github.com/mewebstudio/Purifier/tree/3.2.2"
|
||||
"source": "https://github.com/freescout-help-desk/Purifier/tree/customized"
|
||||
},
|
||||
"time": "2020-04-10T19:42:16+00:00"
|
||||
"time": "2026-07-27T13:06:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
@@ -7419,7 +7423,8 @@
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {
|
||||
"codedge/laravel-selfupdater": 20
|
||||
"codedge/laravel-selfupdater": 20,
|
||||
"mews/purifier": 20
|
||||
},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validates a font family list according to CSS spec
|
||||
*/
|
||||
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
|
||||
{
|
||||
|
||||
protected $mask = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->mask = '_- ';
|
||||
for ($c = 'a'; $c <= 'z'; (function_exists('str_increment') ? $c = str_increment($c) : $c++)) {
|
||||
$this->mask .= $c;
|
||||
}
|
||||
for ($c = 'A'; $c <= 'Z'; (function_exists('str_increment') ? $c = str_increment($c) : $c++)) {
|
||||
$this->mask .= $c;
|
||||
}
|
||||
for ($c = '0'; $c <= '9'; $c++) {
|
||||
$this->mask .= $c;
|
||||
} // cast-y, but should be fine
|
||||
// special bytes used by UTF-8
|
||||
for ($i = 0x80; $i <= 0xFF; $i++) {
|
||||
// We don't bother excluding invalid bytes in this range,
|
||||
// because the our restriction of well-formed UTF-8 will
|
||||
// prevent these from ever occurring.
|
||||
$this->mask .= chr($i);
|
||||
}
|
||||
|
||||
/*
|
||||
PHP's internal strcspn implementation is
|
||||
O(length of string * length of mask), making it inefficient
|
||||
for large masks. However, it's still faster than
|
||||
preg_match 8)
|
||||
for (p = s1;;) {
|
||||
spanp = s2;
|
||||
do {
|
||||
if (*spanp == c || p == s1_end) {
|
||||
return p - s1;
|
||||
}
|
||||
} while (spanp++ < (s2_end - 1));
|
||||
c = *++p;
|
||||
}
|
||||
*/
|
||||
// possible optimization: invert the mask.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return bool|string
|
||||
*/
|
||||
public function validate($string, $config, $context)
|
||||
{
|
||||
static $generic_names = array(
|
||||
'serif' => true,
|
||||
'sans-serif' => true,
|
||||
'monospace' => true,
|
||||
'fantasy' => true,
|
||||
'cursive' => true
|
||||
);
|
||||
$allowed_fonts = $config->get('CSS.AllowedFonts');
|
||||
|
||||
// assume that no font names contain commas in them
|
||||
$fonts = explode(',', $string);
|
||||
$final = '';
|
||||
foreach ($fonts as $font) {
|
||||
$font = trim($font);
|
||||
if ($font === '') {
|
||||
continue;
|
||||
}
|
||||
// match a generic name
|
||||
if (isset($generic_names[$font])) {
|
||||
if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
|
||||
$final .= $font . ', ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// match a quoted name
|
||||
if ($font[0] === '"' || $font[0] === "'") {
|
||||
$length = strlen($font);
|
||||
if ($length <= 2) {
|
||||
continue;
|
||||
}
|
||||
$quote = $font[0];
|
||||
if ($font[$length - 1] !== $quote) {
|
||||
continue;
|
||||
}
|
||||
$font = substr($font, 1, $length - 2);
|
||||
}
|
||||
|
||||
$font = $this->expandCSSEscape($font);
|
||||
|
||||
// $font is a pure representation of the font name
|
||||
|
||||
if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ctype_alnum($font) && $font !== '') {
|
||||
// very simple font, allow it in unharmed
|
||||
$final .= $font . ', ';
|
||||
continue;
|
||||
}
|
||||
|
||||
// bugger out on whitespace. form feed (0C) really
|
||||
// shouldn't show up regardless
|
||||
$font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
|
||||
|
||||
// Here, there are various classes of characters which need
|
||||
// to be treated differently:
|
||||
// - Alphanumeric characters are essentially safe. We
|
||||
// handled these above.
|
||||
// - Spaces require quoting, though most parsers will do
|
||||
// the right thing if there aren't any characters that
|
||||
// can be misinterpreted
|
||||
// - Dashes rarely occur, but they fairly unproblematic
|
||||
// for parsing/rendering purposes.
|
||||
// The above characters cover the majority of Western font
|
||||
// names.
|
||||
// - Arbitrary Unicode characters not in ASCII. Because
|
||||
// most parsers give little thought to Unicode, treatment
|
||||
// of these codepoints is basically uniform, even for
|
||||
// punctuation-like codepoints. These characters can
|
||||
// show up in non-Western pages and are supported by most
|
||||
// major browsers, for example: "MS 明朝" is a
|
||||
// legitimate font-name
|
||||
// <http://ja.wikipedia.org/wiki/MS_明朝>. See
|
||||
// the CSS3 spec for more examples:
|
||||
// <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
|
||||
// You can see live samples of these on the Internet:
|
||||
// <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
|
||||
// However, most of these fonts have ASCII equivalents:
|
||||
// for example, 'MS Mincho', and it's considered
|
||||
// professional to use ASCII font names instead of
|
||||
// Unicode font names. Thanks Takeshi Terada for
|
||||
// providing this information.
|
||||
// The following characters, to my knowledge, have not been
|
||||
// used to name font names.
|
||||
// - Single quote. While theoretically you might find a
|
||||
// font name that has a single quote in its name (serving
|
||||
// as an apostrophe, e.g. Dave's Scribble), I haven't
|
||||
// been able to find any actual examples of this.
|
||||
// Internet Explorer's cssText translation (which I
|
||||
// believe is invoked by innerHTML) normalizes any
|
||||
// quoting to single quotes, and fails to escape single
|
||||
// quotes. (Note that this is not IE's behavior for all
|
||||
// CSS properties, just some sort of special casing for
|
||||
// font-family). So a single quote *cannot* be used
|
||||
// safely in the font-family context if there will be an
|
||||
// innerHTML/cssText translation. Note that Firefox 3.x
|
||||
// does this too.
|
||||
// - Double quote. In IE, these get normalized to
|
||||
// single-quotes, no matter what the encoding. (Fun
|
||||
// fact, in IE8, the 'content' CSS property gained
|
||||
// support, where they special cased to preserve encoded
|
||||
// double quotes, but still translate unadorned double
|
||||
// quotes into single quotes.) So, because their
|
||||
// fixpoint behavior is identical to single quotes, they
|
||||
// cannot be allowed either. Firefox 3.x displays
|
||||
// single-quote style behavior.
|
||||
// - Backslashes are reduced by one (so \\ -> \) every
|
||||
// iteration, so they cannot be used safely. This shows
|
||||
// up in IE7, IE8 and FF3
|
||||
// - Semicolons, commas and backticks are handled properly.
|
||||
// - The rest of the ASCII punctuation is handled properly.
|
||||
// We haven't checked what browsers do to unadorned
|
||||
// versions, but this is not important as long as the
|
||||
// browser doesn't /remove/ surrounding quotes (as IE does
|
||||
// for HTML).
|
||||
//
|
||||
// With these results in hand, we conclude that there are
|
||||
// various levels of safety:
|
||||
// - Paranoid: alphanumeric, spaces and dashes(?)
|
||||
// - International: Paranoid + non-ASCII Unicode
|
||||
// - Edgy: Everything except quotes, backslashes
|
||||
// - NoJS: Standards compliance, e.g. sod IE. Note that
|
||||
// with some judicious character escaping (since certain
|
||||
// types of escaping doesn't work) this is theoretically
|
||||
// OK as long as innerHTML/cssText is not called.
|
||||
// We believe that international is a reasonable default
|
||||
// (that we will implement now), and once we do more
|
||||
// extensive research, we may feel comfortable with dropping
|
||||
// it down to edgy.
|
||||
|
||||
// Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of
|
||||
// str(c)spn assumes that the string was already well formed
|
||||
// Unicode (which of course it is).
|
||||
if (strspn($font, $this->mask) !== strlen($font)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Historical:
|
||||
// In the absence of innerHTML/cssText, these ugly
|
||||
// transforms don't pose a security risk (as \\ and \"
|
||||
// might--these escapes are not supported by most browsers).
|
||||
// We could try to be clever and use single-quote wrapping
|
||||
// when there is a double quote present, but I have choosen
|
||||
// not to implement that. (NOTE: you can reduce the amount
|
||||
// of escapes by one depending on what quoting style you use)
|
||||
// $font = str_replace('\\', '\\5C ', $font);
|
||||
// $font = str_replace('"', '\\22 ', $font);
|
||||
// $font = str_replace("'", '\\27 ', $font);
|
||||
|
||||
// font possibly with spaces, requires quoting
|
||||
$final .= "'$font', ";
|
||||
}
|
||||
$final = rtrim($final, ', ');
|
||||
if ($final === '') {
|
||||
return false;
|
||||
}
|
||||
return $final;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
|
||||
*/
|
||||
class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
|
||||
{
|
||||
|
||||
/**
|
||||
* IPv4 sub-validator.
|
||||
* @type HTMLPurifier_AttrDef_URI_IPv4
|
||||
*/
|
||||
protected $ipv4;
|
||||
|
||||
/**
|
||||
* IPv6 sub-validator.
|
||||
* @type HTMLPurifier_AttrDef_URI_IPv6
|
||||
*/
|
||||
protected $ipv6;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
|
||||
$this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return bool|string
|
||||
*/
|
||||
public function validate($string, $config, $context)
|
||||
{
|
||||
$length = strlen($string);
|
||||
// empty hostname is OK; it's usually semantically equivalent:
|
||||
// the default host as defined by a URI scheme is used:
|
||||
//
|
||||
// If the URI scheme defines a default for host, then that
|
||||
// default applies when the host subcomponent is undefined
|
||||
// or when the registered name is empty (zero length).
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') {
|
||||
//IPv6
|
||||
$ip = substr($string, 1, $length - 2);
|
||||
$valid = $this->ipv6->validate($ip, $config, $context);
|
||||
if ($valid === false) {
|
||||
return false;
|
||||
}
|
||||
return '[' . $valid . ']';
|
||||
}
|
||||
|
||||
// need to do checks on unusual encodings too
|
||||
$ipv4 = $this->ipv4->validate($string, $config, $context);
|
||||
if ($ipv4 !== false) {
|
||||
return $ipv4;
|
||||
}
|
||||
|
||||
// A regular domain name.
|
||||
|
||||
// This doesn't match I18N domain names, but we don't have proper IRI support,
|
||||
// so force users to insert Punycode.
|
||||
|
||||
// There is not a good sense in which underscores should be
|
||||
// allowed, since it's technically not! (And if you go as
|
||||
// far to allow everything as specified by the DNS spec...
|
||||
// well, that's literally everything, modulo some space limits
|
||||
// for the components and the overall name (which, by the way,
|
||||
// we are NOT checking!). So we (arbitrarily) decide this:
|
||||
// let's allow underscores wherever we would have allowed
|
||||
// hyphens, if they are enabled. This is a pretty good match
|
||||
// for browser behavior, for example, a large number of browsers
|
||||
// cannot handle foo_.example.com, but foo_bar.example.com is
|
||||
// fairly well supported.
|
||||
$underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : '';
|
||||
|
||||
// Based off of RFC 1738, but amended so that
|
||||
// as per RFC 3696, the top label need only not be all numeric.
|
||||
// The productions describing this are:
|
||||
$a = '[a-z]'; // alpha
|
||||
$an = '[a-z0-9]'; // alphanum
|
||||
$and = "[a-z0-9-$underscore]"; // alphanum | "-"
|
||||
// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
|
||||
$domainlabel = "$an(?:$and*$an)?";
|
||||
// AMENDED as per RFC 3696
|
||||
// toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum
|
||||
// side condition: not all numeric
|
||||
$toplabel = "$an(?:$and*$an)?";
|
||||
// hostname = *( domainlabel "." ) toplabel [ "." ]
|
||||
if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) {
|
||||
if (!ctype_digit($matches[1])) {
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
|
||||
// PHP 5.3 and later support this functionality natively
|
||||
if (function_exists('idn_to_ascii')) {
|
||||
try {
|
||||
if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
|
||||
$string = idn_to_ascii($string, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
|
||||
} else {
|
||||
$string = idn_to_ascii($string);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
// If we have Net_IDNA2 support, we can support IRIs by
|
||||
// punycoding them. (This is the most portable thing to do,
|
||||
// since otherwise we have to assume browsers support
|
||||
} elseif ($config->get('Core.EnableIDNA')) {
|
||||
$idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true));
|
||||
// we need to encode each period separately
|
||||
$parts = explode('.', $string);
|
||||
try {
|
||||
$new_parts = array();
|
||||
foreach ($parts as $part) {
|
||||
$encodable = false;
|
||||
for ($i = 0, $c = strlen($part); $i < $c; $i++) {
|
||||
if (ord($part[$i]) > 0x7a) {
|
||||
$encodable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$encodable) {
|
||||
$new_parts[] = $part;
|
||||
} else {
|
||||
$new_parts[] = $idna->encode($part);
|
||||
}
|
||||
}
|
||||
$string = implode('.', $new_parts);
|
||||
} catch (Exception $e) {
|
||||
// XXX error reporting
|
||||
}
|
||||
}
|
||||
// Try again
|
||||
if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
|
||||
return $string;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Post-transform that performs validation to the name attribute; if
|
||||
* it is present with an equivalent id attribute, it is passed through;
|
||||
* otherwise validation is performed.
|
||||
*/
|
||||
class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
|
||||
{
|
||||
public $idDef;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $attr
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return array
|
||||
*/
|
||||
public function transform($attr, $config, $context)
|
||||
{
|
||||
if (!isset($attr['name'])) {
|
||||
return $attr;
|
||||
}
|
||||
$name = $attr['name'];
|
||||
if (isset($attr['id']) && $attr['id'] === $name) {
|
||||
return $attr;
|
||||
}
|
||||
$result = $this->idDef->validate($name, $config, $context);
|
||||
if ($result === false) {
|
||||
unset($attr['name']);
|
||||
} else {
|
||||
$attr['name'] = $result;
|
||||
}
|
||||
return $attr;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,205 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validates the attributes of a token. Doesn't manage required attributes
|
||||
* very well. The only reason we factored this out was because RemoveForeignElements
|
||||
* also needed it besides ValidateAttributes.
|
||||
*/
|
||||
class HTMLPurifier_AttrValidator
|
||||
{
|
||||
|
||||
/**
|
||||
* Validates the attributes of a token, mutating it as necessary.
|
||||
* that has valid tokens
|
||||
* @param HTMLPurifier_Token $token Token to validate.
|
||||
* @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config
|
||||
* @param HTMLPurifier_Context $context Instance of HTMLPurifier_Context
|
||||
*/
|
||||
public function validateToken($token, $config, $context)
|
||||
{
|
||||
$definition = $config->getHTMLDefinition();
|
||||
$e =& $context->get('ErrorCollector', true);
|
||||
|
||||
// initialize IDAccumulator if necessary
|
||||
$ok =& $context->get('IDAccumulator', true);
|
||||
if (!$ok) {
|
||||
$id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
|
||||
$context->register('IDAccumulator', $id_accumulator);
|
||||
}
|
||||
|
||||
// initialize CurrentToken if necessary
|
||||
$current_token =& $context->get('CurrentToken', true);
|
||||
if (!$current_token) {
|
||||
$context->register('CurrentToken', $token);
|
||||
}
|
||||
|
||||
if (!$token instanceof HTMLPurifier_Token_Start &&
|
||||
!$token instanceof HTMLPurifier_Token_Empty
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create alias to global definition array, see also $defs
|
||||
// DEFINITION CALL
|
||||
$d_defs = $definition->info_global_attr;
|
||||
|
||||
// don't update token until the very end, to ensure an atomic update
|
||||
$attr = $token->attr;
|
||||
|
||||
// do global transformations (pre)
|
||||
// nothing currently utilizes this
|
||||
foreach ($definition->info_attr_transform_pre as $transform) {
|
||||
$attr = $transform->transform($o = $attr, $config, $context);
|
||||
if ($e) {
|
||||
if ($attr != $o) {
|
||||
$e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do local transformations only applicable to this element (pre)
|
||||
// ex. <p align="right"> to <p style="text-align:right;">
|
||||
foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
|
||||
$attr = $transform->transform($o = $attr, $config, $context);
|
||||
if ($e) {
|
||||
if ($attr != $o) {
|
||||
$e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create alias to this element's attribute definition array, see
|
||||
// also $d_defs (global attribute definition array)
|
||||
// DEFINITION CALL
|
||||
$defs = $definition->info[$token->name]->attr;
|
||||
|
||||
$attr_key = false;
|
||||
$context->register('CurrentAttr', $attr_key);
|
||||
|
||||
// Used to make sure width/height are changed proportionally
|
||||
// when HTML.MaxImgLength is used.
|
||||
$proportions_change = [];
|
||||
|
||||
// iterate through all the attribute keypairs
|
||||
// Watch out for name collisions: $key has previously been used
|
||||
foreach ($attr as $attr_key => $value) {
|
||||
|
||||
// call the definition
|
||||
if (isset($defs[$attr_key])) {
|
||||
// there is a local definition defined
|
||||
if ($defs[$attr_key] === false) {
|
||||
// We've explicitly been told not to allow this element.
|
||||
// This is usually when there's a global definition
|
||||
// that must be overridden.
|
||||
// Theoretically speaking, we could have a
|
||||
// AttrDef_DenyAll, but this is faster!
|
||||
$result = false;
|
||||
} else {
|
||||
// validate according to the element's definition
|
||||
$result = $defs[$attr_key]->validate(
|
||||
$value,
|
||||
$config,
|
||||
$context
|
||||
);
|
||||
if ($token->name == 'img'
|
||||
&& $result
|
||||
&& (int)$value
|
||||
&& (int)$result
|
||||
&& ($attr_key == 'width' || $attr_key == 'height')
|
||||
) {
|
||||
$proportions_change[$attr_key]['original'] = (int)$value;
|
||||
$proportions_change[$attr_key]['new'] = (int)$result;
|
||||
}
|
||||
}
|
||||
} elseif (isset($d_defs[$attr_key])) {
|
||||
// there is a global definition defined, validate according
|
||||
// to the global definition
|
||||
$result = $d_defs[$attr_key]->validate(
|
||||
$value,
|
||||
$config,
|
||||
$context
|
||||
);
|
||||
} else {
|
||||
// system never heard of the attribute? DELETE!
|
||||
$result = false;
|
||||
}
|
||||
|
||||
// put the results into effect
|
||||
if ($result === false || $result === null) {
|
||||
// this is a generic error message that should replaced
|
||||
// with more specific ones when possible
|
||||
if ($e) {
|
||||
$e->send(E_ERROR, 'AttrValidator: Attribute removed');
|
||||
}
|
||||
|
||||
// remove the attribute
|
||||
unset($attr[$attr_key]);
|
||||
} elseif (is_string($result)) {
|
||||
// generally, if a substitution is happening, there
|
||||
// was some sort of implicit correction going on. We'll
|
||||
// delegate it to the attribute classes to say exactly what.
|
||||
|
||||
// simple substitution
|
||||
$attr[$attr_key] = $result;
|
||||
} else {
|
||||
// nothing happens
|
||||
}
|
||||
|
||||
// we'd also want slightly more complicated substitution
|
||||
// involving an array as the return value,
|
||||
// although we're not sure how colliding attributes would
|
||||
// resolve (certain ones would be completely overriden,
|
||||
// others would prepend themselves).
|
||||
}
|
||||
|
||||
if (!empty($proportions_change['width']) && !empty($proportions_change['height'])) {
|
||||
if ($proportions_change['width']['new'] < $proportions_change['width']['original']
|
||||
&& (int)$proportions_change['width']['original']
|
||||
) {
|
||||
// Adjust height.
|
||||
$attr['height'] = floor($proportions_change['height']['original'] * $proportions_change['width']['new'] / $proportions_change['width']['original']);
|
||||
} elseif ($proportions_change['height']['new'] < $proportions_change['height']['original']
|
||||
&& (int)$proportions_change['height']['original']
|
||||
) {
|
||||
// Adjust width.
|
||||
$attr['width'] = floor($proportions_change['height']['original'] * $proportions_change['height']['new'] / $proportions_change['height']['original']);
|
||||
}
|
||||
}
|
||||
|
||||
$context->destroy('CurrentAttr');
|
||||
|
||||
// post transforms
|
||||
|
||||
// global (error reporting untested)
|
||||
foreach ($definition->info_attr_transform_post as $transform) {
|
||||
$attr = $transform->transform($o = $attr, $config, $context);
|
||||
if ($e) {
|
||||
if ($attr != $o) {
|
||||
$e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// local (error reporting untested)
|
||||
foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
|
||||
$attr = $transform->transform($o = $attr, $config, $context);
|
||||
if ($e) {
|
||||
if ($attr != $o) {
|
||||
$e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$token->attr = $attr;
|
||||
|
||||
// destroy CurrentToken if we made it ourselves
|
||||
if (!$current_token) {
|
||||
$context->destroy('CurrentToken');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Definition for list containers ul and ol.
|
||||
*
|
||||
* What does this do? The big thing is to handle ol/ul at the top
|
||||
* level of list nodes, which should be handled specially by /folding/
|
||||
* them into the previous list node. We generally shouldn't ever
|
||||
* see other disallowed elements, because the autoclose behavior
|
||||
* in MakeWellFormed handles it.
|
||||
*/
|
||||
class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
|
||||
{
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
public $type = 'list';
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
// lying a little bit, so that we can handle ul and ol ourselves
|
||||
// XXX: This whole business with 'wrap' is all a bit unsatisfactory
|
||||
public $elements = array('li' => true, 'ul' => true, 'ol' => true);
|
||||
|
||||
public $whitespace;
|
||||
|
||||
/**
|
||||
* @param array $children
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return array
|
||||
*/
|
||||
public function validateChildren($children, $config, $context)
|
||||
{
|
||||
// Flag for subclasses
|
||||
$this->whitespace = false;
|
||||
|
||||
// if there are no tokens, delete parent node
|
||||
if (empty($children)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if li is not allowed, delete parent node
|
||||
if (!isset($config->getHTMLDefinition()->info['li'])) {
|
||||
trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
// the new set of children
|
||||
$result = array();
|
||||
|
||||
// a little sanity check to make sure it's not ALL whitespace
|
||||
$all_whitespace = true;
|
||||
|
||||
$current_li = null;
|
||||
|
||||
foreach ($children as $node) {
|
||||
if (!empty($node->is_whitespace)) {
|
||||
$result[] = $node;
|
||||
continue;
|
||||
}
|
||||
$all_whitespace = false; // phew, we're not talking about whitespace
|
||||
|
||||
if ($node->name === 'li') {
|
||||
// good
|
||||
$current_li = $node;
|
||||
$result[] = $node;
|
||||
} else {
|
||||
// we want to tuck this into the previous li
|
||||
// Invariant: we expect the node to be ol/ul
|
||||
// ToDo: Make this more robust in the case of not ol/ul
|
||||
// by distinguishing between existing li and li created
|
||||
// to handle non-list elements; non-list elements should
|
||||
// not be appended to an existing li; only li created
|
||||
// for non-list. This distinction is not currently made.
|
||||
if ($current_li === null) {
|
||||
$current_li = new HTMLPurifier_Node_Element('li');
|
||||
$result[] = $current_li;
|
||||
}
|
||||
$current_li->children[] = $node;
|
||||
$current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo
|
||||
}
|
||||
}
|
||||
if (empty($result)) {
|
||||
return false;
|
||||
}
|
||||
if ($all_whitespace) {
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,617 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A UTF-8 specific character encoder that handles cleaning and transforming.
|
||||
* @note All functions in this class should be static.
|
||||
*/
|
||||
class HTMLPurifier_Encoder
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructor throws fatal error if you attempt to instantiate class
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error-handler that mutes errors, alternative to shut-up operator.
|
||||
*/
|
||||
public static function muteErrorHandler()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* iconv wrapper which mutes errors, but doesn't work around bugs.
|
||||
* @param string $in Input encoding
|
||||
* @param string $out Output encoding
|
||||
* @param string $text The text to convert
|
||||
* @return string
|
||||
*/
|
||||
public static function unsafeIconv($in, $out, $text)
|
||||
{
|
||||
set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
|
||||
$r = iconv($in, $out, $text);
|
||||
restore_error_handler();
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* iconv wrapper which mutes errors and works around bugs.
|
||||
* @param string $in Input encoding
|
||||
* @param string $out Output encoding
|
||||
* @param string $text The text to convert
|
||||
* @param int $max_chunk_size
|
||||
* @return string
|
||||
*/
|
||||
public static function iconv($in, $out, $text, $max_chunk_size = 8000)
|
||||
{
|
||||
$code = self::testIconvTruncateBug();
|
||||
if ($code == self::ICONV_OK) {
|
||||
return self::unsafeIconv($in, $out, $text);
|
||||
} elseif ($code == self::ICONV_TRUNCATES) {
|
||||
// we can only work around this if the input character set
|
||||
// is utf-8
|
||||
if ($in == 'utf-8') {
|
||||
if ($max_chunk_size < 4) {
|
||||
trigger_error('max_chunk_size is too small', E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
// split into 8000 byte chunks, but be careful to handle
|
||||
// multibyte boundaries properly
|
||||
if (($c = strlen($text)) <= $max_chunk_size) {
|
||||
return self::unsafeIconv($in, $out, $text);
|
||||
}
|
||||
$r = '';
|
||||
$i = 0;
|
||||
while (true) {
|
||||
if ($i + $max_chunk_size >= $c) {
|
||||
$r .= self::unsafeIconv($in, $out, substr($text, $i));
|
||||
break;
|
||||
}
|
||||
// wibble the boundary
|
||||
if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) {
|
||||
$chunk_size = $max_chunk_size;
|
||||
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) {
|
||||
$chunk_size = $max_chunk_size - 1;
|
||||
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) {
|
||||
$chunk_size = $max_chunk_size - 2;
|
||||
} elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) {
|
||||
$chunk_size = $max_chunk_size - 3;
|
||||
} else {
|
||||
return false; // rather confusing UTF-8...
|
||||
}
|
||||
$chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths
|
||||
$r .= self::unsafeIconv($in, $out, $chunk);
|
||||
$i += $chunk_size;
|
||||
}
|
||||
return $r;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans a UTF-8 string for well-formedness and SGML validity
|
||||
*
|
||||
* It will parse according to UTF-8 and return a valid UTF8 string, with
|
||||
* non-SGML codepoints excluded.
|
||||
*
|
||||
* Specifically, it will permit:
|
||||
* \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}
|
||||
* Source: https://www.w3.org/TR/REC-xml/#NT-Char
|
||||
* Arguably this function should be modernized to the HTML5 set
|
||||
* of allowed characters:
|
||||
* https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
|
||||
* which simultaneously expand and restrict the set of allowed characters.
|
||||
*
|
||||
* @param string $str The string to clean
|
||||
* @param bool $force_php
|
||||
* @return string
|
||||
*
|
||||
* @note Just for reference, the non-SGML code points are 0 to 31 and
|
||||
* 127 to 159, inclusive. However, we allow code points 9, 10
|
||||
* and 13, which are the tab, line feed and carriage return
|
||||
* respectively. 128 and above the code points map to multibyte
|
||||
* UTF-8 representations.
|
||||
*
|
||||
* @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
|
||||
* hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
|
||||
* LGPL license. Notes on what changed are inside, but in general,
|
||||
* the original code transformed UTF-8 text into an array of integer
|
||||
* Unicode codepoints. Understandably, transforming that back to
|
||||
* a string would be somewhat expensive, so the function was modded to
|
||||
* directly operate on the string. However, this discourages code
|
||||
* reuse, and the logic enumerated here would be useful for any
|
||||
* function that needs to be able to understand UTF-8 characters.
|
||||
* As of right now, only smart lossless character encoding converters
|
||||
* would need that, and I'm probably not going to implement them.
|
||||
*/
|
||||
public static function cleanUTF8($str, $force_php = false)
|
||||
{
|
||||
// UTF-8 validity is checked since PHP 4.3.5
|
||||
// This is an optimization: if the string is already valid UTF-8, no
|
||||
// need to do PHP stuff. 99% of the time, this will be the case.
|
||||
if (preg_match(
|
||||
'/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du',
|
||||
$str ?? ''
|
||||
)) {
|
||||
return $str;
|
||||
}
|
||||
|
||||
$mState = 0; // cached expected number of octets after the current octet
|
||||
// until the beginning of the next UTF8 character sequence
|
||||
$mUcs4 = 0; // cached Unicode character
|
||||
$mBytes = 1; // cached expected number of octets in the current sequence
|
||||
|
||||
// original code involved an $out that was an array of Unicode
|
||||
// codepoints. Instead of having to convert back into UTF-8, we've
|
||||
// decided to directly append valid UTF-8 characters onto a string
|
||||
// $out once they're done. $char accumulates raw bytes, while $mUcs4
|
||||
// turns into the Unicode code point, so there's some redundancy.
|
||||
|
||||
$out = '';
|
||||
$char = '';
|
||||
|
||||
$len = strlen($str);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$in = ord($str[$i]);
|
||||
$char .= $str[$i]; // append byte to char
|
||||
if (0 == $mState) {
|
||||
// When mState is zero we expect either a US-ASCII character
|
||||
// or a multi-octet sequence.
|
||||
if (0 == (0x80 & ($in))) {
|
||||
// US-ASCII, pass straight through.
|
||||
if (($in <= 31 || $in == 127) &&
|
||||
!($in == 9 || $in == 13 || $in == 10) // save \r\t\n
|
||||
) {
|
||||
// control characters, remove
|
||||
} else {
|
||||
$out .= $char;
|
||||
}
|
||||
// reset
|
||||
$char = '';
|
||||
$mBytes = 1;
|
||||
} elseif (0xC0 == (0xE0 & ($in))) {
|
||||
// First octet of 2 octet sequence
|
||||
$mUcs4 = ($in);
|
||||
$mUcs4 = ($mUcs4 & 0x1F) << 6;
|
||||
$mState = 1;
|
||||
$mBytes = 2;
|
||||
} elseif (0xE0 == (0xF0 & ($in))) {
|
||||
// First octet of 3 octet sequence
|
||||
$mUcs4 = ($in);
|
||||
$mUcs4 = ($mUcs4 & 0x0F) << 12;
|
||||
$mState = 2;
|
||||
$mBytes = 3;
|
||||
} elseif (0xF0 == (0xF8 & ($in))) {
|
||||
// First octet of 4 octet sequence
|
||||
$mUcs4 = ($in);
|
||||
$mUcs4 = ($mUcs4 & 0x07) << 18;
|
||||
$mState = 3;
|
||||
$mBytes = 4;
|
||||
} elseif (0xF8 == (0xFC & ($in))) {
|
||||
// First octet of 5 octet sequence.
|
||||
//
|
||||
// This is illegal because the encoded codepoint must be
|
||||
// either:
|
||||
// (a) not the shortest form or
|
||||
// (b) outside the Unicode range of 0-0x10FFFF.
|
||||
// Rather than trying to resynchronize, we will carry on
|
||||
// until the end of the sequence and let the later error
|
||||
// handling code catch it.
|
||||
$mUcs4 = ($in);
|
||||
$mUcs4 = ($mUcs4 & 0x03) << 24;
|
||||
$mState = 4;
|
||||
$mBytes = 5;
|
||||
} elseif (0xFC == (0xFE & ($in))) {
|
||||
// First octet of 6 octet sequence, see comments for 5
|
||||
// octet sequence.
|
||||
$mUcs4 = ($in);
|
||||
$mUcs4 = ($mUcs4 & 1) << 30;
|
||||
$mState = 5;
|
||||
$mBytes = 6;
|
||||
} else {
|
||||
// Current octet is neither in the US-ASCII range nor a
|
||||
// legal first octet of a multi-octet sequence.
|
||||
$mState = 0;
|
||||
$mUcs4 = 0;
|
||||
$mBytes = 1;
|
||||
$char = '';
|
||||
}
|
||||
} else {
|
||||
// When mState is non-zero, we expect a continuation of the
|
||||
// multi-octet sequence
|
||||
if (0x80 == (0xC0 & ($in))) {
|
||||
// Legal continuation.
|
||||
$shift = ($mState - 1) * 6;
|
||||
$tmp = $in;
|
||||
$tmp = ($tmp & 0x0000003F) << $shift;
|
||||
$mUcs4 |= $tmp;
|
||||
|
||||
if (0 == --$mState) {
|
||||
// End of the multi-octet sequence. mUcs4 now contains
|
||||
// the final Unicode codepoint to be output
|
||||
|
||||
// Check for illegal sequences and codepoints.
|
||||
|
||||
// From Unicode 3.1, non-shortest form is illegal
|
||||
if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
|
||||
((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
|
||||
((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
|
||||
(4 < $mBytes) ||
|
||||
// From Unicode 3.2, surrogate characters = illegal
|
||||
(($mUcs4 & 0xFFFFF800) == 0xD800) ||
|
||||
// Codepoints outside the Unicode range are illegal
|
||||
($mUcs4 > 0x10FFFF)
|
||||
) {
|
||||
|
||||
} elseif (0xFEFF != $mUcs4 && // omit BOM
|
||||
// check for valid Char unicode codepoints
|
||||
(
|
||||
0x9 == $mUcs4 ||
|
||||
0xA == $mUcs4 ||
|
||||
0xD == $mUcs4 ||
|
||||
(0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
|
||||
// 7F-9F is not strictly prohibited by XML,
|
||||
// but it is non-SGML, and thus we don't allow it
|
||||
(0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
|
||||
(0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) ||
|
||||
(0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
|
||||
)
|
||||
) {
|
||||
$out .= $char;
|
||||
}
|
||||
// initialize UTF8 cache (reset)
|
||||
$mState = 0;
|
||||
$mUcs4 = 0;
|
||||
$mBytes = 1;
|
||||
$char = '';
|
||||
}
|
||||
} else {
|
||||
// ((0xC0 & (*in) != 0x80) && (mState != 0))
|
||||
// Incomplete multi-octet sequence.
|
||||
// used to result in complete fail, but we'll reset
|
||||
$mState = 0;
|
||||
$mUcs4 = 0;
|
||||
$mBytes = 1;
|
||||
$char ='';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a Unicode codepoint into its corresponding UTF-8 character.
|
||||
* @note Based on Feyd's function at
|
||||
* <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
|
||||
* which is in public domain.
|
||||
* @note While we're going to do code point parsing anyway, a good
|
||||
* optimization would be to refuse to translate code points that
|
||||
* are non-SGML characters. However, this could lead to duplication.
|
||||
* @note This is very similar to the unichr function in
|
||||
* maintenance/generate-entity-file.php (although this is superior,
|
||||
* due to its sanity checks).
|
||||
*/
|
||||
|
||||
// +----------+----------+----------+----------+
|
||||
// | 33222222 | 22221111 | 111111 | |
|
||||
// | 10987654 | 32109876 | 54321098 | 76543210 | bit
|
||||
// +----------+----------+----------+----------+
|
||||
// | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
|
||||
// | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
|
||||
// | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
|
||||
// | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
|
||||
// +----------+----------+----------+----------+
|
||||
// | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
|
||||
// | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
|
||||
// +----------+----------+----------+----------+
|
||||
|
||||
public static function unichr($code)
|
||||
{
|
||||
if ($code > 1114111 or $code < 0 or
|
||||
($code >= 55296 and $code <= 57343) ) {
|
||||
// bits are set outside the "valid" range as defined
|
||||
// by UNICODE 4.1.0
|
||||
return '';
|
||||
}
|
||||
|
||||
$x = $y = $z = $w = 0;
|
||||
if ($code < 128) {
|
||||
// regular ASCII character
|
||||
$x = $code;
|
||||
} else {
|
||||
// set up bits for UTF-8
|
||||
$x = ($code & 63) | 128;
|
||||
if ($code < 2048) {
|
||||
$y = (($code & 2047) >> 6) | 192;
|
||||
} else {
|
||||
$y = (($code & 4032) >> 6) | 128;
|
||||
if ($code < 65536) {
|
||||
$z = (($code >> 12) & 15) | 224;
|
||||
} else {
|
||||
$z = (($code >> 12) & 63) | 128;
|
||||
$w = (($code >> 18) & 7) | 240;
|
||||
}
|
||||
}
|
||||
}
|
||||
// set up the actual character
|
||||
$ret = '';
|
||||
if ($w) {
|
||||
$ret .= chr($w);
|
||||
}
|
||||
if ($z) {
|
||||
$ret .= chr($z);
|
||||
}
|
||||
if ($y) {
|
||||
$ret .= chr($y);
|
||||
}
|
||||
$ret .= chr($x);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function iconvAvailable()
|
||||
{
|
||||
static $iconv = null;
|
||||
if ($iconv === null) {
|
||||
$iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE;
|
||||
}
|
||||
return $iconv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to UTF-8 based on configuration.
|
||||
* @param string $str The string to convert
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
*/
|
||||
public static function convertToUTF8($str, $config, $context)
|
||||
{
|
||||
$encoding = $config->get('Core.Encoding');
|
||||
if ($encoding === 'utf-8') {
|
||||
return $str;
|
||||
}
|
||||
static $iconv = null;
|
||||
if ($iconv === null) {
|
||||
$iconv = self::iconvAvailable();
|
||||
}
|
||||
if ($iconv && !$config->get('Test.ForceNoIconv')) {
|
||||
// unaffected by bugs, since UTF-8 support all characters
|
||||
$str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str);
|
||||
if ($str === false) {
|
||||
// $encoding is not a valid encoding
|
||||
trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
|
||||
return '';
|
||||
}
|
||||
// If the string is bjorked by Shift_JIS or a similar encoding
|
||||
// that doesn't support all of ASCII, convert the naughty
|
||||
// characters to their true byte-wise ASCII/UTF-8 equivalents.
|
||||
$str = strtr($str, self::testEncodingSupportsASCII($encoding));
|
||||
return $str;
|
||||
} elseif ($encoding === 'iso-8859-1') {
|
||||
$str = utf8_encode($str);
|
||||
return $str;
|
||||
}
|
||||
$bug = HTMLPurifier_Encoder::testIconvTruncateBug();
|
||||
if ($bug == self::ICONV_OK) {
|
||||
trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
|
||||
} else {
|
||||
trigger_error(
|
||||
'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' .
|
||||
'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541',
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string from UTF-8 based on configuration.
|
||||
* @param string $str The string to convert
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
* @note Currently, this is a lossy conversion, with unexpressable
|
||||
* characters being omitted.
|
||||
*/
|
||||
public static function convertFromUTF8($str, $config, $context)
|
||||
{
|
||||
$encoding = $config->get('Core.Encoding');
|
||||
if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
|
||||
$str = self::convertToASCIIDumbLossless($str);
|
||||
}
|
||||
if ($encoding === 'utf-8') {
|
||||
return $str;
|
||||
}
|
||||
static $iconv = null;
|
||||
if ($iconv === null) {
|
||||
$iconv = self::iconvAvailable();
|
||||
}
|
||||
if ($iconv && !$config->get('Test.ForceNoIconv')) {
|
||||
// Undo our previous fix in convertToUTF8, otherwise iconv will barf
|
||||
$ascii_fix = self::testEncodingSupportsASCII($encoding);
|
||||
if (!$escape && !empty($ascii_fix)) {
|
||||
$clear_fix = array();
|
||||
foreach ($ascii_fix as $utf8 => $native) {
|
||||
$clear_fix[$utf8] = '';
|
||||
}
|
||||
$str = strtr($str, $clear_fix);
|
||||
}
|
||||
$str = strtr($str, array_flip($ascii_fix));
|
||||
// Normal stuff
|
||||
$str = self::iconv('utf-8', $encoding . '//IGNORE', $str);
|
||||
return $str;
|
||||
} elseif ($encoding === 'iso-8859-1') {
|
||||
$str = utf8_decode($str);
|
||||
return $str;
|
||||
}
|
||||
trigger_error('Encoding not supported', E_USER_ERROR);
|
||||
// You might be tempted to assume that the ASCII representation
|
||||
// might be OK, however, this is *not* universally true over all
|
||||
// encodings. So we take the conservative route here, rather
|
||||
// than forcibly turn on %Core.EscapeNonASCIICharacters
|
||||
}
|
||||
|
||||
/**
|
||||
* Lossless (character-wise) conversion of HTML to ASCII
|
||||
* @param string $str UTF-8 string to be converted to ASCII
|
||||
* @return string ASCII encoded string with non-ASCII character entity-ized
|
||||
* @warning Adapted from MediaWiki, claiming fair use: this is a common
|
||||
* algorithm. If you disagree with this license fudgery,
|
||||
* implement it yourself.
|
||||
* @note Uses decimal numeric entities since they are best supported.
|
||||
* @note This is a DUMB function: it has no concept of keeping
|
||||
* character entities that the projected character encoding
|
||||
* can allow. We could possibly implement a smart version
|
||||
* but that would require it to also know which Unicode
|
||||
* codepoints the charset supported (not an easy task).
|
||||
* @note Sort of with cleanUTF8() but it assumes that $str is
|
||||
* well-formed UTF-8
|
||||
*/
|
||||
public static function convertToASCIIDumbLossless($str)
|
||||
{
|
||||
$bytesleft = 0;
|
||||
$result = '';
|
||||
$working = 0;
|
||||
$len = strlen($str);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$bytevalue = ord($str[$i]);
|
||||
if ($bytevalue <= 0x7F) { //0xxx xxxx
|
||||
$result .= chr($bytevalue);
|
||||
$bytesleft = 0;
|
||||
} elseif ($bytevalue <= 0xBF) { //10xx xxxx
|
||||
$working = $working << 6;
|
||||
$working += ($bytevalue & 0x3F);
|
||||
$bytesleft--;
|
||||
if ($bytesleft <= 0) {
|
||||
$result .= "&#" . $working . ";";
|
||||
}
|
||||
} elseif ($bytevalue <= 0xDF) { //110x xxxx
|
||||
$working = $bytevalue & 0x1F;
|
||||
$bytesleft = 1;
|
||||
} elseif ($bytevalue <= 0xEF) { //1110 xxxx
|
||||
$working = $bytevalue & 0x0F;
|
||||
$bytesleft = 2;
|
||||
} else { //1111 0xxx
|
||||
$working = $bytevalue & 0x07;
|
||||
$bytesleft = 3;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** No bugs detected in iconv. */
|
||||
const ICONV_OK = 0;
|
||||
|
||||
/** Iconv truncates output if converting from UTF-8 to another
|
||||
* character set with //IGNORE, and a non-encodable character is found */
|
||||
const ICONV_TRUNCATES = 1;
|
||||
|
||||
/** Iconv does not support //IGNORE, making it unusable for
|
||||
* transcoding purposes */
|
||||
const ICONV_UNUSABLE = 2;
|
||||
|
||||
/**
|
||||
* glibc iconv has a known bug where it doesn't handle the magic
|
||||
* //IGNORE stanza correctly. In particular, rather than ignore
|
||||
* characters, it will return an EILSEQ after consuming some number
|
||||
* of characters, and expect you to restart iconv as if it were
|
||||
* an E2BIG. Old versions of PHP did not respect the errno, and
|
||||
* returned the fragment, so as a result you would see iconv
|
||||
* mysteriously truncating output. We can work around this by
|
||||
* manually chopping our input into segments of about 8000
|
||||
* characters, as long as PHP ignores the error code. If PHP starts
|
||||
* paying attention to the error code, iconv becomes unusable.
|
||||
*
|
||||
* @return int Error code indicating severity of bug.
|
||||
*/
|
||||
public static function testIconvTruncateBug()
|
||||
{
|
||||
static $code = null;
|
||||
if ($code === null) {
|
||||
// better not use iconv, otherwise infinite loop!
|
||||
$r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000));
|
||||
if ($r === false) {
|
||||
$code = self::ICONV_UNUSABLE;
|
||||
} elseif (($c = strlen($r)) < 9000) {
|
||||
$code = self::ICONV_TRUNCATES;
|
||||
} elseif ($c > 9000) {
|
||||
trigger_error(
|
||||
'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' .
|
||||
'include your iconv version as per phpversion()',
|
||||
E_USER_ERROR
|
||||
);
|
||||
} else {
|
||||
$code = self::ICONV_OK;
|
||||
}
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* This expensive function tests whether or not a given character
|
||||
* encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
|
||||
* fail this test, and require special processing. Variable width
|
||||
* encodings shouldn't ever fail.
|
||||
*
|
||||
* @param string $encoding Encoding name to test, as per iconv format
|
||||
* @param bool $bypass Whether or not to bypass the precompiled arrays.
|
||||
* @return Array of UTF-8 characters to their corresponding ASCII,
|
||||
* which can be used to "undo" any overzealous iconv action.
|
||||
*/
|
||||
public static function testEncodingSupportsASCII($encoding, $bypass = false)
|
||||
{
|
||||
// All calls to iconv here are unsafe, proof by case analysis:
|
||||
// If ICONV_OK, no difference.
|
||||
// If ICONV_TRUNCATE, all calls involve one character inputs,
|
||||
// so bug is not triggered.
|
||||
// If ICONV_UNUSABLE, this call is irrelevant
|
||||
static $encodings = array();
|
||||
if (!$bypass) {
|
||||
if (isset($encodings[$encoding])) {
|
||||
return $encodings[$encoding];
|
||||
}
|
||||
$lenc = strtolower($encoding);
|
||||
switch ($lenc) {
|
||||
case 'shift_jis':
|
||||
return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
|
||||
case 'johab':
|
||||
return array("\xE2\x82\xA9" => '\\');
|
||||
}
|
||||
if (strpos($lenc, 'iso-8859-') === 0) {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
$ret = array();
|
||||
if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) {
|
||||
return false;
|
||||
}
|
||||
for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
|
||||
$c = chr($i); // UTF-8 char
|
||||
$r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion
|
||||
if ($r === '' ||
|
||||
// This line is needed for iconv implementations that do not
|
||||
// omit characters that do not exist in the target character set
|
||||
($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c)
|
||||
) {
|
||||
// Reverse engineer: what's the UTF-8 equiv of this byte
|
||||
// sequence? This assumes that there's no variable width
|
||||
// encoding that doesn't support ASCII.
|
||||
$ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
|
||||
}
|
||||
}
|
||||
$encodings[$encoding] = $ret;
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,162 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a measurable length, with a string numeric magnitude
|
||||
* and a unit. This object is immutable.
|
||||
*/
|
||||
class HTMLPurifier_Length
|
||||
{
|
||||
|
||||
/**
|
||||
* String numeric magnitude.
|
||||
* @type string
|
||||
*/
|
||||
protected $n;
|
||||
|
||||
/**
|
||||
* String unit. False is permitted if $n = 0.
|
||||
* @type string|bool
|
||||
*/
|
||||
protected $unit;
|
||||
|
||||
/**
|
||||
* Whether or not this length is valid. Null if not calculated yet.
|
||||
* @type bool
|
||||
*/
|
||||
protected $isValid;
|
||||
|
||||
/**
|
||||
* Array Lookup array of units recognized by CSS 3
|
||||
* @type array
|
||||
*/
|
||||
protected static $allowedUnits = array(
|
||||
'em' => true, 'ex' => true, 'px' => true, 'in' => true,
|
||||
'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true,
|
||||
'ch' => true, 'rem' => true, 'vw' => true, 'vh' => true,
|
||||
'vmin' => true, 'vmax' => true
|
||||
);
|
||||
|
||||
/**
|
||||
* @param string $n Magnitude
|
||||
* @param bool|string $u Unit
|
||||
*/
|
||||
public function __construct($n = '0', $u = false)
|
||||
{
|
||||
$this->n = (string) $n;
|
||||
$this->unit = $u !== false ? (string) $u : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $s Unit string, like '2em' or '3.4in'
|
||||
* @return HTMLPurifier_Length
|
||||
* @warning Does not perform validation.
|
||||
*/
|
||||
public static function make($s)
|
||||
{
|
||||
if ($s instanceof HTMLPurifier_Length) {
|
||||
return $s;
|
||||
}
|
||||
$n_length = strspn($s, '1234567890.+-');
|
||||
$n = substr($s, 0, $n_length);
|
||||
$unit = substr($s, $n_length);
|
||||
if ($unit === '') {
|
||||
$unit = false;
|
||||
}
|
||||
return new HTMLPurifier_Length($n, $unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the number and unit.
|
||||
* @return bool
|
||||
*/
|
||||
protected function validate()
|
||||
{
|
||||
// Special case:
|
||||
if ($this->n === '+0' || $this->n === '-0') {
|
||||
$this->n = '0';
|
||||
}
|
||||
if ($this->n === '0' && $this->unit === false) {
|
||||
return true;
|
||||
}
|
||||
if ($this->unit === false || !ctype_lower((string)$this->unit)) {
|
||||
$this->unit = strtolower($this->unit);
|
||||
}
|
||||
if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) {
|
||||
return false;
|
||||
}
|
||||
// Hack:
|
||||
$def = new HTMLPurifier_AttrDef_CSS_Number();
|
||||
$result = $def->validate($this->n, false, false);
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
$this->n = $result;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of number.
|
||||
* @return string
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
if (!$this->isValid()) {
|
||||
return false;
|
||||
}
|
||||
return $this->n . $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves string numeric magnitude.
|
||||
* @return string
|
||||
*/
|
||||
public function getN()
|
||||
{
|
||||
return $this->n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves string unit.
|
||||
* @return string
|
||||
*/
|
||||
public function getUnit()
|
||||
{
|
||||
return $this->unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this length unit is valid.
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
if ($this->isValid === null) {
|
||||
$this->isValid = $this->validate();
|
||||
}
|
||||
return $this->isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
|
||||
* @param HTMLPurifier_Length $l
|
||||
* @return int
|
||||
* @warning If both values are too large or small, this calculation will
|
||||
* not work properly
|
||||
*/
|
||||
public function compareTo($l)
|
||||
{
|
||||
if ($l === false) {
|
||||
return false;
|
||||
}
|
||||
if ($l->unit !== $this->unit) {
|
||||
$converter = new HTMLPurifier_UnitConverter();
|
||||
$l = $converter->convert($l, $this->unit);
|
||||
if ($l === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $this->n - $l->n;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,387 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Forgivingly lexes HTML (SGML-style) markup into tokens.
|
||||
*
|
||||
* A lexer parses a string of SGML-style markup and converts them into
|
||||
* corresponding tokens. It doesn't check for well-formedness, although its
|
||||
* internal mechanism may make this automatic (such as the case of
|
||||
* HTMLPurifier_Lexer_DOMLex). There are several implementations to choose
|
||||
* from.
|
||||
*
|
||||
* A lexer is HTML-oriented: it might work with XML, but it's not
|
||||
* recommended, as we adhere to a subset of the specification for optimization
|
||||
* reasons. This might change in the future. Also, most tokenizers are not
|
||||
* expected to handle DTDs or PIs.
|
||||
*
|
||||
* This class should not be directly instantiated, but you may use create() to
|
||||
* retrieve a default copy of the lexer. Being a supertype, this class
|
||||
* does not actually define any implementation, but offers commonly used
|
||||
* convenience functions for subclasses.
|
||||
*
|
||||
* @note The unit tests will instantiate this class for testing purposes, as
|
||||
* many of the utility functions require a class to be instantiated.
|
||||
* This means that, even though this class is not runnable, it will
|
||||
* not be declared abstract.
|
||||
*
|
||||
* @par
|
||||
*
|
||||
* @note
|
||||
* We use tokens rather than create a DOM representation because DOM would:
|
||||
*
|
||||
* @par
|
||||
* -# Require more processing and memory to create,
|
||||
* -# Is not streamable, and
|
||||
* -# Has the entire document structure (html and body not needed).
|
||||
*
|
||||
* @par
|
||||
* However, DOM is helpful in that it makes it easy to move around nodes
|
||||
* without a lot of lookaheads to see when a tag is closed. This is a
|
||||
* limitation of the token system and some workarounds would be nice.
|
||||
*/
|
||||
class HTMLPurifier_Lexer
|
||||
{
|
||||
public $_entity_parser;
|
||||
|
||||
/**
|
||||
* Whether or not this lexer implements line-number/column-number tracking.
|
||||
* If it does, set to true.
|
||||
*/
|
||||
public $tracksLineNumbers = false;
|
||||
|
||||
// -- STATIC ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Retrieves or sets the default Lexer as a Prototype Factory.
|
||||
*
|
||||
* By default HTMLPurifier_Lexer_DOMLex will be returned. There are
|
||||
* a few exceptions involving special features that only DirectLex
|
||||
* implements.
|
||||
*
|
||||
* @note The behavior of this class has changed, rather than accepting
|
||||
* a prototype object, it now accepts a configuration object.
|
||||
* To specify your own prototype, set %Core.LexerImpl to it.
|
||||
* This change in behavior de-singletonizes the lexer object.
|
||||
*
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @return HTMLPurifier_Lexer
|
||||
* @throws HTMLPurifier_Exception
|
||||
*/
|
||||
public static function create($config)
|
||||
{
|
||||
if (!($config instanceof HTMLPurifier_Config)) {
|
||||
$lexer = $config;
|
||||
trigger_error(
|
||||
"Passing a prototype to
|
||||
HTMLPurifier_Lexer::create() is deprecated, please instead
|
||||
use %Core.LexerImpl",
|
||||
E_USER_WARNING
|
||||
);
|
||||
} else {
|
||||
$lexer = $config->get('Core.LexerImpl');
|
||||
}
|
||||
|
||||
$needs_tracking =
|
||||
$config->get('Core.MaintainLineNumbers') ||
|
||||
$config->get('Core.CollectErrors');
|
||||
|
||||
$inst = null;
|
||||
if (is_object($lexer)) {
|
||||
$inst = $lexer;
|
||||
} else {
|
||||
if (is_null($lexer)) {
|
||||
do {
|
||||
// auto-detection algorithm
|
||||
if ($needs_tracking) {
|
||||
$lexer = 'DirectLex';
|
||||
break;
|
||||
}
|
||||
|
||||
if (class_exists('DOMDocument', false) &&
|
||||
method_exists('DOMDocument', 'loadHTML') &&
|
||||
!extension_loaded('domxml')
|
||||
) {
|
||||
// check for DOM support, because while it's part of the
|
||||
// core, it can be disabled compile time. Also, the PECL
|
||||
// domxml extension overrides the default DOM, and is evil
|
||||
// and nasty and we shan't bother to support it
|
||||
$lexer = 'DOMLex';
|
||||
} else {
|
||||
$lexer = 'DirectLex';
|
||||
}
|
||||
} while (0);
|
||||
} // do..while so we can break
|
||||
|
||||
// instantiate recognized string names
|
||||
switch ($lexer) {
|
||||
case 'DOMLex':
|
||||
$inst = new HTMLPurifier_Lexer_DOMLex();
|
||||
break;
|
||||
case 'DirectLex':
|
||||
$inst = new HTMLPurifier_Lexer_DirectLex();
|
||||
break;
|
||||
case 'PH5P':
|
||||
$inst = new HTMLPurifier_Lexer_PH5P();
|
||||
break;
|
||||
default:
|
||||
throw new HTMLPurifier_Exception(
|
||||
"Cannot instantiate unrecognized Lexer type " .
|
||||
htmlspecialchars($lexer)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$inst) {
|
||||
throw new HTMLPurifier_Exception('No lexer was instantiated');
|
||||
}
|
||||
|
||||
// once PHP DOM implements native line numbers, or we
|
||||
// hack out something using XSLT, remove this stipulation
|
||||
if ($needs_tracking && !$inst->tracksLineNumbers) {
|
||||
throw new HTMLPurifier_Exception(
|
||||
'Cannot use lexer that does not support line numbers with ' .
|
||||
'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)'
|
||||
);
|
||||
}
|
||||
|
||||
return $inst;
|
||||
|
||||
}
|
||||
|
||||
// -- CONVENIENCE MEMBERS ---------------------------------------------
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->_entity_parser = new HTMLPurifier_EntityParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Most common entity to raw value conversion table for special entities.
|
||||
* @type array
|
||||
*/
|
||||
protected $_special_entity2str =
|
||||
array(
|
||||
'"' => '"',
|
||||
'&' => '&',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
''' => "'",
|
||||
''' => "'",
|
||||
''' => "'"
|
||||
);
|
||||
|
||||
public function parseText($string, $config) {
|
||||
return $this->parseData($string, false, $config);
|
||||
}
|
||||
|
||||
public function parseAttr($string, $config) {
|
||||
return $this->parseData($string, true, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses special entities into the proper characters.
|
||||
*
|
||||
* This string will translate escaped versions of the special characters
|
||||
* into the correct ones.
|
||||
*
|
||||
* @param string $string String character data to be parsed.
|
||||
* @return string Parsed character data.
|
||||
*/
|
||||
public function parseData($string, $is_attr, $config)
|
||||
{
|
||||
// following functions require at least one character
|
||||
if ($string === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// subtracts amps that cannot possibly be escaped
|
||||
$num_amp = substr_count($string, '&') - substr_count($string, '& ') -
|
||||
($string[strlen($string) - 1] === '&' ? 1 : 0);
|
||||
|
||||
if (!$num_amp) {
|
||||
return $string;
|
||||
} // abort if no entities
|
||||
$num_esc_amp = substr_count($string, '&');
|
||||
$string = strtr($string, $this->_special_entity2str);
|
||||
|
||||
// code duplication for sake of optimization, see above
|
||||
$num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
|
||||
($string[strlen($string) - 1] === '&' ? 1 : 0);
|
||||
|
||||
if ($num_amp_2 <= $num_esc_amp) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
// hmm... now we have some uncommon entities. Use the callback.
|
||||
if ($config->get('Core.LegacyEntityDecoder')) {
|
||||
$string = $this->_entity_parser->substituteSpecialEntities($string);
|
||||
} else {
|
||||
if ($is_attr) {
|
||||
$string = $this->_entity_parser->substituteAttrEntities($string);
|
||||
} else {
|
||||
$string = $this->_entity_parser->substituteTextEntities($string);
|
||||
}
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexes an HTML string into tokens.
|
||||
* @param $string String HTML.
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return HTMLPurifier_Token[] array representation of HTML.
|
||||
*/
|
||||
public function tokenizeHTML($string, $config, $context)
|
||||
{
|
||||
trigger_error('Call to abstract class', E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates CDATA sections into regular sections (through escaping).
|
||||
* @param string $string HTML string to process.
|
||||
* @return string HTML with CDATA sections escaped.
|
||||
*/
|
||||
protected static function escapeCDATA($string)
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/<!\[CDATA\[(.+?)\]\]>/s',
|
||||
array('HTMLPurifier_Lexer', 'CDATACallback'),
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special CDATA case that is especially convoluted for <script>
|
||||
* @param string $string HTML string to process.
|
||||
* @return string HTML with CDATA sections escaped.
|
||||
*/
|
||||
protected static function escapeCommentedCDATA($string)
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s',
|
||||
array('HTMLPurifier_Lexer', 'CDATACallback'),
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special Internet Explorer conditional comments should be removed.
|
||||
* @param string $string HTML string to process.
|
||||
* @return string HTML with conditional comments removed.
|
||||
*/
|
||||
protected static function removeIEConditional($string)
|
||||
{
|
||||
// https://github.com/freescout-helpdesk/freescout/issues/3894
|
||||
return preg_replace(
|
||||
// This change caused: https://github.com/freescout-helpdesk/freescout/issues/3907#issuecomment-2043126228
|
||||
// '#<!--\[if [^>]+\]>(.*?)<!\[endif\]-->#si', // probably should generalize for all strings
|
||||
// '$1',
|
||||
'#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings
|
||||
'',
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for escapeCDATA() that does the work.
|
||||
*
|
||||
* @warning Though this is public in order to let the callback happen,
|
||||
* calling it directly is not recommended.
|
||||
* @param array $matches PCRE matches array, with index 0 the entire match
|
||||
* and 1 the inside of the CDATA section.
|
||||
* @return string Escaped internals of the CDATA section.
|
||||
*/
|
||||
protected static function CDATACallback($matches)
|
||||
{
|
||||
// not exactly sure why the character set is needed, but whatever
|
||||
return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a piece of HTML and normalizes it by converting entities, fixing
|
||||
* encoding, extracting bits, and other good stuff.
|
||||
* @param string $html HTML.
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
* @todo Consider making protected
|
||||
*/
|
||||
public function normalize($html, $config, $context)
|
||||
{
|
||||
// normalize newlines to \n
|
||||
if ($config->get('Core.NormalizeNewlines')) {
|
||||
$html = str_replace("\r\n", "\n", $html);
|
||||
$html = str_replace("\r", "\n", $html);
|
||||
}
|
||||
|
||||
if ($config->get('HTML.Trusted')) {
|
||||
// escape convoluted CDATA
|
||||
$html = $this->escapeCommentedCDATA($html);
|
||||
}
|
||||
|
||||
// escape CDATA
|
||||
$html = $this->escapeCDATA($html);
|
||||
|
||||
$html = $this->removeIEConditional($html);
|
||||
|
||||
// extract body from document if applicable
|
||||
if ($config->get('Core.ConvertDocumentToFragment')) {
|
||||
$e = false;
|
||||
if ($config->get('Core.CollectErrors')) {
|
||||
$e =& $context->get('ErrorCollector');
|
||||
}
|
||||
$new_html = $this->extractBody($html);
|
||||
if ($e && $new_html != $html) {
|
||||
$e->send(E_WARNING, 'Lexer: Extracted body');
|
||||
}
|
||||
$html = $new_html;
|
||||
}
|
||||
|
||||
// expand entities that aren't the big five
|
||||
if ($config->get('Core.LegacyEntityDecoder')) {
|
||||
$html = $this->_entity_parser->substituteNonSpecialEntities($html);
|
||||
}
|
||||
|
||||
// clean into wellformed UTF-8 string for an SGML context: this has
|
||||
// to be done after entity expansion because the entities sometimes
|
||||
// represent non-SGML characters (horror, horror!)
|
||||
$html = HTMLPurifier_Encoder::cleanUTF8($html);
|
||||
|
||||
// if processing instructions are to removed, remove them now
|
||||
if ($config->get('Core.RemoveProcessingInstructions')) {
|
||||
$html = preg_replace('#<\?.+?\?>#s', '', $html);
|
||||
}
|
||||
|
||||
$hidden_elements = $config->get('Core.HiddenElements');
|
||||
if ($config->get('Core.AggressivelyRemoveScript') &&
|
||||
!($config->get('HTML.Trusted') || !$config->get('Core.RemoveScriptContents')
|
||||
|| empty($hidden_elements["script"]))) {
|
||||
$html = preg_replace('#<script[^>]*>.*?</script>#i', '', $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string of HTML (fragment or document) and returns the content
|
||||
* @todo Consider making protected
|
||||
*/
|
||||
public function extractBody($html)
|
||||
{
|
||||
$matches = array();
|
||||
$result = preg_match('|(.*?)<body[^>]*>(.*)</body>|is', $html ?? '', $matches);
|
||||
if ($result) {
|
||||
// Make sure it's not in a comment
|
||||
$comment_start = strrpos($matches[1], '<!--');
|
||||
$comment_end = strrpos($matches[1], '-->');
|
||||
if ($comment_start === false ||
|
||||
($comment_end !== false && $comment_end > $comment_start)) {
|
||||
return $matches[2];
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,338 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Parser that uses PHP 5's DOM extension (part of the core).
|
||||
*
|
||||
* In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
|
||||
* It gives us a forgiving HTML parser, which we use to transform the HTML
|
||||
* into a DOM, and then into the tokens. It is blazingly fast (for large
|
||||
* documents, it performs twenty times faster than
|
||||
* HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5.
|
||||
*
|
||||
* @note Any empty elements will have empty tokens associated with them, even if
|
||||
* this is prohibited by the spec. This is cannot be fixed until the spec
|
||||
* comes into play.
|
||||
*
|
||||
* @note PHP's DOM extension does not actually parse any entities, we use
|
||||
* our own function to do that.
|
||||
*
|
||||
* @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
|
||||
* If this is a huge problem, due to the fact that HTML is hand
|
||||
* edited and you are unable to get a parser cache that caches the
|
||||
* the output of HTML Purifier while keeping the original HTML lying
|
||||
* around, you may want to run Tidy on the resulting output or use
|
||||
* HTMLPurifier_DirectLex
|
||||
*/
|
||||
|
||||
class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
|
||||
{
|
||||
|
||||
/**
|
||||
* @type HTMLPurifier_TokenFactory
|
||||
*/
|
||||
private $factory;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// setup the factory
|
||||
parent::__construct();
|
||||
$this->factory = new HTMLPurifier_TokenFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return HTMLPurifier_Token[]
|
||||
*/
|
||||
public function tokenizeHTML($html, $config, $context)
|
||||
{
|
||||
$html = $this->normalize($html, $config, $context);
|
||||
|
||||
// attempt to armor stray angled brackets that cannot possibly
|
||||
// form tags and thus are probably being used as emoticons
|
||||
if ($config->get('Core.AggressivelyFixLt')) {
|
||||
$char = '[^a-z!\/]';
|
||||
$comment = "/<!--(.*?)(-->|\z)/is";
|
||||
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html ?? '');
|
||||
do {
|
||||
$old = $html;
|
||||
$html = preg_replace("/<($char)/i", '<\\1', $html);
|
||||
} while ($html !== $old);
|
||||
$html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
|
||||
}
|
||||
|
||||
// preprocess html, essential for UTF-8
|
||||
$html = $this->wrapHTML($html, $config, $context);
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->encoding = 'UTF-8'; // theoretically, the above has this covered
|
||||
|
||||
$options = 0;
|
||||
if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) {
|
||||
$options |= LIBXML_PARSEHUGE;
|
||||
}
|
||||
|
||||
set_error_handler(array($this, 'muteErrorHandler'));
|
||||
// loadHTML() fails on PHP 5.3 when second parameter is given
|
||||
if ($options) {
|
||||
$doc->loadHTML($html, $options);
|
||||
} else {
|
||||
$doc->loadHTML($html);
|
||||
}
|
||||
restore_error_handler();
|
||||
|
||||
$body = $doc->getElementsByTagName('html')->item(0)-> // <html>
|
||||
getElementsByTagName('body')->item(0); // <body>
|
||||
|
||||
$div = $body->getElementsByTagName('div')->item(0); // <div>
|
||||
$tokens = array();
|
||||
$this->tokenizeDOM($div, $tokens, $config);
|
||||
// If the div has a sibling, that means we tripped across
|
||||
// a premature </div> tag. So remove the div we parsed,
|
||||
// and then tokenize the rest of body. We can't tokenize
|
||||
// the sibling directly as we'll lose the tags in that case.
|
||||
if ($div->nextSibling) {
|
||||
$body->removeChild($div);
|
||||
$this->tokenizeDOM($body, $tokens, $config);
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterative function that tokenizes a node, putting it into an accumulator.
|
||||
* To iterate is human, to recurse divine - L. Peter Deutsch
|
||||
* @param DOMNode $node DOMNode to be tokenized.
|
||||
* @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens.
|
||||
* @return HTMLPurifier_Token of node appended to previously passed tokens.
|
||||
*/
|
||||
protected function tokenizeDOM($node, &$tokens, $config)
|
||||
{
|
||||
$level = 0;
|
||||
$nodes = array($level => new HTMLPurifier_Queue(array($node)));
|
||||
$closingNodes = array();
|
||||
do {
|
||||
while (!$nodes[$level]->isEmpty()) {
|
||||
$node = $nodes[$level]->shift(); // FIFO
|
||||
$collect = $level > 0 ? true : false;
|
||||
$needEndingTag = $this->createStartNode($node, $tokens, $collect, $config);
|
||||
if ($needEndingTag) {
|
||||
$closingNodes[$level][] = $node;
|
||||
}
|
||||
if ($node->childNodes && $node->childNodes->length) {
|
||||
$level++;
|
||||
$nodes[$level] = new HTMLPurifier_Queue();
|
||||
foreach ($node->childNodes as $childNode) {
|
||||
$nodes[$level]->push($childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
$level--;
|
||||
if ($level && isset($closingNodes[$level])) {
|
||||
while ($node = array_pop($closingNodes[$level])) {
|
||||
$this->createEndNode($node, $tokens);
|
||||
}
|
||||
}
|
||||
} while ($level > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Portably retrieve the tag name of a node; deals with older versions
|
||||
* of libxml like 2.7.6
|
||||
* @param DOMNode $node
|
||||
*/
|
||||
protected function getTagName($node)
|
||||
{
|
||||
if (isset($node->tagName)) {
|
||||
return $node->tagName;
|
||||
} else if (isset($node->nodeName)) {
|
||||
return $node->nodeName;
|
||||
} else if (isset($node->localName)) {
|
||||
return $node->localName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Portably retrieve the data of a node; deals with older versions
|
||||
* of libxml like 2.7.6
|
||||
* @param DOMNode $node
|
||||
*/
|
||||
protected function getData($node)
|
||||
{
|
||||
if (isset($node->data)) {
|
||||
return $node->data;
|
||||
} else if (isset($node->nodeValue)) {
|
||||
return $node->nodeValue;
|
||||
} else if (isset($node->textContent)) {
|
||||
return $node->textContent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param DOMNode $node DOMNode to be tokenized.
|
||||
* @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens.
|
||||
* @param bool $collect Says whether or start and close are collected, set to
|
||||
* false at first recursion because it's the implicit DIV
|
||||
* tag you're dealing with.
|
||||
* @return bool if the token needs an endtoken
|
||||
* @todo data and tagName properties don't seem to exist in DOMNode?
|
||||
*/
|
||||
protected function createStartNode($node, &$tokens, $collect, $config)
|
||||
{
|
||||
// intercept non element nodes. WE MUST catch all of them,
|
||||
// but we're not getting the character reference nodes because
|
||||
// those should have been preprocessed
|
||||
if ($node->nodeType === XML_TEXT_NODE) {
|
||||
$data = $this->getData($node); // Handle variable data property
|
||||
if ($data !== null) {
|
||||
$tokens[] = $this->factory->createText($data);
|
||||
}
|
||||
return false;
|
||||
} elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
|
||||
// undo libxml's special treatment of <script> and <style> tags
|
||||
$last = end($tokens);
|
||||
$data = $node->data;
|
||||
// (note $node->tagname is already normalized)
|
||||
if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
|
||||
$new_data = trim($data);
|
||||
if (substr($new_data, 0, 4) === '<!--') {
|
||||
$data = substr($new_data, 4);
|
||||
if (substr($data, -3) === '-->') {
|
||||
$data = substr($data, 0, -3);
|
||||
} else {
|
||||
// Highly suspicious! Not sure what to do...
|
||||
}
|
||||
}
|
||||
}
|
||||
$tokens[] = $this->factory->createText($this->parseText($data, $config));
|
||||
return false;
|
||||
} elseif ($node->nodeType === XML_COMMENT_NODE) {
|
||||
// this is code is only invoked for comments in script/style in versions
|
||||
// of libxml pre-2.6.28 (regular comments, of course, are still
|
||||
// handled regularly)
|
||||
$tokens[] = $this->factory->createComment($node->data);
|
||||
return false;
|
||||
} elseif ($node->nodeType !== XML_ELEMENT_NODE) {
|
||||
// not-well tested: there may be other nodes we have to grab
|
||||
return false;
|
||||
}
|
||||
$attr = $node->hasAttributes() ? $this->transformAttrToAssoc($node->attributes) : array();
|
||||
$tag_name = $this->getTagName($node); // Handle variable tagName property
|
||||
if (empty($tag_name)) {
|
||||
return (bool) $node->childNodes->length;
|
||||
}
|
||||
// We still have to make sure that the element actually IS empty
|
||||
if (!$node->childNodes->length) {
|
||||
if ($collect) {
|
||||
$tokens[] = $this->factory->createEmpty($tag_name, $attr);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if ($collect) {
|
||||
$tokens[] = $this->factory->createStart($tag_name, $attr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DOMNode $node
|
||||
* @param HTMLPurifier_Token[] $tokens
|
||||
*/
|
||||
protected function createEndNode($node, &$tokens)
|
||||
{
|
||||
$tag_name = $this->getTagName($node); // Handle variable tagName property
|
||||
$tokens[] = $this->factory->createEnd($tag_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
|
||||
*
|
||||
* @param DOMNamedNodeMap $node_map DOMNamedNodeMap of DOMAttr objects.
|
||||
* @return array Associative array of attributes.
|
||||
*/
|
||||
protected function transformAttrToAssoc($node_map)
|
||||
{
|
||||
// NamedNodeMap is documented very well, so we're using undocumented
|
||||
// features, namely, the fact that it implements Iterator and
|
||||
// has a ->length attribute
|
||||
if ($node_map->length === 0) {
|
||||
return array();
|
||||
}
|
||||
$array = array();
|
||||
foreach ($node_map as $attr) {
|
||||
$array[$attr->name] = $attr->value;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error handler that mutes all errors
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
*/
|
||||
public function muteErrorHandler($errno, $errstr)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function for undoing escaping of stray angled brackets
|
||||
* in comments
|
||||
* @param array $matches
|
||||
* @return string
|
||||
*/
|
||||
public function callbackUndoCommentSubst($matches)
|
||||
{
|
||||
return '<!--' . strtr($matches[1], array('&' => '&', '<' => '<')) . $matches[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function that entity-izes ampersands in comments so that
|
||||
* callbackUndoCommentSubst doesn't clobber them
|
||||
* @param array $matches
|
||||
* @return string
|
||||
*/
|
||||
public function callbackArmorCommentEntities($matches)
|
||||
{
|
||||
return '<!--' . str_replace('&', '&', $matches[1]) . $matches[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an HTML fragment in the necessary HTML
|
||||
* @param string $html
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return string
|
||||
*/
|
||||
protected function wrapHTML($html, $config, $context, $use_div = true)
|
||||
{
|
||||
$def = $config->getDefinition('HTML');
|
||||
$ret = '';
|
||||
|
||||
if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
|
||||
$ret .= '<!DOCTYPE html ';
|
||||
if (!empty($def->doctype->dtdPublic)) {
|
||||
$ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
|
||||
}
|
||||
if (!empty($def->doctype->dtdSystem)) {
|
||||
$ret .= '"' . $def->doctype->dtdSystem . '" ';
|
||||
}
|
||||
$ret .= '>';
|
||||
}
|
||||
|
||||
$ret .= '<html><head>';
|
||||
$ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
|
||||
// No protection if $html contains a stray </div>!
|
||||
$ret .= '</head><body>';
|
||||
if ($use_div) $ret .= '<div>';
|
||||
$ret .= $html;
|
||||
if ($use_div) $ret .= '</div>';
|
||||
$ret .= '</body></html>';
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -1,275 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Mews\Purifier;
|
||||
|
||||
/**
|
||||
* Laravel 5 HTMLPurifier package
|
||||
*
|
||||
* @copyright Copyright (c) 2015 MeWebStudio
|
||||
* @version 2.0.0
|
||||
* @author Muharrem ERİN
|
||||
* @contact me@mewebstudio.com
|
||||
* @web http://www.mewebstudio.com
|
||||
* @date 2014-04-02
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
use Exception;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
use HTMLPurifier_HTMLDefinition;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class Purifier
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* @var Repository
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var HTMLPurifier
|
||||
*/
|
||||
protected $purifier;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Filesystem $files
|
||||
* @param Repository $config
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(Filesystem $files, Repository $config)
|
||||
{
|
||||
$this->files = $files;
|
||||
$this->config = $config;
|
||||
|
||||
$this->setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function setUp()
|
||||
{
|
||||
if (!$this->config->has('purifier')) {
|
||||
throw new Exception('Configuration parameters not loaded!');
|
||||
}
|
||||
|
||||
$this->checkCacheDirectory();
|
||||
|
||||
// Create a new configuration object
|
||||
$config = $this->getConfig();
|
||||
|
||||
// Create HTMLPurifier object
|
||||
$this->purifier = new HTMLPurifier($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom definition
|
||||
*
|
||||
* @see http://htmlpurifier.org/docs/enduser-customize.html
|
||||
* @param array $definitionConfig
|
||||
* @param HTMLPurifier_Config $configObject Defaults to using default config
|
||||
*
|
||||
* @return HTMLPurifier_Config $configObject
|
||||
*/
|
||||
private function addCustomDefinition(array $definitionConfig, ?HTMLPurifier_Config $configObject = null)
|
||||
{
|
||||
if (!$configObject) {
|
||||
$configObject = HTMLPurifier_Config::createDefault();
|
||||
$configObject->loadArray($this->getConfig());
|
||||
}
|
||||
|
||||
// Setup the custom definition
|
||||
$configObject->set('HTML.DefinitionID', $definitionConfig['id']);
|
||||
$configObject->set('HTML.DefinitionRev', $definitionConfig['rev']);
|
||||
|
||||
// Enable debug mode
|
||||
if (!isset($definitionConfig['debug']) || $definitionConfig['debug']) {
|
||||
$configObject->set('Cache.DefinitionImpl', null);
|
||||
}
|
||||
|
||||
// Start configuring the definition
|
||||
if ($def = $configObject->maybeGetRawHTMLDefinition()) {
|
||||
// Create the definition attributes
|
||||
if (!empty($definitionConfig['attributes'])) {
|
||||
$this->addCustomAttributes($definitionConfig['attributes'], $def);
|
||||
}
|
||||
|
||||
// Create the definition elements
|
||||
if (!empty($definitionConfig['elements'])) {
|
||||
$this->addCustomElements($definitionConfig['elements'], $def);
|
||||
}
|
||||
}
|
||||
|
||||
return $configObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provided attributes to the provided definition
|
||||
*
|
||||
* @param array $attributes
|
||||
* @param HTMLPurifier_HTMLDefinition $definition
|
||||
*
|
||||
* @return HTMLPurifier_HTMLDefinition $definition
|
||||
*/
|
||||
private function addCustomAttributes(array $attributes, HTMLPurifier_HTMLDefinition $definition)
|
||||
{
|
||||
foreach ($attributes as $attribute) {
|
||||
// Get configuration of attribute
|
||||
$required = !empty($attribute[3]) ? true : false;
|
||||
$onElement = $attribute[0];
|
||||
$attrName = $required ? $attribute[1] . '*' : $attribute[1];
|
||||
$validValues = $attribute[2];
|
||||
|
||||
$definition->addAttribute($onElement, $attrName, $validValues);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add provided elements to the provided definition
|
||||
*
|
||||
* @param array $elements
|
||||
* @param HTMLPurifier_HTMLDefinition $definition
|
||||
*
|
||||
* @return HTMLPurifier_HTMLDefinition $definition
|
||||
*/
|
||||
private function addCustomElements(array $elements, HTMLPurifier_HTMLDefinition $definition)
|
||||
{
|
||||
foreach ($elements as $element) {
|
||||
// Get configuration of element
|
||||
$name = $element[0];
|
||||
$contentSet = $element[1];
|
||||
$allowedChildren = $element[2];
|
||||
$attributeCollection = $element[3];
|
||||
$attributes = isset($element[4]) ? $element[4] : null;
|
||||
|
||||
if (!empty($attributes)) {
|
||||
$definition->addElement($name, $contentSet, $allowedChildren, $attributeCollection, $attributes);
|
||||
} else {
|
||||
$definition->addElement($name, $contentSet, $allowedChildren, $attributeCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check/Create cache directory
|
||||
*/
|
||||
private function checkCacheDirectory()
|
||||
{
|
||||
$cachePath = $this->config->get('purifier.cachePath');
|
||||
|
||||
if ($cachePath) {
|
||||
if (!$this->files->isDirectory($cachePath)) {
|
||||
$this->files->makeDirectory($cachePath, $this->config->get('purifier.cacheFileMode', 0755),true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $config
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function getConfig($config = null)
|
||||
{
|
||||
// Create a new configuration object
|
||||
$configObject = HTMLPurifier_Config::createDefault();
|
||||
|
||||
// Allow configuration to be modified
|
||||
if (! $this->config->get('purifier.finalize')) {
|
||||
$configObject->autoFinalize = false;
|
||||
}
|
||||
|
||||
// Set default config
|
||||
$defaultConfig = [];
|
||||
$defaultConfig['Core.Encoding'] = $this->config->get('purifier.encoding');
|
||||
$defaultConfig['Cache.SerializerPath'] = $this->config->get('purifier.cachePath');
|
||||
$defaultConfig['Cache.SerializerPermissions'] = $this->config->get('purifier.cacheFileMode', 0755);
|
||||
|
||||
if (! $config) {
|
||||
$config = $this->config->get('purifier.settings.default');
|
||||
} elseif (is_string($config)) {
|
||||
$config = $this->config->get('purifier.settings.' . $config);
|
||||
}
|
||||
|
||||
if (! is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
// Merge configurations
|
||||
$config = $defaultConfig + $config;
|
||||
|
||||
// Load to Purifier config
|
||||
$configObject->loadArray($config);
|
||||
|
||||
// Load custom definition if set
|
||||
if ($definitionConfig = $this->config->get('purifier.settings.custom_definition')) {
|
||||
$this->addCustomDefinition($definitionConfig, $configObject);
|
||||
}
|
||||
|
||||
// Load custom elements if set
|
||||
if ($elements = $this->config->get('purifier.settings.custom_elements')) {
|
||||
if ($def = $configObject->maybeGetRawHTMLDefinition()) {
|
||||
$this->addCustomElements($elements, $def);
|
||||
}
|
||||
}
|
||||
|
||||
// Load custom attributes if set
|
||||
if ($attributes = $this->config->get('purifier.settings.custom_attributes')) {
|
||||
if ($def = $configObject->maybeGetRawHTMLDefinition()) {
|
||||
$this->addCustomAttributes($attributes, $def);
|
||||
}
|
||||
}
|
||||
|
||||
return $configObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $dirty
|
||||
* @param null $config
|
||||
* @param \Closure|null $postCreateConfigHook
|
||||
* @return mixed
|
||||
*/
|
||||
public function clean($dirty, $config = null, ?\Closure $postCreateConfigHook = null)
|
||||
{
|
||||
if (is_array($dirty)) {
|
||||
return array_map(function ($item) use ($config) {
|
||||
return $this->clean($item, $config);
|
||||
}, $dirty);
|
||||
}
|
||||
|
||||
$configObject = null;
|
||||
if ($config !== null) {
|
||||
$configObject = $this->getConfig($config);
|
||||
|
||||
if ($postCreateConfigHook !== null) {
|
||||
$postCreateConfigHook->call($this, $configObject);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->purifier->purify($dirty, $configObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTMLPurifier instance.
|
||||
*
|
||||
* @return \HTMLPurifier
|
||||
*/
|
||||
public function getInstance()
|
||||
{
|
||||
return $this->purifier;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user