From 3b17e2a8d157828fe1ed382084e3c060bf5dad51 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Fri, 11 Oct 2024 13:16:08 +0300 Subject: [PATCH 1/8] Add PropertyString, cleanHtml helper and escapeHtmlExt helper. --- composer.json | 1 + composer.lock | 63 +++++- .../src/VuFind/Content/Summaries/Demo.php | 4 + .../src/VuFind/RecordDriver/DefaultRecord.php | 23 ++- .../src/VuFind/String/PropertyString.php | 187 ++++++++++++++++++ .../VuFind/String/PropertyStringInterface.php | 92 +++++++++ .../src/VuFind/View/Helper/Root/CleanHtml.php | 79 ++++++++ .../View/Helper/Root/CleanHtmlFactory.php | 147 ++++++++++++++ .../View/Helper/Root/RecordDataFormatter.php | 4 + .../DefaultRecord/data-summary.phtml | 16 +- .../DefaultRecord/data-summary.phtml | 16 +- themes/root/theme.config.php | 5 + 12 files changed, 623 insertions(+), 14 deletions(-) create mode 100644 module/VuFind/src/VuFind/String/PropertyString.php create mode 100644 module/VuFind/src/VuFind/String/PropertyStringInterface.php create mode 100644 module/VuFind/src/VuFind/View/Helper/Root/CleanHtml.php create mode 100644 module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php diff --git a/composer.json b/composer.json index f399ac8a8e0..171f00c8877 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "composer/package-versions-deprecated": "1.11.99.5", "composer/semver": "3.4.2", "endroid/qr-code": "5.0.9", + "ezyang/htmlpurifier": "4.17.0", "guzzlehttp/guzzle": "7.9.2", "jaybizzle/crawler-detect": "^1.2", "laminas/laminas-cache": "3.12.2", diff --git a/composer.lock b/composer.lock index f227e348fb1..da4733f6d52 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": "a3b3369d5130dc83c7a9a6616ebbce7a", + "content-hash": "9868f802ae7cbc11e33ea21415c8798a", "packages": [ { "name": "ahand/mobileesp", @@ -1155,6 +1155,67 @@ ], "time": "2024-05-08T08:09:28+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.15.4", 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:'), ]; } } diff --git a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php index 6a3ddb08a76..5b72982acfa 100644 --- a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php +++ b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php @@ -1271,9 +1271,9 @@ public function getSystemDetails() public function getSummary() { // We need to return an array, so if we have a description, turn it into an - // array (it should be a flat string according to the default schema, but we - // might as well support the array case just to be on the safe side: - return (array)($this->fields['description'] ?? []); + // array (it is a flat string in the default Solr schema, but we also + // support multivalued fields for other backends): + return $this->getFieldAsArray('description'); } /** @@ -1816,4 +1816,21 @@ public function getCoordinateLabels() { return (array)($this->fields['long_lat_label'] ?? []); } + + /** + * Get a field as an array + * + * @param string $field Field + * + * @return array + */ + protected function getFieldAsArray(string $field): array + { + // Make sure to return only non-empty values and avoid casting since description can be a PropertyString too: + $value = $this->fields['description'] ?? ''; + if ('' === $value) { + return []; + } + return is_array($value) ? $value : [$value]; + } } diff --git a/module/VuFind/src/VuFind/String/PropertyString.php b/module/VuFind/src/VuFind/String/PropertyString.php new file mode 100644 index 00000000000..3cddb283301 --- /dev/null +++ b/module/VuFind/src/VuFind/String/PropertyString.php @@ -0,0 +1,187 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFind\String; + +use function array_key_exists; + +/** + * Class for a string with additional properties. + * + * @category VuFind + * @package String + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class PropertyString implements PropertyStringInterface +{ + /** + * Constructor + * + * @param string $string String value + * @param array $properties Associative array of any additional properties. Use a custom prefix for locally + * defined properties. Double underscore is a reserved prefix, and currently the following keys are defined: + * __ids Identifiers (e.g. subject URIs) + * __html HTML presentation + */ + public function __construct(protected string $string, protected array $properties = []) + { + } + + /** + * Set string value + * + * @param string $str String value + * + * @return static + */ + public function setString(string $str): static + { + $this->string = $str; + return $this; + } + + /** + * Get string value + * + * @return string + */ + public function getString(): string + { + return $this->string; + } + + /** + * Set HTML string + * + * @param string $html HTML + * + * @return static + */ + public function setHtml(string $html): static + { + $this['__html'] = $html; + return $this; + } + + /** + * Get HTML string + * + * Note: This could contain anything and must be sanitized for display + * + * @return ?string + */ + public function getHtml(): ?string + { + return $this['__html']; + } + + /** + * Set identifiers + * + * @param array $ids Identifiers + * + * @return static + */ + public function setIds(array $ids): static + { + $this['__ids'] = $ids; + return $this; + } + + /** + * Get identifiers + * + * @return ?array + */ + public function getIds(): ?array + { + return $this['__ids']; + } + + /** + * Check if offset exists + * + * @param mixed $offset Offset + * + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->properties); + } + + /** + * Return value of offset + * + * @param mixed $offset Offset + * + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->properties[$offset] ?? null; + } + + /** + * Set value of offset + * + * @param mixed $offset Offset + * @param mixed $value Value + * + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->properties[$offset] = $value; + } + + /** + * Unset value of offset + * + * @param mixed $offset Offset + * + * @return void + */ + public function offsetUnset(mixed $offset): void + { + unset($this->properties[$offset]); + } + + /** + * Return string value + * + * @return string + */ + public function __toString(): string + { + return $this->string; + } +} diff --git a/module/VuFind/src/VuFind/String/PropertyStringInterface.php b/module/VuFind/src/VuFind/String/PropertyStringInterface.php new file mode 100644 index 00000000000..81f6673a645 --- /dev/null +++ b/module/VuFind/src/VuFind/String/PropertyStringInterface.php @@ -0,0 +1,92 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFind\String; + +/** + * Interface for a string with additional properties. + * + * @category VuFind + * @package String + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +interface PropertyStringInterface extends \ArrayAccess, \Stringable +{ + /** + * Set string value + * + * @param string $str String value + * + * @return static + */ + public function setString(string $str): static; + + /** + * Get string value + * + * @return string + */ + public function getString(): string; + + /** + * Set HTML string + * + * @param string $html HTML + * + * @return static + */ + public function setHtml(string $html): static; + + /** + * Get HTML string + * + * Note: This could contain anything and must be sanitized for display + * + * @return ?string + */ + public function getHtml(): ?string; + + /** + * Set identifiers + * + * @param array $ids Identifiers + * + * @return static + */ + public function setIds(array $ids): static; + + /** + * Get identifiers + * + * @return ?array + */ + public function getIds(): ?array; +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/CleanHtml.php b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtml.php new file mode 100644 index 00000000000..c728fcf731e --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtml.php @@ -0,0 +1,79 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ + +namespace VuFind\View\Helper\Root; + +use Closure; + +/** + * HTML Cleaner view helper + * + * @category VuFind + * @package View_Helpers + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +class CleanHtml extends \Laminas\View\Helper\AbstractHelper +{ + /** + * Purifier + * + * @var \HTMLPurifier + */ + protected $purifier = null; + + /** + * Constructor + * + * @param Closure $purifierFactory Purifier factory callback + */ + public function __construct(protected Closure $purifierFactory) + { + } + + /** + * Clean up HTML + * + * @param string $html HTML + * @param boolean $targetBlank Whether to add target=_blank to outgoing links + * + * @return string + */ + public function __invoke($html, $targetBlank = false): string + { + if (!str_contains($html, '<')) { + return $html; + } + if (null === ($this->purifier[$targetBlank] ?? null)) { + $this->purifier[$targetBlank] = ($this->purifierFactory)(compact('targetBlank')); + } + return $this->purifier[$targetBlank]->purify($html); + } +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php new file mode 100644 index 00000000000..561a5f0f91c --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php @@ -0,0 +1,147 @@ + + * @author Aleksi Peebles + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace VuFind\View\Helper\Root; + +use Closure; +use HTMLPurifier; +use HTMLPurifier_Config; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; + +/** + * CleanHtml helper factory. + * + * @category VuFind + * @package View_Helpers + * @author Ere Maijala + * @author Aleksi Peebles + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class CleanHtmlFactory implements FactoryInterface +{ + /** + * Service manager + * + * @var ContainerInterface + */ + protected ContainerInterface $container; + + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + + $this->container = $container; + return new $requestedName(Closure::fromCallable([$this, 'createPurifier'])); + } + + /** + * Create a purifier instance. + * + * N.B. This is a relatively slow method. + * + * @param array $options Additional options. Currently supported: + * targetBlank true/false Whether to add target="_blank" to external links + * + * @return HTMLPurifier + */ + protected function createPurifier(array $options): HTMLPurifier + { + $config = \HTMLPurifier_Config::createDefault(); + // Set cache path to the object cache + $cacheDir + = $this->container->get(\VuFind\Cache\Manager::class)->getCache('object')->getOptions()->getCacheDir(); + if ($cacheDir) { + $config->set('Cache.SerializerPath', $cacheDir); + } + if ($options['targetBlank'] ?? false) { + $config->set('HTML.Nofollow', 1); + $config->set('HTML.TargetBlank', 1); + } + + // Setting the following option makes purifier’s DOMLex pass the + // LIBXML_PARSEHUGE option to DOMDocument::loadHtml method. This in turn + // ensures that PHP calls htmlCtxtUseOptions (see + // github.com/php/php-src/blob/PHP-8.1.14/ext/dom/document.c#L1870), + // which ensures that the libxml2 options (namely keepBlanks) are set up + // properly, and whitespace nodes are preserved. This should not be an + // issue from libxml2 version 2.9.5, but during testing the issue was + // still intermittently present. Regardless of that, CentOS 7.x have an + // older libxml2 that exhibits the issue. + $config->set('Core.AllowParseManyTags', true); + + $this->setAdditionalConfiguration($config); + return new \HTMLPurifier($config); + } + + /** + * Sets additional configuration + * + * @param HTMLPurifier_Config $config Configuration + * + * @return void + */ + protected function setAdditionalConfiguration(HTMLPurifier_Config $config) + { + // Add support for deprecated tags: + $definition = $config->getHTMLDefinition(true); + $definition->addElement( + 'details', + 'Block', + 'Flow', + 'Common', + ['open' => new \HTMLPurifier_AttrDef_HTML_Bool(true)] + ); + $definition->addElement('summary', 'Block', 'Flow', 'Common'); + } +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/RecordDataFormatter.php b/module/VuFind/src/VuFind/View/Helper/Root/RecordDataFormatter.php index 37405783a3a..9889e90a15c 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/RecordDataFormatter.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/RecordDataFormatter.php @@ -33,6 +33,7 @@ use Laminas\View\Helper\AbstractHelper; use VuFind\RecordDriver\AbstractBase as RecordDriver; +use VuFind\String\PropertyStringInterface; use function call_user_func; use function count; @@ -125,6 +126,9 @@ protected function sortCallback($a, $b) */ protected function allowValue($value, $options, $ignoreCombineAlt = false) { + if ($value instanceof PropertyStringInterface) { + $value = (string)$value; + } if (!empty($value) || ($ignoreCombineAlt && ($options['renderType'] ?? 'Simple') == 'CombineAlt')) { return true; } diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml index bd4333cb4e5..206db205499 100644 --- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -1,10 +1,16 @@ driver->getSummary() as $summary): ?> - escapeHtml($summary) ?>
+ escapeHtmlExt($summary, allowHtml: true) ?>
driver->getCleanISBN(); ?> summaries($isbn) as $provider => $content): ?> - - '); ?> - escapeHtml($summary) . '
')?> - + ', + array_map( + function ($summary) { + $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); + return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); + }, + $content + ) +)?> diff --git a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml index bd4333cb4e5..206db205499 100644 --- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -1,10 +1,16 @@ driver->getSummary() as $summary): ?> - escapeHtml($summary) ?>
+ escapeHtmlExt($summary, allowHtml: true) ?>
driver->getCleanISBN(); ?> summaries($isbn) as $provider => $content): ?> - - '); ?> - escapeHtml($summary) . '
')?> - + ', + array_map( + function ($summary) { + $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); + return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); + }, + $content + ) +)?> diff --git a/themes/root/theme.config.php b/themes/root/theme.config.php index 1858b5fbe5c..0e32664efbe 100644 --- a/themes/root/theme.config.php +++ b/themes/root/theme.config.php @@ -17,6 +17,7 @@ 'VuFind\View\Helper\Root\Captcha' => 'VuFind\View\Helper\Root\CaptchaFactory', 'VuFind\View\Helper\Root\Cart' => 'VuFind\View\Helper\Root\CartFactory', 'VuFind\View\Helper\Root\Citation' => 'VuFind\View\Helper\Root\CitationFactory', + 'VuFind\View\Helper\Root\CleanHtml' => 'VuFind\View\Helper\Root\CleanHtmlFactory', 'VuFind\View\Helper\Root\Component' => 'Laminas\ServiceManager\Factory\InvokableFactory', 'VuFind\View\Helper\Root\Config' => 'VuFind\View\Helper\Root\ConfigFactory', 'VuFind\View\Helper\Root\Content' => 'VuFind\View\Helper\Root\ContentFactory', @@ -30,6 +31,7 @@ 'VuFind\View\Helper\Root\DateTime' => 'VuFind\View\Helper\Root\DateTimeFactory', 'VuFind\View\Helper\Root\DisplayLanguageOption' => 'VuFind\View\Helper\Root\DisplayLanguageOptionFactory', 'VuFind\View\Helper\Root\Doi' => 'VuFind\View\Helper\Root\DoiFactory', + 'VuFind\View\Helper\Root\EscapeHtmlExt' => 'VuFind\View\Helper\Root\EscapeHtmlExtFactory', 'VuFind\View\Helper\Root\ExplainElement' => 'Laminas\ServiceManager\Factory\InvokableFactory', 'VuFind\View\Helper\Root\Export' => 'VuFind\View\Helper\Root\ExportFactory', 'VuFind\View\Helper\Root\Feedback' => 'VuFind\View\Helper\Root\FeedbackFactory', @@ -115,6 +117,7 @@ 'captcha' => 'VuFind\View\Helper\Root\Captcha', 'cart' => 'VuFind\View\Helper\Root\Cart', 'citation' => 'VuFind\View\Helper\Root\Citation', + 'cleanHtml' => 'VuFind\View\Helper\Root\CleanHtml', 'component' => 'VuFind\View\Helper\Root\Component', 'config' => 'VuFind\View\Helper\Root\Config', 'content' => 'VuFind\View\Helper\Root\Content', @@ -128,6 +131,7 @@ 'dateTime' => 'VuFind\View\Helper\Root\DateTime', 'displayLanguageOption' => 'VuFind\View\Helper\Root\DisplayLanguageOption', 'doi' => 'VuFind\View\Helper\Root\Doi', + 'escapeHtmlExt' => 'VuFind\View\Helper\Root\EscapeHtmlExt', 'explainElement' => 'VuFind\View\Helper\Root\ExplainElement', 'export' => 'VuFind\View\Helper\Root\Export', 'feedback' => 'VuFind\View\Helper\Root\Feedback', @@ -196,6 +200,7 @@ 'truncate' => 'VuFind\View\Helper\Root\Truncate', 'userlist' => 'VuFind\View\Helper\Root\UserList', 'usertags' => 'VuFind\View\Helper\Root\UserTags', + 'Laminas\View\Helper\Url' => 'VuFind\View\Helper\Root\Url', ], ], From be7162995977285dcb6ec40ade6ba0107609306b Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Fri, 11 Oct 2024 15:30:57 +0300 Subject: [PATCH 2/8] Add EscapeHtmlExt. --- .../VuFind/View/Helper/Root/EscapeHtmlExt.php | 92 +++++++++++++++++++ .../View/Helper/Root/EscapeHtmlExtFactory.php | 76 +++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php create mode 100644 module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExtFactory.php diff --git a/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php new file mode 100644 index 00000000000..01fbb39d528 --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php @@ -0,0 +1,92 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace VuFind\View\Helper\Root; + +use Laminas\Escaper\Escaper; +use VuFind\String\PropertyStringInterface; + +/** + * Extended Escape HTML view helper + * + * @category VuFind + * @package View_Helpers + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EscapeHtmlExt extends \Laminas\View\Helper\Escaper\AbstractHelper +{ + /** + * Constructor + * + * @param Escaper $escaper Escaper + * @param CleanHtml $cleanHtml Clean HTML helper + */ + public function __construct(Escaper $escaper, protected CleanHtml $cleanHtml) + { + $this->escaper = $escaper; + } + + /** + * Invoke this helper: escape a value + * + * @param mixed $value Value to escape + * @param int $recurse Expects one of the recursion constants; + * used to decide whether or not to recurse the given value when escaping + * @param bool $allowHtml Whether to allow sanitized HTML if passed a PropertyString + * + * @return mixed Given a scalar, a scalar value is returned. Given an object, with the $recurse flag not + * allowing object recursion, returns a string. Otherwise, returns an array. + * + * @throws Exception\InvalidArgumentException + */ + public function __invoke($value, $recurse = self::RECURSE_NONE, bool $allowHtml = false) + { + if ($value instanceof PropertyStringInterface) { + if ($allowHtml && $html = $value->getHtml()) { + return ($this->cleanHtml)($html); + } + $value = (string)$value; + } + return $this->escape($value); + } + + /** + * Escape a string + * + * @param string $value String to escape + * + * @return string + */ + protected function escape($value) + { + return $this->escaper->escapeHtml($value); + } +} diff --git a/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExtFactory.php b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExtFactory.php new file mode 100644 index 00000000000..aecb8c94634 --- /dev/null +++ b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExtFactory.php @@ -0,0 +1,76 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ + +namespace VuFind\View\Helper\Root; + +use Laminas\Escaper\Escaper; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; + +/** + * Extended Escape HTML helper factory. + * + * @category VuFind + * @package View_Helpers + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EscapeHtmlExtFactory implements FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException&\Throwable if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + + $helpers = $container->get('ViewHelperManager'); + return new $requestedName(new Escaper(), $helpers->get('cleanHtml')); + } +} From d5c0ca5cd4c236d69f460bf20ae6a5ee9cfc120e Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Fri, 11 Oct 2024 20:20:50 +0300 Subject: [PATCH 3/8] Fixes. --- module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php | 4 +++- module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php index 5b72982acfa..de6e7f77e12 100644 --- a/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php +++ b/module/VuFind/src/VuFind/RecordDriver/DefaultRecord.php @@ -1826,11 +1826,13 @@ public function getCoordinateLabels() */ protected function getFieldAsArray(string $field): array { - // Make sure to return only non-empty values and avoid casting since description can be a PropertyString too: + // Make sure to return only non-empty values: $value = $this->fields['description'] ?? ''; if ('' === $value) { return []; } + // Avoid casting since description can be a PropertyString too (and casting would return an array of object + // properties): return is_array($value) ? $value : [$value]; } } diff --git a/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php index 01fbb39d528..7720ba37db6 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php @@ -51,7 +51,7 @@ class EscapeHtmlExt extends \Laminas\View\Helper\Escaper\AbstractHelper */ public function __construct(Escaper $escaper, protected CleanHtml $cleanHtml) { - $this->escaper = $escaper; + parent::__construct($escaper); } /** From c1ff4e9be466972ea83067f37712b9f86e32fb77 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 15 Oct 2024 11:07:44 +0300 Subject: [PATCH 4/8] Add a static factory method. --- module/VuFind/src/VuFind/String/PropertyString.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/module/VuFind/src/VuFind/String/PropertyString.php b/module/VuFind/src/VuFind/String/PropertyString.php index 3cddb283301..0497072500e 100644 --- a/module/VuFind/src/VuFind/String/PropertyString.php +++ b/module/VuFind/src/VuFind/String/PropertyString.php @@ -55,6 +55,19 @@ public function __construct(protected string $string, protected array $propertie { } + /** + * Create a PropertyString from an HTML string + * + * @param string $html HTML + * @param array $properties Any additional properties (see __construct) + * + * @return static + */ + public static function fromHtml(string $html, array $properties = []): static + { + return (new static(strip_tags($html), $properties))->setHtml($html); + } + /** * Set string value * From da9165f30ea20560e24c5f0910ce6fca026bb3c9 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 15 Oct 2024 11:13:17 +0300 Subject: [PATCH 5/8] Don't try to be too clever. --- module/VuFind/src/VuFind/String/PropertyString.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/VuFind/src/VuFind/String/PropertyString.php b/module/VuFind/src/VuFind/String/PropertyString.php index 0497072500e..c2da2519bb2 100644 --- a/module/VuFind/src/VuFind/String/PropertyString.php +++ b/module/VuFind/src/VuFind/String/PropertyString.php @@ -61,11 +61,11 @@ public function __construct(protected string $string, protected array $propertie * @param string $html HTML * @param array $properties Any additional properties (see __construct) * - * @return static + * @return PropertyString */ - public static function fromHtml(string $html, array $properties = []): static + public static function fromHtml(string $html, array $properties = []): PropertyString { - return (new static(strip_tags($html), $properties))->setHtml($html); + return (new PropertyString(strip_tags($html), $properties))->setHtml($html); } /** From 61ef47ee5654da32ffaabf5676d875e00529c5fe Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 15 Oct 2024 11:13:26 +0300 Subject: [PATCH 6/8] Fix comment. --- module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php index 561a5f0f91c..7f5ee309c46 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/CleanHtmlFactory.php @@ -133,7 +133,7 @@ protected function createPurifier(array $options): HTMLPurifier */ protected function setAdditionalConfiguration(HTMLPurifier_Config $config) { - // Add support for deprecated tags: + // Add support for details and summary elements: $definition = $config->getHTMLDefinition(true); $definition->addElement( 'details', From 9bcfe47c62ba4bdb4ca788808254dd5716979dca Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 15 Oct 2024 12:03:49 +0300 Subject: [PATCH 7/8] Fix indentation. Co-authored-by: Demian Katz --- .../templates/RecordDriver/DefaultRecord/data-summary.phtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml index 206db205499..92ea3ddf7e9 100644 --- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -7,8 +7,8 @@ '
', array_map( function ($summary) { - $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); - return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); + $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); + return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); }, $content ) From 1461a008cfda9698694ab76eb5c819fddcf88f70 Mon Sep 17 00:00:00 2001 From: Ere Maijala Date: Tue, 15 Oct 2024 12:11:45 +0300 Subject: [PATCH 8/8] Fix indentation more. --- .../DefaultRecord/data-summary.phtml | 20 ++++++++++--------- .../DefaultRecord/data-summary.phtml | 20 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml index 206db205499..a5d8e73fb4e 100644 --- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -3,14 +3,16 @@ driver->getCleanISBN(); ?> summaries($isbn) as $provider => $content): ?> - ', - array_map( - function ($summary) { - $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); - return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); - }, - $content + ', + array_map( + function ($summary) { + $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); + return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); + }, + $content + ) ) -)?> + ?> diff --git a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml index 92ea3ddf7e9..a5d8e73fb4e 100644 --- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml +++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml @@ -3,14 +3,16 @@ driver->getCleanISBN(); ?> summaries($isbn) as $provider => $content): ?> - ', - array_map( - function ($summary) { - $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); - return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); - }, - $content + ', + array_map( + function ($summary) { + $htmlContent = str_starts_with($summary, '<') && str_ends_with($summary, '>'); + return $htmlContent ? $summary : ($this->escapeHtmlExt($summary, allowHtml: true)); + }, + $content + ) ) -)?> + ?>