diff --git a/ts/WoltLabSuite/Core/Component/GridView/Setup.ts b/ts/WoltLabSuite/Core/Component/GridView/Setup.ts index 23bf8e1a759..1e866757153 100644 --- a/ts/WoltLabSuite/Core/Component/GridView/Setup.ts +++ b/ts/WoltLabSuite/Core/Component/GridView/Setup.ts @@ -20,7 +20,7 @@ export class GridViewSetup { sortField: string = "", sortOrder: string = "ASC", filters?: Map, - gridViewParameters?: Map, + gridViewParameters?: Map, ): Promise { const url = new URL(`${window.WSC_RPC_API_URL}core/grid-views/render`); url.searchParams.set("gridView", gridViewClass); @@ -34,7 +34,13 @@ export class GridViewSetup { } if (gridViewParameters) { gridViewParameters.forEach((value, key) => { - url.searchParams.set(`gridViewParameters[${key}]`, value); + if (Array.isArray(value)) { + value.forEach((innerValue, innerKey) => { + url.searchParams.set(`gridViewParameters[${key}][${innerKey}]`, innerValue); + }); + } else { + url.searchParams.set(`gridViewParameters[${key}]`, value); + } }); } const json = (await prepareRequest(url).get().fetchAsJson()) as ResponseGridView; diff --git a/ts/WoltLabSuite/Core/Component/ListView/Setup.ts b/ts/WoltLabSuite/Core/Component/ListView/Setup.ts index bb63db9bfc8..cbd35169d83 100644 --- a/ts/WoltLabSuite/Core/Component/ListView/Setup.ts +++ b/ts/WoltLabSuite/Core/Component/ListView/Setup.ts @@ -17,7 +17,7 @@ export class ListViewSetup { async fromPreset( title: string, listViewClass: string, - listViewParameters?: Map, + listViewParameters?: Map, filters?: Map, sortField: string = "", sortOrder: string = "ASC", @@ -36,7 +36,13 @@ export class ListViewSetup { } if (listViewParameters) { listViewParameters.forEach((value, key) => { - url.searchParams.set(`listViewParameters[${key}]`, value); + if (Array.isArray(value)) { + value.forEach((innerValue, innerKey) => { + url.searchParams.set(`listViewParameters[${key}][${innerKey}]`, innerValue); + }); + } else { + url.searchParams.set(`listViewParameters[${key}]`, value); + } }); } const json = (await prepareRequest(url).get().fetchAsJson()) as ResponseListView; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Setup.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Setup.js index a9b37463564..ca6db4ab4be 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Setup.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GridView/Setup.js @@ -55,7 +55,14 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi } if (gridViewParameters) { gridViewParameters.forEach((value, key) => { - url.searchParams.set(`gridViewParameters[${key}]`, value); + if (Array.isArray(value)) { + value.forEach((innerValue, innerKey) => { + url.searchParams.set(`gridViewParameters[${key}][${innerKey}]`, innerValue); + }); + } + else { + url.searchParams.set(`gridViewParameters[${key}]`, value); + } }); } const json = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson()); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js index 9f40914855e..6513fbf704f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js @@ -55,7 +55,14 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi } if (listViewParameters) { listViewParameters.forEach((value, key) => { - url.searchParams.set(`listViewParameters[${key}]`, value); + if (Array.isArray(value)) { + value.forEach((innerValue, innerKey) => { + url.searchParams.set(`listViewParameters[${key}][${innerKey}]`, innerValue); + }); + } + else { + url.searchParams.set(`listViewParameters[${key}]`, value); + } }); } const json = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson()); diff --git a/wcfsetup/install/files/lib/action/GridViewColumnConfigurationAction.class.php b/wcfsetup/install/files/lib/action/GridViewColumnConfigurationAction.class.php index dfbb03c0d4e..a7640787da8 100644 --- a/wcfsetup/install/files/lib/action/GridViewColumnConfigurationAction.class.php +++ b/wcfsetup/install/files/lib/action/GridViewColumnConfigurationAction.class.php @@ -30,6 +30,13 @@ final class GridViewColumnConfigurationAction implements RequestHandlerInterface #[\Override] public function handle(ServerRequestInterface $request): ResponseInterface { + /** + * @var array{ + * gridView:string, + * columns: list, + * gridViewParameters: array + * } $parameters + */ $parameters = Helper::mapQueryParameters( $request->getQueryParams(), <<<'EOT' @@ -41,20 +48,18 @@ public function handle(ServerRequestInterface $request): ResponseInterface EOT ); - if (!\is_subclass_of($parameters['gridView'], AbstractGridView::class)) { + if ( + !\is_subclass_of($parameters['gridView'], AbstractGridView::class) + || !(new \ReflectionClass($parameters['gridView']))->isInstantiable() + ) { throw new UserInputException('gridView', 'invalid'); } - try { - /** @var AbstractGridView> $view */ - $view = new $parameters['gridView'](...$parameters['gridViewParameters']); - } catch (\ArgumentCountError | \TypeError $e) { - if (\ENABLE_DEBUG_MODE !== 0) { - throw $e; - } else { - throw new IllegalLinkException(); - } - } + /** @var AbstractGridView> $view */ + $view = Helper::mapQueryParametersToConstructor( + $parameters['gridViewParameters'], + $parameters['gridView'] + ); if (!$view->isAccessible()) { throw new PermissionDeniedException(); diff --git a/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php b/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php index 45114e8475f..142bba32a9d 100644 --- a/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php +++ b/wcfsetup/install/files/lib/action/GridViewFilterAction.class.php @@ -30,6 +30,13 @@ final class GridViewFilterAction implements RequestHandlerInterface #[\Override] public function handle(ServerRequestInterface $request): ResponseInterface { + /** + * @var array{ + * gridView:string, + * filters: string[], + * gridViewParameters: array + * } $parameters + */ $parameters = Helper::mapQueryParameters( $request->getQueryParams(), <<<'EOT' @@ -41,20 +48,18 @@ public function handle(ServerRequestInterface $request): ResponseInterface EOT ); - if (!\is_subclass_of($parameters['gridView'], AbstractGridView::class)) { + if ( + !\is_subclass_of($parameters['gridView'], AbstractGridView::class) + || !(new \ReflectionClass($parameters['gridView']))->isInstantiable() + ) { throw new UserInputException('gridView', 'invalid'); } - try { - /** @var AbstractGridView> $view */ - $view = new $parameters['gridView'](...$parameters['gridViewParameters']); - } catch (\ArgumentCountError | \TypeError $e) { - if (\ENABLE_DEBUG_MODE !== 0) { - throw $e; - } else { - throw new IllegalLinkException(); - } - } + /** @var AbstractGridView> $view */ + $view = Helper::mapQueryParametersToConstructor( + $parameters['gridViewParameters'], + $parameters['gridView'] + ); if (!$view->isAccessible()) { throw new PermissionDeniedException(); diff --git a/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php b/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php index b4e96cfaa79..6d890548b81 100644 --- a/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php +++ b/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php @@ -30,6 +30,13 @@ final class ListViewFilterAction implements RequestHandlerInterface #[\Override] public function handle(ServerRequestInterface $request): ResponseInterface { + /** + * @var array{ + * listView: string, + * filters: string[], + * listViewParameters: array + * } $parameters + */ $parameters = Helper::mapQueryParameters( $request->getQueryParams(), <<<'EOT' @@ -41,20 +48,18 @@ public function handle(ServerRequestInterface $request): ResponseInterface EOT ); - if (!\is_subclass_of($parameters['listView'], AbstractListView::class)) { + if ( + !\is_subclass_of($parameters['listView'], AbstractListView::class) + || !(new \ReflectionClass($parameters['listView']))->isInstantiable() + ) { throw new UserInputException('listView', 'invalid'); } - try { - /** @var AbstractListView> $view */ - $view = new $parameters['listView'](...$parameters['listViewParameters']); - } catch (\ArgumentCountError | \TypeError $e) { - if (\ENABLE_DEBUG_MODE !== 0) { - throw $e; - } else { - throw new IllegalLinkException(); - } - } + /** @var AbstractListView> $view */ + $view = Helper::mapQueryParametersToConstructor( + $parameters['listViewParameters'], + $parameters['listView'] + ); if (!$view->isAccessible()) { throw new PermissionDeniedException(); diff --git a/wcfsetup/install/files/lib/http/Helper.class.php b/wcfsetup/install/files/lib/http/Helper.class.php index 42b920bbf8c..fb2174f9ca9 100644 --- a/wcfsetup/install/files/lib/http/Helper.class.php +++ b/wcfsetup/install/files/lib/http/Helper.class.php @@ -187,6 +187,79 @@ public static function mapApiParameters(ServerRequestInterface $request, string }; } + /** + * Creates an instance of the provided class by validating the query + * parameters against the signature of its constructor. Unknown keys + * are skipped and values are gracefully converted into the requested + * types. Parameters without a specific type, such as a plain `array`, + * are passed through as-is. + * + * Missing parameters or values that cannot be casted to the requested + * type will yield a `MappingError`. The class must be instantiable and + * its constructor may only accept builtin types, objects cannot be + * safely created from user input. + * + * @template T of object + * @param mixed[] $queryParameters + * @param class-string $className + * @return T + * @throws MappingError + * @throws \LogicException + * @since 6.3 + */ + public static function mapQueryParametersToConstructor(array $queryParameters, string $className): object + { + $reflectionClass = new \ReflectionClass($className); + $constructor = $reflectionClass->getConstructor(); + if ($constructor === null) { + return $reflectionClass->newInstance(); + } + + foreach ($constructor->getParameters() as $parameter) { + if (!self::isBuiltinType($parameter->getType())) { + throw new \LogicException(\sprintf( + "The parameter '%s' of '%s::__construct()' must be of a builtin type.", + $parameter->getName(), + $className + )); + } + } + + // Only the constructor arguments are mapped, mapping the whole object + // would also attempt to populate all other properties of the class. + $object = $reflectionClass->newInstanceWithoutConstructor(); + $constructorClosure = $constructor->getClosure($object); + + $arguments = (new MapperBuilder()) + ->allowSuperfluousKeys() + ->allowScalarValueCasting() + ->allowUndefinedValues() + ->allowPermissiveTypes() + ->argumentsMapper() + ->mapArguments($constructorClosure, Source::array($queryParameters)); + + $constructorClosure(...$arguments); + + return $object; + } + + /** + * Returns true if the type and, for union types, all of its members + * are builtin types. Missing types are treated as `mixed`. + */ + private static function isBuiltinType(?\ReflectionType $type): bool + { + return match (true) { + $type === null => true, + $type instanceof \ReflectionNamedType => $type->isBuiltin(), + $type instanceof \ReflectionUnionType => \array_all( + $type->getTypes(), + static fn(\ReflectionType $type) => self::isBuiltinType($type) + ), + default => false, + }; + } + /** * Fetch a database object using a parameter value in the URL. * diff --git a/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php b/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php index 464ae8040e7..36dfb7b4982 100644 --- a/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php +++ b/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php @@ -101,6 +101,9 @@ public function readParameters() throw new IllegalLinkException(); } + // Restrict the tag ids to existing tags and normalize them into a list. + $this->tagIDs = \array_keys($this->tags); + $this->availableObjectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject'); foreach ($this->availableObjectTypes as $key => $objectType) { if (!$objectType->validateOptions() || !$objectType->validatePermissions()) { diff --git a/wcfsetup/install/files/lib/page/TaggedListViewPage.class.php b/wcfsetup/install/files/lib/page/TaggedListViewPage.class.php index b2a0ebe563f..44f102ca919 100644 --- a/wcfsetup/install/files/lib/page/TaggedListViewPage.class.php +++ b/wcfsetup/install/files/lib/page/TaggedListViewPage.class.php @@ -92,6 +92,9 @@ public function readParameters() throw new IllegalLinkException(); } + // Restrict the tag ids to existing tags and normalize them into a list. + $this->tagIDs = \array_keys($this->tags); + $this->loadObjectTypes(); if ($this->availableObjectTypes === []) { throw new IllegalLinkException(); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetGridView.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetGridView.class.php index f8a87aa455e..0e0add9d8a9 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetGridView.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetGridView.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetGridViewParameters::class); - if (!\is_subclass_of($parameters->gridView, AbstractGridView::class)) { + if ( + !\is_subclass_of($parameters->gridView, AbstractGridView::class) + || !(new \ReflectionClass($parameters->gridView))->isInstantiable() + ) { throw new UserInputException('gridView', 'invalid'); } - $view = new $parameters->gridView(...$parameters->gridViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->gridViewParameters, + $parameters->gridView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractGridView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRow.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRow.class.php index ffe995f562f..bdec0bdc293 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRow.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRow.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRowParameters::class); - if (!\is_subclass_of($parameters->gridView, AbstractGridView::class)) { + if ( + !\is_subclass_of($parameters->gridView, AbstractGridView::class) + || !(new \ReflectionClass($parameters->gridView))->isInstantiable() + ) { throw new UserInputException('gridView', 'invalid'); } - $view = new $parameters->gridView(...$parameters->gridViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->gridViewParameters, + $parameters->gridView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractGridView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php index 129eacc1473..63ac514077a 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/gridViews/GetRows.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRowsParameters::class); - if (!\is_subclass_of($parameters->gridView, AbstractGridView::class)) { + if ( + !\is_subclass_of($parameters->gridView, AbstractGridView::class) + || !(new \ReflectionClass($parameters->gridView))->isInstantiable() + ) { throw new UserInputException('gridView', 'invalid'); } - $view = new $parameters->gridView(...$parameters->gridViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->gridViewParameters, + $parameters->gridView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractGridView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php index 098af503864..8dff7ff38f9 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetItemParameters::class); - if (!\is_subclass_of($parameters->listView, AbstractListView::class)) { + if ( + !\is_subclass_of($parameters->listView, AbstractListView::class) + || !(new \ReflectionClass($parameters->listView))->isInstantiable() + ) { throw new UserInputException('listView', 'invalid'); } - $view = new $parameters->listView(...$parameters->listViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->listViewParameters, + $parameters->listView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractListView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php index 503dc0f134b..519804bc282 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetItemsParameters::class); - if (!\is_subclass_of($parameters->listView, AbstractListView::class)) { + if ( + !\is_subclass_of($parameters->listView, AbstractListView::class) + || !(new \ReflectionClass($parameters->listView))->isInstantiable() + ) { throw new UserInputException('listView', 'invalid'); } - $view = new $parameters->listView(...$parameters->listViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->listViewParameters, + $parameters->listView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractListView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/RenderListView.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/RenderListView.class.php index 5ffa9b9f5ee..f3d492966d5 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/RenderListView.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/RenderListView.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, RenderListViewParameters::class); - if (!\is_subclass_of($parameters->listView, AbstractListView::class)) { + if ( + !\is_subclass_of($parameters->listView, AbstractListView::class) + || !(new \ReflectionClass($parameters->listView))->isInstantiable() + ) { throw new UserInputException('listView', 'invalid'); } - $view = new $parameters->listView(...$parameters->listViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->listViewParameters, + $parameters->listView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractListView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNode.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNode.class.php index 68d2579f54a..6443f0fe6d5 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNode.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNode.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetNodeParameters::class); - if (!\is_subclass_of($parameters->nodeTreeView, AbstractNodeTreeView::class)) { + if ( + !\is_subclass_of($parameters->nodeTreeView, AbstractNodeTreeView::class) + || !(new \ReflectionClass($parameters->nodeTreeView))->isInstantiable() + ) { throw new UserInputException('nodeTreeView', 'invalid'); } - $view = new $parameters->nodeTreeView(...$parameters->nodeTreeViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->nodeTreeViewParameters, + $parameters->nodeTreeView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractNodeTreeView); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNodes.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNodes.class.php index 5944a0a5442..f0425a1ad1a 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNodes.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/nodeTreeViews/GetNodes.class.php @@ -28,11 +28,17 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetNodesParameters::class); - if (!\is_subclass_of($parameters->nodeTreeView, AbstractNodeTreeView::class)) { + if ( + !\is_subclass_of($parameters->nodeTreeView, AbstractNodeTreeView::class) + || !(new \ReflectionClass($parameters->nodeTreeView))->isInstantiable() + ) { throw new UserInputException('nodeTreeView', 'invalid'); } - $view = new $parameters->nodeTreeView(...$parameters->nodeTreeViewParameters); + $view = Helper::mapQueryParametersToConstructor( + $parameters->nodeTreeViewParameters, + $parameters->nodeTreeView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractNodeTreeView); diff --git a/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php b/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php index f30db755c83..bbec7843dca 100644 --- a/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php @@ -17,7 +17,7 @@ class TaggedArticleListView extends ArticleListView { public function __construct( - /** @var list */ + /** @var non-empty-list */ public readonly array $tagIDs, ) { parent::__construct(); @@ -52,6 +52,7 @@ public function isAccessible(): bool { return parent::isAccessible() && \MODULE_TAGGING !== 0 - && WCF::getSession()->hasPermission('user.tag.canViewTag'); + && WCF::getSession()->hasPermission('user.tag.canViewTag') + && \count($this->tagIDs) <= \SEARCH_MAX_COMBINED_TAGS; } }