diff --git a/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php b/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php index 24edbcbdd36c403c165775e8d95a6ed0f9a74ab4..3476e580e4996b6c144ffcb6005dbcaed225fb13 100644 --- a/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php +++ b/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php @@ -16,6 +16,8 @@ namespace TYPO3\CMS\Fluid\Core\Widget; use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; use TYPO3\CMS\Fluid\Core\Widget\Exception\WidgetContextNotFoundException; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; @@ -41,11 +43,17 @@ class AjaxWidgetContextHolder implements SingletonInterface */ protected $widgetContextsStorageKey = 'TYPO3\\CMS\\Fluid\\Core\\Widget\\AjaxWidgetContextHolder_widgetContexts'; + /** + * @var HashService + */ + protected $hashService; + /** * Constructor */ public function __construct() { + $this->hashService = GeneralUtility::makeInstance(HashService::class); $this->loadWidgetContexts(); } @@ -55,13 +63,9 @@ class AjaxWidgetContextHolder implements SingletonInterface protected function loadWidgetContexts() { if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) { - $this->widgetContexts = $this->buildWidgetContextsFromArray( - json_decode($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey ?? null), true) ?? [] - ); + $this->widgetContexts = $this->unserializeWithHmac($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey)); } else { - $this->widgetContexts = $this->buildWidgetContextsFromArray( - json_decode($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] ?? '', true) ?? [] - ); + $this->widgetContexts = isset($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) ? $this->unserializeWithHmac($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) : []; $GLOBALS['BE_USER']->writeUC(); } } @@ -100,27 +104,33 @@ class AjaxWidgetContextHolder implements SingletonInterface protected function storeWidgetContexts() { if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) { - $GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, json_encode($this->widgetContexts)); + $GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, $this->serializeWithHmac($this->widgetContexts)); $GLOBALS['TSFE']->fe_user->storeSessionData(); } else { - $GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = json_encode($this->widgetContexts); + $GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = $this->serializeWithHmac($this->widgetContexts); $GLOBALS['BE_USER']->writeUC(); } } - /** - * Builds WidgetContext instances from JSON representation, - * this is basically required for AJAX widgets only. - * - * @param array $data - * @return WidgetContext[] - */ - protected function buildWidgetContextsFromArray(array $data): array + protected function serializeWithHmac(array $widgetContexts): string { - $widgetContexts = []; - foreach ($data as $widgetId => $widgetContextData) { - $widgetContexts[$widgetId] = WidgetContext::fromArray($widgetContextData); + $data = serialize($widgetContexts); + return $this->hashService->appendHmac($data); + } + + protected function unserializeWithHmac(?string $data): array + { + if ($data === null || $data === '') { + return []; + } + try { + $data = $this->hashService->validateAndStripHmac($data); + // widget contexts literally can contain everything, that why using + // HMAC-signed `unserialize()` is the only option here unless Extbase + // structures have been refactored further + $widgetContexts = unserialize($data); + } finally { + return is_array($widgetContexts ?? null) ? $widgetContexts : []; } - return $widgetContexts; } } diff --git a/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php b/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php index 5f2035182ade580ef807b7b9665db5f70dfdb836..0049f86a4a5818532f4f7e9dc7a3db3f1efc6c7b 100644 --- a/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php +++ b/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php @@ -29,19 +29,8 @@ use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; * * @internal It is a purely internal class which should not be used outside of Fluid. */ -class WidgetContext implements \JsonSerializable +class WidgetContext { - protected const JSON_SERIALIZABLE_PROPERTIES = [ - 'widgetIdentifier', - 'ajaxWidgetIdentifier', - 'widgetConfiguration', - 'controllerObjectName', - 'parentPluginNamespace', - 'parentExtensionName', - 'parentPluginName', - 'widgetViewHelperClassName', - ]; - /** * Uniquely identifies a Widget Instance on a certain page. * @@ -58,8 +47,7 @@ class WidgetContext implements \JsonSerializable /** * User-supplied widget configuration, available inside the widget - * controller as $this->widgetConfiguration. Array items must be - * null, scalar, array or implement \JsonSerializable interface. + * controller as $this->widgetConfiguration. * * @var array */ @@ -109,51 +97,6 @@ class WidgetContext implements \JsonSerializable */ protected $widgetViewHelperClassName; - public static function fromArray(array $data): WidgetContext - { - $target = new WidgetContext(); - foreach ($data as $propertyName => $propertyValue) { - if (!in_array($propertyName, self::JSON_SERIALIZABLE_PROPERTIES, true)) { - continue; - } - if (!property_exists($target, $propertyName)) { - continue; - } - $target->{$propertyName} = $propertyValue; - } - return $target; - } - - public function jsonSerialize() - { - $properties = array_fill_keys(self::JSON_SERIALIZABLE_PROPERTIES, true); - $data = array_intersect_key(get_object_vars($this), $properties); - - if (is_array($data['widgetConfiguration'] ?? null)) { - $data['widgetConfiguration'] = array_filter( - $data['widgetConfiguration'], - function ($value): bool { - if ($value === null || is_scalar($value) || is_array($value) - || is_object($value) && $value instanceof \JsonSerializable - ) { - return true; - } - throw new Exception( - sprintf( - 'Widget configuration items either must be null, scalar, array or JSON serializable, got "%s"', - is_object($value) ? get_class($value) : gettype($value) - ), - 1588178538 - ); - } - ); - } else { - $data['widgetConfiguration'] = null; - } - - return $data; - } - /** * @return string */ @@ -323,4 +266,12 @@ class WidgetContext implements \JsonSerializable { return $this->viewHelperChildNodeRenderingContext; } + + /** + * @return array + */ + public function __sleep() + { + return ['widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName', 'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName']; + } } diff --git a/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php b/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php index 14035d27ede4f19e83938152961b488d31648dd6..1adc67137ad61ed888ac53cc1a3073f49981d1ee 100644 --- a/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php +++ b/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php @@ -129,14 +129,14 @@ class WidgetContextTest extends UnitTestCase /** * @test */ - public function jsonEncodeContainsExpectedPropertyNames() + public function sleepReturnsExpectedPropertyNames() { self::assertEquals( [ 'widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName', 'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName' ], - array_keys(json_decode(json_encode($this->widgetContext), true)) + $this->widgetContext->__sleep() ); } }