diff --git a/composer.json b/composer.json
index c12ad3a9936..2138ebbeebf 100644
--- a/composer.json
+++ b/composer.json
@@ -34,6 +34,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",
"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 fa631f84947..1d10b690137 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": "566e3f40ebe7e97a8b64fc55d6a3eff0",
+ "content-hash": "2c6aabddbb175854df8716c3a50d19d8",
"packages": [
{
"name": "ahand/mobileesp",
@@ -1215,6 +1215,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/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..de6e7f77e12 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,23 @@ 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:
+ $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/String/PropertyString.php b/module/VuFind/src/VuFind/String/PropertyString.php
new file mode 100644
index 00000000000..c2da2519bb2
--- /dev/null
+++ b/module/VuFind/src/VuFind/String/PropertyString.php
@@ -0,0 +1,200 @@
+
+ * @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 = [])
+ {
+ }
+
+ /**
+ * Create a PropertyString from an HTML string
+ *
+ * @param string $html HTML
+ * @param array $properties Any additional properties (see __construct)
+ *
+ * @return PropertyString
+ */
+ public static function fromHtml(string $html, array $properties = []): PropertyString
+ {
+ return (new PropertyString(strip_tags($html), $properties))->setHtml($html);
+ }
+
+ /**
+ * 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..7f5ee309c46
--- /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 details and summary elements:
+ $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/EscapeHtmlExt.php b/module/VuFind/src/VuFind/View/Helper/Root/EscapeHtmlExt.php
new file mode 100644
index 00000000000..7720ba37db6
--- /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)
+ {
+ parent::__construct($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'));
+ }
+}
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..a5d8e73fb4e 100644
--- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/data-summary.phtml
@@ -1,10 +1,18 @@
driver->getSummary() as $summary): ?>
- =$this->escapeHtml($summary) ?>
+ =$this->escapeHtmlExt($summary, allowHtml: true) ?>
driver->getCleanISBN(); ?>
summaries($isbn) as $provider => $content): ?>
-
- '); ?>
- =$htmlContent ? $summary : ($this->escapeHtml($summary) . '
')?>
-
+ =
+ implode(
+ '
',
+ 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..a5d8e73fb4e 100644
--- a/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml
+++ b/themes/bootstrap5/templates/RecordDriver/DefaultRecord/data-summary.phtml
@@ -1,10 +1,18 @@
driver->getSummary() as $summary): ?>
- =$this->escapeHtml($summary) ?>
+ =$this->escapeHtmlExt($summary, allowHtml: true) ?>
driver->getCleanISBN(); ?>
summaries($isbn) as $provider => $content): ?>
-
- '); ?>
- =$htmlContent ? $summary : ($this->escapeHtml($summary) . '
')?>
-
+ =
+ implode(
+ '
',
+ 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 1850e710c53..38e159d00fe 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',
@@ -120,6 +122,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',
@@ -133,6 +136,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',