diff --git a/composer.json b/composer.json index d8395ab68da..cb940d2fbbc 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ "composer/package-versions-deprecated": "1.11.99.5", "composer/semver": "3.4.3", "endroid/qr-code": "5.1.0", + "ezyang/htmlpurifier": "4.17.0", "firebase/php-jwt": "6.10.1", "guzzlehttp/guzzle": "7.9.2", "jaybizzle/crawler-detect": "^1.2", diff --git a/composer.lock b/composer.lock index 034a6627e41..a376026b19e 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "8d386ab3bfc89dcd3e5d371af7a37be5", + "content-hash": "0b5a440adf0f633dbb78b4f6e8c91a3f", "packages": [ { "name": "ahand/mobileesp", @@ -1214,6 +1214,67 @@ ], "time": "2024-09-08T08:52:55+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.17.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "shasum": "" + }, + "require": { + "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" + }, + "require-dev": { + "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": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + }, + "time": "2023-11-17T15:01:25+00:00" + }, { "name": "filp/whoops", "version": "2.16.0", diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 509ed20ffa9..60e82587ebf 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -2642,3 +2642,24 @@ description = "The REST API provides access to search functions and records cont ; By default, VuFind sorts text in a locale-agnostic way; if this setting is ; turned on, the current user-selected locale will impact sort order. ;use_locale_sorting = true + +; Data retrieved from records is escaped by default, on the assumption that it does not contain HTML. +; This section can be used to allow limited HTML in specific contexts; set values to true to allow HTML, +; and leave false to escape content. +; See http://htmlpurifier.org/ for more information about the purifier. +; See also \VuFind\View\Helper\Root\CleanHtmlFactory for additional information. +[Allowed_HTML_Contexts] +; Title on the record page +;title = false +; Alternative titles on the record page +;title-alt = false +; Summary on the record page +;summary = false + +; \VuFind\View\Helper\Root\CleanHtmlFactory defines the defaults for HTML elements allowed in different +; rendering contexts (e.g. default, heading or link). You can customize the defaults by setting the +; context-specific list of allowed elements below. +[HTML_Rendering_Contexts] +;allowed_elements[default] = "" +;allowed_elements[heading] = "a,b,br,em,i,span,strong,sub,sup,u" +;allowed_elements[link] = "abbr,acronym,b,bdo,big,br,cite,dfn,em,i,img,q,samp,small,span,strong,sub,sup,var" diff --git a/module/VuFind/src/VuFind/Content/Summaries/Demo.php b/module/VuFind/src/VuFind/Content/Summaries/Demo.php index 2e7d91deacc..c3a22f191c9 100644 --- a/module/VuFind/src/VuFind/Content/Summaries/Demo.php +++ b/module/VuFind/src/VuFind/Content/Summaries/Demo.php @@ -29,6 +29,8 @@ namespace VuFind\Content\Summaries; +use VuFind\String\PropertyString; + /** * Demo (fake data) summaries content loader. * @@ -56,6 +58,8 @@ public function loadByIsbn($key, \VuFindCode\ISBN $isbnObj) return [ 'Demo summary key: ' . $key, 'Demo summary ISBN: ' . $isbnObj->get13(), + (new PropertyString('Demo non-HTML summary')) + ->setHtml('Demo HTML Summary:
Foo
') + ->setIds(['id_foo', 'id_bar']); + $str['attr'] = 'bonus'; + $str['attr2'] = 'bonus2'; + + $this->assertEquals('Foo', (string)$str); + $this->assertEquals('Foo', $str->getString()); + $this->assertEquals('Foo
', $str->getHtml()); + $this->assertEquals(['id_foo', 'id_bar'], $str->getIds()); + $this->assertEquals('bonus', $str['attr']); + $this->assertEquals('bonus2', $str['attr2']); + $this->assertTrue(isset($str['attr'])); + $this->assertFalse(isset($str['nattr'])); + + unset($str['attr']); + unset($str['nattr']); + $this->assertFalse(isset($str['attr'])); + + $str->addId('id_baz'); + $this->assertEquals(['id_foo', 'id_bar', 'id_baz'], $str->getIds()); + + $this->assertNull($str->isHtmlTrusted()); + $str->setHtmlTrusted(true); + $this->assertTrue($str->isHtmlTrusted()); + } + + /** + * Data provider for testFromHtml + * + * @return array + */ + public static function fromHtmlProvider(): array + { + return [ + 'plain string, no attributes' => [ + 'Plain string', + [], + 'Plain string', + 'Plain string', + [], + ], + 'plain string, attributes' => [ + 'Plain string', + ['foo' => 'bar', 'bar' => 'baz'], + 'Plain string', + 'Plain string', + ['foo' => 'bar', 'bar' => 'baz'], + ], + 'HTML string, array attributes' => [ + 'HTML string', + ['foo' => ['bar', 'baz']], + 'HTML string', + 'HTML string', + ['foo' => ['bar', 'baz']], + ], + 'HTML string, reserved array attributes' => [ + 'HTML string', + ['__html' => ['bar', 'baz']], + 'HTML string', + 'HTML string', + ['__html' => 'HTML string'], + ], + ]; + } + + /** + * Test the fromHtml static constructor + * + * @param string $html Input HTML + * @param array $attrs Additional attributes + * @param string $expectedPlain Expected plain text result + * @param string $expectedHtml Expected HTML result + * @param array $expectedAttrs Expected attributes + * + * @return void + * + * @dataProvider fromHtmlProvider + */ + public function testFromHtml( + string $html, + array $attrs, + string $expectedPlain, + string $expectedHtml, + array $expectedAttrs + ): void { + $str = PropertyString::fromHtml($html, $attrs); + $this->assertEquals($expectedPlain, (string)$str); + $this->assertEquals($expectedHtml, $str->getHtml()); + foreach ($expectedAttrs as $key => $value) { + $this->assertEquals($value, $str[$key]); + } + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CleanHtmlTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CleanHtmlTest.php new file mode 100644 index 00000000000..97e23288345 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CleanHtmlTest.php @@ -0,0 +1,96 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ + +namespace VuFindTest\View\Helper\Root; + +use VuFindTest\Feature\ViewTrait; + +/** + * CleanHtml view helper Test Class + * + * @category VuFind + * @package Tests + * @author Ere Maijala=$this->truncate($summary, 300)?>
+ + truncate($summary, 300); ?> +=$this->escapeOrCleanHtml($shortSummary, $summary, 'summary')?>
- 300): ?> +=$this->transEsc('Full description')?>
@@ -86,7 +88,7 @@ = - $this->record($this->driver)->renderTemplate( + $recordHelper->renderTemplate( 'core-fields.phtml', [ 'driver' => $this->driver, diff --git a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml index bd4333cb4e5..37ace4afa6b 100644 --- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -1,10 +1,17 @@ driver->getSummary() as $summary): ?> - =$this->escapeHtml($summary) ?>