diff --git a/composer.json b/composer.json index 027d5d7..fd4c700 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,6 @@ "yiisoft/yii2-composer": "~2.0.4", "psr/simple-cache": "~1.0.0", "psr/http-message": "~1.0.0", - "ezyang/htmlpurifier": "~4.6", "cebe/markdown": "~1.0.0 | ~1.1.0", "bower-asset/jquery": "3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "~3.2.2 | ~3.3.5", @@ -84,6 +83,7 @@ "bower-asset/yii2-pjax": "~2.0.1" }, "require-dev": { + "ezyang/htmlpurifier": "~4.6", "phpunit/phpunit": "~6.2.3", "cebe/indent": "~1.0.2", "friendsofphp/php-cs-fixer": "~2.2.3" @@ -95,6 +95,7 @@ } ], "suggest": { + "ezyang/htmlpurifier": "required at `yii\\helpers\\HtmlPurifier` for 'html' data format support (e.g. `yii\\i18n\\Formatter:asHtml()`)", "yiisoft/yii2-coding-standards": "you can use this package to check for code style issues when contributing to yii" }, "autoload": { diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5e9f040..7c9a322 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -39,6 +39,7 @@ Yii Framework 2 Change Log - Chg #14178: Removed HHVM-specific code (samdark) - Enh #14671: use `random_int()` instead of `mt_rand()` to generate cryptographically secure pseudo-random integers (yyxx9988) - Chg #14761: Removed Yii autoloader in favor of Composer's PSR-4 implementation (samdark) +- Chg #15448: Package "ezyang/htmlpurifier" has been made optional and is not installed by default (klimov-paul) 2.0.14 under development ------------------------ diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 63f4520..880fd71 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -154,6 +154,9 @@ Upgrade from Yii 2.0.x `yii\validators\FileValidator::buildMimeTypeRegexp()` have been made `public`. Make sure you use correct access level specification in case you override these methods. * Default script position for the `yii\web\View::registerJs()` changed to `View::POS_END`. +* Package "ezyang/htmlpurifier" has been made optional and is not installed by default. If you need to use + `yii\helpers\HtmlPurifier` or `yii\i18n\Formatter::asHtml()` (e.g. 'html' data format), you'll have to install + this package manually for your project. Upgrade from Yii 2.0.13 diff --git a/framework/composer.json b/framework/composer.json index d690317..573c655 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -71,12 +71,14 @@ "yiisoft/yii2-composer": "~2.0.4", "psr/simple-cache": "~1.0.0", "psr/http-message": "~1.0.0", - "ezyang/htmlpurifier": "~4.6", "cebe/markdown": "~1.0.0 | ~1.1.0", "bower-asset/jquery": "3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/punycode": "1.3.*", "bower-asset/yii2-pjax": "~2.0.1" }, + "suggest": { + "ezyang/htmlpurifier": "version '~4.6' required at 'yii\\helpers\\HtmlPurifier' for 'html' data format support (e.g. 'yii\\i18n\\Formatter:asHtml()' and 'yii\\helpers\\StringHelper::truncateHtml()')" + }, "autoload": { "psr-4": {"yii\\": ""}, "classmap": [ diff --git a/framework/helpers/BaseHtmlPurifier.php b/framework/helpers/BaseHtmlPurifier.php index 3c748a0..23f10de 100644 --- a/framework/helpers/BaseHtmlPurifier.php +++ b/framework/helpers/BaseHtmlPurifier.php @@ -7,11 +7,22 @@ namespace yii\helpers; +use Yii; +use yii\base\InvalidConfigException; + /** * BaseHtmlPurifier provides concrete implementation for [[HtmlPurifier]]. * * Do not use BaseHtmlPurifier. Use [[HtmlPurifier]] instead. * + * This helper requires `ezyang/htmlpurifier` library to be installed. This can be done via composer: + * + * ``` + * composer require --prefer-dist "ezyang/htmlpurifier:~4.6" + * ``` + * + * @see http://htmlpurifier.org/ + * * @author Alexander Makarov * @since 2.0 */ @@ -39,28 +50,128 @@ class BaseHtmlPurifier * ->addAttribute('img', 'data-type', 'Text'); * }); * ``` - * * @return string the purified HTML content. */ public static function process($content, $config = null) { - $configInstance = \HTMLPurifier_Config::create($config instanceof \Closure ? null : $config); + $configInstance = static::createConfig($config); $configInstance->autoFinalize = false; + $purifier = \HTMLPurifier::instance($configInstance); - $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); - $purifier->config->set('Cache.SerializerPermissions', 0775); + + return $purifier->purify($content); + } + + /** + * Truncate a HTML string. + * + * @param string $html The HTML string to be truncated. + * @param int $count + * @param string $suffix String to append to the end of the truncated string. + * @param string|bool $encoding + * @return string + * @since 2.1.0 + */ + public static function truncate($html, $count, $suffix, $encoding = false) + { + $config = static::createConfig(); + + $lexer = \HTMLPurifier_Lexer::create($config); + $tokens = $lexer->tokenizeHTML($html, $config, new \HTMLPurifier_Context()); + $openTokens = []; + $totalCount = 0; + $depth = 0; + $truncated = []; + foreach ($tokens as $token) { + if ($token instanceof \HTMLPurifier_Token_Start) { //Tag begins + $openTokens[$depth] = $token->name; + $truncated[] = $token; + ++$depth; + } elseif ($token instanceof \HTMLPurifier_Token_Text && $totalCount <= $count) { //Text + if ($encoding === false) { + preg_match('/^(\s*)/um', $token->data, $prefixSpace) ?: $prefixSpace = ['', '']; + $token->data = $prefixSpace[1] . StringHelper::truncateWords(ltrim($token->data), $count - $totalCount, ''); + $currentCount = StringHelper::countWords($token->data); + } else { + $token->data = StringHelper::truncate($token->data, $count - $totalCount, '', $encoding); + $currentCount = mb_strlen($token->data, $encoding); + } + $totalCount += $currentCount; + $truncated[] = $token; + } elseif ($token instanceof \HTMLPurifier_Token_End) { //Tag ends + if ($token->name === $openTokens[$depth - 1]) { + --$depth; + unset($openTokens[$depth]); + $truncated[] = $token; + } + } elseif ($token instanceof \HTMLPurifier_Token_Empty) { //Self contained tags, i.e. etc. + $truncated[] = $token; + } + if ($totalCount >= $count) { + if (0 < count($openTokens)) { + krsort($openTokens); + foreach ($openTokens as $name) { + $truncated[] = new \HTMLPurifier_Token_End($name); + } + } + break; + } + } + $context = new \HTMLPurifier_Context(); + $generator = new \HTMLPurifier_Generator($config, $context); + + return $generator->generateFromTokens($truncated) . ($totalCount >= $count ? $suffix : ''); + } + + /** + * Creates a HtmlPurifier configuration instance. + * @see \HTMLPurifier_Config::create() + * @param array|\Closure|null $config The config to use for HtmlPurifier. + * If not specified or `null` the default config will be used. + * You can use an array or an anonymous function to provide configuration options: + * + * - An array will be passed to the `HTMLPurifier_Config::create()` method. + * - An anonymous function will be called after the config was created. + * The signature should be: `function($config)` where `$config` will be an + * instance of `HTMLPurifier_Config`. + * + * Here is a usage example of such a function: + * + * ```php + * // Allow the HTML5 data attribute `data-type` on `img` elements. + * $content = HtmlPurifier::process($content, function ($config) { + * $config->getHTMLDefinition(true) + * ->addAttribute('img', 'data-type', 'Text'); + * }); + * ``` + * + * @return \HTMLPurifier_Config HTMLPurifier config instance. + * @throws InvalidConfigException in case "ezyang/htmlpurifier" package is not available. + * @since 2.1.0 + */ + public static function createConfig($config = null) + { + if (!class_exists(\HTMLPurifier_Config::class)) { + throw new InvalidConfigException('Unable to load "' . \HTMLPurifier_Config::class . '" class. Make sure you have installed "ezyang/htmlpurifier:~4.6" composer package.'); + } + + $configInstance = \HTMLPurifier_Config::create($config instanceof \Closure ? null : $config); + if (Yii::$app !== null) { + $configInstance->set('Cache.SerializerPath', Yii::$app->getRuntimePath()); + $configInstance->set('Cache.SerializerPermissions', 0775); + } static::configure($configInstance); if ($config instanceof \Closure) { call_user_func($config, $configInstance); } - return $purifier->purify($content); + return $configInstance; } /** * Allow the extended HtmlPurifier class to set some default config options. - * @param \HTMLPurifier_Config $config + * @param \HTMLPurifier_Config $config HTMLPurifier config instance. * @since 2.0.3 */ protected static function configure($config) diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index f7b1a40..8624d38 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -154,10 +154,7 @@ class BaseStringHelper */ protected static function truncateHtml($string, $count, $suffix, $encoding = false) { - $config = \HTMLPurifier_Config::create(null); - if (Yii::$app !== null) { - $config->set('Cache.SerializerPath', Yii::$app->getRuntimePath()); - } + $config = HtmlPurifier::createConfig(); $lexer = \HTMLPurifier_Lexer::create($config); $tokens = $lexer->tokenizeHTML($string, $config, new \HTMLPurifier_Context()); $openTokens = []; diff --git a/tests/framework/helpers/HtmlPurifierTest.php b/tests/framework/helpers/HtmlPurifierTest.php new file mode 100644 index 0000000..7b29f2c --- /dev/null +++ b/tests/framework/helpers/HtmlPurifierTest.php @@ -0,0 +1,54 @@ +markTestSkipped('"ezyang/htmlpurifier" package required'); + return; + } + + parent::setUp(); + $this->mockApplication(); + } + + /** + * Data provider for [[testProcess()]] + * @return array test data. + */ + public function dataProviderProcess() + { + return [ + ['Some html', 'Some html'], + ['Some script', 'Some script'], + ]; + } + + /** + * @dataProvider dataProviderProcess + * + * @param string $content + * @param string $expectedResult + */ + public function testProcess($content, $expectedResult) + { + $this->assertSame($expectedResult, HtmlPurifier::process($content)); + } +} \ No newline at end of file