| Total Complexity | 140 |
| Total Lines | 1083 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| Bugs | 0 | Features | 0 |
Complex classes like RoutingService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use RoutingService, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 41 | class RoutingService implements LoggerAwareInterface |
||
| 42 | { |
||
| 43 | use LoggerAwareTrait; |
||
| 44 | |||
| 45 | /** |
||
| 46 | * Default plugin namespace |
||
| 47 | */ |
||
| 48 | const PLUGIN_NAMESPACE = 'tx_solr'; |
||
| 49 | |||
| 50 | /** |
||
| 51 | * Settings from routing configuration |
||
| 52 | * |
||
| 53 | * @var array |
||
| 54 | */ |
||
| 55 | protected $settings = []; |
||
| 56 | |||
| 57 | /** |
||
| 58 | * List of filter that are placed as path arguments |
||
| 59 | * |
||
| 60 | * @var array |
||
| 61 | */ |
||
| 62 | protected $pathArguments = []; |
||
| 63 | |||
| 64 | /** |
||
| 65 | * Plugin/extension namespace |
||
| 66 | * |
||
| 67 | * @var string |
||
| 68 | */ |
||
| 69 | protected $pluginNamespace = 'tx_solr'; |
||
| 70 | |||
| 71 | /** |
||
| 72 | * List of TYPO3 core parameters, that we should ignore |
||
| 73 | * |
||
| 74 | * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter |
||
| 75 | * @var string[] |
||
| 76 | */ |
||
| 77 | protected $coreParameters = ['no_cache', 'cHash', 'id', 'MP', 'type']; |
||
| 78 | |||
| 79 | /** |
||
| 80 | * @var UrlFacetService |
||
| 81 | */ |
||
| 82 | protected $urlFacetPathService; |
||
| 83 | |||
| 84 | /** |
||
| 85 | * @var UrlFacetService |
||
| 86 | */ |
||
| 87 | protected $urlFacetQueryService; |
||
| 88 | |||
| 89 | /** |
||
| 90 | * RoutingService constructor. |
||
| 91 | * |
||
| 92 | * @param array $settings |
||
| 93 | * @param string $pluginNamespace |
||
| 94 | */ |
||
| 95 | public function __construct(array $settings = [], string $pluginNamespace = self::PLUGIN_NAMESPACE) |
||
| 96 | { |
||
| 97 | $this->settings = $settings; |
||
| 98 | $this->pluginNamespace = $pluginNamespace; |
||
| 99 | if (empty($this->pluginNamespace)) { |
||
| 100 | $this->pluginNamespace = self::PLUGIN_NAMESPACE; |
||
| 101 | } |
||
| 102 | $this->initUrlFacetService(); |
||
| 103 | } |
||
| 104 | |||
| 105 | /** |
||
| 106 | * Creates a clone of the current service and replace the settings inside |
||
| 107 | * |
||
| 108 | * @param array $settings |
||
| 109 | * @return RoutingService |
||
| 110 | */ |
||
| 111 | public function withSettings(array $settings): RoutingService |
||
| 112 | { |
||
| 113 | $service = clone $this; |
||
| 114 | $service->settings = $settings; |
||
| 115 | $service->initUrlFacetService(); |
||
| 116 | return $service; |
||
| 117 | } |
||
| 118 | |||
| 119 | /** |
||
| 120 | * Creates a clone of the current service and replace the settings inside |
||
| 121 | * |
||
| 122 | * @param array $pathArguments |
||
| 123 | * @return RoutingService |
||
| 124 | */ |
||
| 125 | public function withPathArguments(array $pathArguments): RoutingService |
||
| 126 | { |
||
| 127 | $service = clone $this; |
||
| 128 | $service->pathArguments = $pathArguments; |
||
| 129 | $service->initUrlFacetService(); |
||
| 130 | return $service; |
||
| 131 | } |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Load configuration from routing configuration |
||
| 135 | * |
||
| 136 | * @param array $routingConfiguration |
||
| 137 | * @return $this |
||
| 138 | */ |
||
| 139 | public function fromRoutingConfiguration(array $routingConfiguration): RoutingService |
||
| 140 | { |
||
| 141 | if (empty($routingConfiguration) || |
||
| 142 | empty($routingConfiguration['type']) || |
||
| 143 | !$this->isRouteEnhancerForSolr((string)$routingConfiguration['type'])) { |
||
| 144 | return $this; |
||
| 145 | } |
||
| 146 | |||
| 147 | if (isset($routingConfiguration['solr'])) { |
||
| 148 | $this->settings = $routingConfiguration['solr']; |
||
| 149 | $this->initUrlFacetService(); |
||
| 150 | } |
||
| 151 | |||
| 152 | if (isset($routingConfiguration['_arguments'])) { |
||
| 153 | $this->pathArguments = $routingConfiguration['_arguments']; |
||
| 154 | } |
||
| 155 | |||
| 156 | return $this; |
||
| 157 | } |
||
| 158 | |||
| 159 | /** |
||
| 160 | * Reset the routing service |
||
| 161 | * |
||
| 162 | * @return $this |
||
| 163 | */ |
||
| 164 | public function reset(): RoutingService |
||
| 165 | { |
||
| 166 | $this->settings = []; |
||
| 167 | $this->pathArguments = []; |
||
| 168 | $this->pluginNamespace = self::PLUGIN_NAMESPACE; |
||
| 169 | return $this; |
||
| 170 | } |
||
| 171 | |||
| 172 | /** |
||
| 173 | * Initialize url facet services for different types |
||
| 174 | * |
||
| 175 | * @return $this |
||
| 176 | */ |
||
| 177 | protected function initUrlFacetService(): RoutingService |
||
| 178 | { |
||
| 179 | $this->urlFacetPathService = new UrlFacetService('path', $this->settings); |
||
| 180 | $this->urlFacetQueryService = new UrlFacetService('query', $this->settings); |
||
| 181 | |||
| 182 | return $this; |
||
| 183 | } |
||
| 184 | |||
| 185 | /** |
||
| 186 | * @return UrlFacetService |
||
| 187 | */ |
||
| 188 | public function getUrlFacetPathService(): UrlFacetService |
||
| 189 | { |
||
| 190 | return $this->urlFacetPathService; |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * @return UrlFacetService |
||
| 195 | */ |
||
| 196 | public function getUrlFacetQueryService(): UrlFacetService |
||
| 197 | { |
||
| 198 | return $this->urlFacetQueryService; |
||
| 199 | } |
||
| 200 | |||
| 201 | /** |
||
| 202 | * Test if the given parameter is a Core parameter |
||
| 203 | * |
||
| 204 | * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter |
||
| 205 | * @param string $parameterName |
||
| 206 | * @return bool |
||
| 207 | */ |
||
| 208 | public function isCoreParameter(string $parameterName): bool |
||
| 209 | { |
||
| 210 | return in_array($parameterName, $this->coreParameters); |
||
| 211 | } |
||
| 212 | |||
| 213 | /** |
||
| 214 | * This returns the plugin namespace |
||
| 215 | * @see https://docs.typo3.org/p/apache-solr-for-typo3/solr/master/en-us/Configuration/Reference/TxSolrView.html#pluginnamespace |
||
| 216 | * |
||
| 217 | * @return string |
||
| 218 | */ |
||
| 219 | public function getPluginNamespace(): string |
||
| 220 | { |
||
| 221 | return $this->pluginNamespace; |
||
| 222 | } |
||
| 223 | |||
| 224 | /** |
||
| 225 | * Determine if an enhancer is in use for Solr |
||
| 226 | * |
||
| 227 | * @param string $enhancerName |
||
| 228 | * @return bool |
||
| 229 | */ |
||
| 230 | public function isRouteEnhancerForSolr(string $enhancerName): bool |
||
| 231 | { |
||
| 232 | if (empty($enhancerName)) { |
||
| 233 | return false; |
||
| 234 | } |
||
| 235 | |||
| 236 | if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName])) { |
||
| 237 | return false; |
||
| 238 | } |
||
| 239 | $className = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName]; |
||
| 240 | |||
| 241 | if (!class_exists($className)) { |
||
| 242 | return false; |
||
| 243 | } |
||
| 244 | |||
| 245 | $interfaces = class_implements($className); |
||
| 246 | |||
| 247 | return in_array(SolrRouteEnhancerInterface::class, $interfaces); |
||
| 248 | } |
||
| 249 | |||
| 250 | /** |
||
| 251 | * Masks Solr filter inside of the query parameters |
||
| 252 | * |
||
| 253 | * @param string $uriPath |
||
| 254 | * @return string |
||
| 255 | */ |
||
| 256 | public function finalizePathQuery(string $uriPath): string |
||
| 257 | { |
||
| 258 | $pathSegments = explode('/', $uriPath); |
||
| 259 | $query = array_pop($pathSegments); |
||
| 260 | $queryValues = explode($this->urlFacetPathService->getMultiValueSeparator(), $query); |
||
| 261 | $queryValues = array_map([$this->urlFacetPathService, 'decodeSingleValue'], $queryValues); |
||
| 262 | /* |
||
| 263 | * In some constellations the path query contains the facet type in front. |
||
| 264 | * This leads to the result, that the query values could contain the same facet value multiple times. |
||
| 265 | * |
||
| 266 | * In order to avoid this behaviour, the query values need to be checked and clean up. |
||
| 267 | * 1. Remove possible prefix information |
||
| 268 | * 2. Apply character replacements |
||
| 269 | * 3. Filter duplicate values |
||
| 270 | */ |
||
| 271 | for ($i = 0; $i < count($queryValues); $i++) { |
||
|
|
|||
| 272 | $queryValues[$i] = urldecode($queryValues[$i]); |
||
| 273 | if ($this->containsFacetAndValueSeparator((string)$queryValues[$i])) { |
||
| 274 | [$facetName, $facetValue] = explode( |
||
| 275 | $this->detectFacetAndValueSeparator((string)$queryValues[$i]), |
||
| 276 | (string)$queryValues[$i], |
||
| 277 | 2 |
||
| 278 | ); |
||
| 279 | |||
| 280 | if ($this->isPathArgument((string)$facetName)) { |
||
| 281 | $queryValues[$i] = $facetValue; |
||
| 282 | } |
||
| 283 | |||
| 284 | } |
||
| 285 | $queryValues[$i] = $this->urlFacetPathService->applyCharacterMap($queryValues[$i]); |
||
| 286 | } |
||
| 287 | |||
| 288 | $queryValues = array_unique($queryValues); |
||
| 289 | $queryValues = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $queryValues); |
||
| 290 | sort($queryValues); |
||
| 291 | $pathSegments[] = implode( |
||
| 292 | $this->urlFacetPathService->getMultiValueSeparator(), |
||
| 293 | $queryValues |
||
| 294 | ); |
||
| 295 | return implode('/', $pathSegments); |
||
| 296 | } |
||
| 297 | |||
| 298 | /** |
||
| 299 | * This method checks if the query parameter should be masked. |
||
| 300 | * |
||
| 301 | * @return bool |
||
| 302 | */ |
||
| 303 | public function shouldMaskQueryParameter(): bool |
||
| 304 | { |
||
| 305 | if (!isset($this->settings['query']['mask']) || |
||
| 306 | !(bool)$this->settings['query']['mask']) { |
||
| 307 | return false; |
||
| 308 | } |
||
| 309 | |||
| 310 | $targetFields = $this->getQueryParameterMap(); |
||
| 311 | |||
| 312 | return !empty($targetFields); |
||
| 313 | } |
||
| 314 | |||
| 315 | /** |
||
| 316 | * Masks Solr filter inside of the query parameters |
||
| 317 | * |
||
| 318 | * @param array $queryParams |
||
| 319 | * @return array |
||
| 320 | */ |
||
| 321 | public function maskQueryParameters(array $queryParams): array |
||
| 322 | { |
||
| 323 | if (!$this->shouldMaskQueryParameter()) { |
||
| 324 | return $queryParams; |
||
| 325 | } |
||
| 326 | |||
| 327 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
| 328 | $this->logger |
||
| 329 | ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace()); |
||
| 330 | return $queryParams; |
||
| 331 | } |
||
| 332 | |||
| 333 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
| 334 | $this->logger |
||
| 335 | ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace()); |
||
| 336 | return $queryParams; |
||
| 337 | } |
||
| 338 | $queryParameterMap = $this->getQueryParameterMap(); |
||
| 339 | $newQueryParams = $queryParams; |
||
| 340 | |||
| 341 | $newFilterArray = []; |
||
| 342 | foreach ($newQueryParams[$this->getPluginNamespace()]['filter'] as $queryParamName => $queryParamValue) { |
||
| 343 | $defaultSeparator = $this->detectFacetAndValueSeparator((string)$queryParamValue); |
||
| 344 | [$facetName, $facetValue] = explode($defaultSeparator, $queryParamValue, 2); |
||
| 345 | $keep = false; |
||
| 346 | if (isset($queryParameterMap[$facetName]) && |
||
| 347 | isset($newQueryParams[$queryParameterMap[$facetName]])) { |
||
| 348 | $this->logger->error( |
||
| 349 | 'Mask error: Facet "' . $facetName . '" as "' . $queryParameterMap[$facetName] . |
||
| 350 | '" already in query!' |
||
| 351 | ); |
||
| 352 | $keep = true; |
||
| 353 | } |
||
| 354 | if (!isset($queryParameterMap[$facetName]) || $keep) { |
||
| 355 | $newFilterArray[] = $queryParamValue; |
||
| 356 | continue; |
||
| 357 | } |
||
| 358 | |||
| 359 | $newQueryParams[$queryParameterMap[$facetName]] = $facetValue; |
||
| 360 | } |
||
| 361 | |||
| 362 | $newQueryParams[$this->getPluginNamespace()]['filter'] = $newFilterArray; |
||
| 363 | |||
| 364 | return $this->cleanUpQueryParameters($newQueryParams); |
||
| 365 | } |
||
| 366 | |||
| 367 | /** |
||
| 368 | * Unmask incoming parameters if needed |
||
| 369 | * |
||
| 370 | * @param array $queryParams |
||
| 371 | * @return array |
||
| 372 | */ |
||
| 373 | public function unmaskQueryParameters(array $queryParams): array |
||
| 374 | { |
||
| 375 | if (!$this->shouldMaskQueryParameter()) { |
||
| 376 | return $queryParams; |
||
| 377 | } |
||
| 378 | |||
| 379 | /* |
||
| 380 | * The array $queryParameterMap contains the mapping of |
||
| 381 | * facet name to new url name. In order to unmask we need to switch key and values. |
||
| 382 | */ |
||
| 383 | $queryParameterMap = $this->getQueryParameterMap(); |
||
| 384 | $queryParameterMapSwitched = []; |
||
| 385 | foreach ($queryParameterMap as $value => $key) { |
||
| 386 | $queryParameterMapSwitched[$key] = $value; |
||
| 387 | } |
||
| 388 | |||
| 389 | $newQueryParams = []; |
||
| 390 | foreach ($queryParams as $queryParamName => $queryParamValue) { |
||
| 391 | // A merge is needed! |
||
| 392 | if (!isset($queryParameterMapSwitched[$queryParamName])) { |
||
| 393 | if (isset($newQueryParams[$queryParamName])) { |
||
| 394 | $newQueryParams[$queryParamName] = array_merge_recursive( |
||
| 395 | $newQueryParams[$queryParamName], |
||
| 396 | $queryParamValue |
||
| 397 | ); |
||
| 398 | } else { |
||
| 399 | $newQueryParams[$queryParamName] = $queryParamValue; |
||
| 400 | } |
||
| 401 | continue; |
||
| 402 | } |
||
| 403 | if (!isset($newQueryParams[$this->getPluginNamespace()])) { |
||
| 404 | $newQueryParams[$this->getPluginNamespace()] = []; |
||
| 405 | } |
||
| 406 | if (!isset($newQueryParams[$this->getPluginNamespace()]['filter'])) { |
||
| 407 | $newQueryParams[$this->getPluginNamespace()]['filter'] = []; |
||
| 408 | } |
||
| 409 | |||
| 410 | $newQueryParams[$this->getPluginNamespace()]['filter'][] = |
||
| 411 | $queryParameterMapSwitched[$queryParamName] . ':' . $queryParamValue; |
||
| 412 | } |
||
| 413 | |||
| 414 | return $this->cleanUpQueryParameters($newQueryParams); |
||
| 415 | } |
||
| 416 | |||
| 417 | /** |
||
| 418 | * This method check if the query parameters should be touched or not. |
||
| 419 | * |
||
| 420 | * There are following requirements: |
||
| 421 | * - Masking is activated and the mal is valid or |
||
| 422 | * - Concat is activated |
||
| 423 | * |
||
| 424 | * @return bool |
||
| 425 | */ |
||
| 426 | public function shouldConcatQueryParameters(): bool |
||
| 427 | { |
||
| 428 | /* |
||
| 429 | * The concat will activate automatically if parameters should be masked. |
||
| 430 | * This solution is less complex since not every mapping parameter needs to be tested |
||
| 431 | */ |
||
| 432 | if ($this->shouldMaskQueryParameter()) { |
||
| 433 | return true; |
||
| 434 | } |
||
| 435 | |||
| 436 | return isset($this->settings['query']['concat']) && (bool)$this->settings['query']['concat']; |
||
| 437 | } |
||
| 438 | |||
| 439 | /** |
||
| 440 | * Returns the query parameter map |
||
| 441 | * |
||
| 442 | * Note TYPO3 core query arguments removed from the configured map! |
||
| 443 | * |
||
| 444 | * @return array |
||
| 445 | */ |
||
| 446 | public function getQueryParameterMap(): array |
||
| 447 | { |
||
| 448 | if (!isset($this->settings['query']['map']) || |
||
| 449 | !is_array($this->settings['query']['map']) || |
||
| 450 | empty($this->settings['query']['map'])) { |
||
| 451 | return []; |
||
| 452 | } |
||
| 453 | // TODO: Test if there is more than one value! |
||
| 454 | $self = $this; |
||
| 455 | return array_filter( |
||
| 456 | $this->settings['query']['map'], |
||
| 457 | function($value) use ($self) { |
||
| 458 | return !$self->isCoreParameter($value); |
||
| 459 | } |
||
| 460 | ); |
||
| 461 | } |
||
| 462 | |||
| 463 | /** |
||
| 464 | * Group all filter values together and concat e |
||
| 465 | * Note: this will just handle filter values |
||
| 466 | * |
||
| 467 | * IN: |
||
| 468 | * tx_solr => [ |
||
| 469 | * filter => [ |
||
| 470 | * color:red |
||
| 471 | * product:candy |
||
| 472 | * color:blue |
||
| 473 | * taste:sour |
||
| 474 | * ] |
||
| 475 | * ] |
||
| 476 | * |
||
| 477 | * OUT: |
||
| 478 | * tx_solr => [ |
||
| 479 | * filter => [ |
||
| 480 | * color:blue,red |
||
| 481 | * product:candy |
||
| 482 | * taste:sour |
||
| 483 | * ] |
||
| 484 | * ] |
||
| 485 | * @param array $queryParams |
||
| 486 | * @return array |
||
| 487 | */ |
||
| 488 | public function concatQueryParameter(array $queryParams = []): array |
||
| 489 | { |
||
| 490 | if (!$this->shouldConcatQueryParameters()) { |
||
| 491 | return $queryParams; |
||
| 492 | } |
||
| 493 | |||
| 494 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
| 495 | $this->logger |
||
| 496 | ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace()); |
||
| 497 | return $queryParams; |
||
| 498 | } |
||
| 499 | |||
| 500 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
| 501 | $this->logger |
||
| 502 | ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace()); |
||
| 503 | return $queryParams; |
||
| 504 | } |
||
| 505 | |||
| 506 | $queryParams[$this->getPluginNamespace()]['filter'] = |
||
| 507 | $this->concatFilterValues($queryParams[$this->getPluginNamespace()]['filter']); |
||
| 508 | |||
| 509 | return $this->cleanUpQueryParameters($queryParams); |
||
| 510 | } |
||
| 511 | |||
| 512 | /** |
||
| 513 | * This method expect a filter array that should be concat instead of the whole query |
||
| 514 | * |
||
| 515 | * @param array $filterArray |
||
| 516 | * @return array |
||
| 517 | */ |
||
| 518 | public function concatFilterValues(array $filterArray): array |
||
| 519 | { |
||
| 520 | if (empty($filterArray) || !$this->shouldConcatQueryParameters()) { |
||
| 521 | return $filterArray; |
||
| 522 | } |
||
| 523 | |||
| 524 | $queryParameterMap = $this->getQueryParameterMap(); |
||
| 525 | $newFilterArray = []; |
||
| 526 | $defaultSeparator = $this->detectFacetAndValueSeparator((string)$filterArray[0]); |
||
| 527 | // Collect parameter names and rename parameter if required |
||
| 528 | foreach ($filterArray as $set) { |
||
| 529 | $separator = $this->detectFacetAndValueSeparator((string)$set); |
||
| 530 | [$facetName, $facetValue] = explode($separator, $set, 2); |
||
| 531 | if (isset($queryParameterMap[$facetName])) { |
||
| 532 | $facetName = $queryParameterMap[$facetName]; |
||
| 533 | } |
||
| 534 | if (!isset($newFilterArray[$facetName])) { |
||
| 535 | $newFilterArray[$facetName] = [$facetValue]; |
||
| 536 | } else { |
||
| 537 | $newFilterArray[$facetName][] = $facetValue; |
||
| 538 | } |
||
| 539 | } |
||
| 540 | |||
| 541 | foreach ($newFilterArray as $facetName => $facetValues) { |
||
| 542 | $newFilterArray[$facetName] = $facetName . $defaultSeparator . $this->queryParameterFacetsToString($facetValues); |
||
| 543 | } |
||
| 544 | |||
| 545 | return array_values($newFilterArray); |
||
| 546 | } |
||
| 547 | |||
| 548 | /** |
||
| 549 | * Inflate given query parameters if configured |
||
| 550 | * Note: this will just combine filter values |
||
| 551 | * |
||
| 552 | * IN: |
||
| 553 | * tx_solr => [ |
||
| 554 | * filter => [ |
||
| 555 | * color:blue,red |
||
| 556 | * product:candy |
||
| 557 | * taste:sour |
||
| 558 | * ] |
||
| 559 | * ] |
||
| 560 | * |
||
| 561 | * OUT: |
||
| 562 | * tx_solr => [ |
||
| 563 | * filter => [ |
||
| 564 | * color:red |
||
| 565 | * product:candy |
||
| 566 | * color:blue |
||
| 567 | * taste:sour |
||
| 568 | * ] |
||
| 569 | * ] |
||
| 570 | * |
||
| 571 | * @param array $queryParams |
||
| 572 | * @return array |
||
| 573 | */ |
||
| 574 | public function inflateQueryParameter(array $queryParams = []): array |
||
| 575 | { |
||
| 576 | if (!$this->shouldConcatQueryParameters()) { |
||
| 577 | return $queryParams; |
||
| 578 | } |
||
| 579 | |||
| 580 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
| 581 | $queryParams[$this->getPluginNamespace()] = []; |
||
| 582 | } |
||
| 583 | |||
| 584 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
| 585 | $queryParams[$this->getPluginNamespace()]['filter'] = []; |
||
| 586 | } |
||
| 587 | |||
| 588 | $newQueryParams = []; |
||
| 589 | foreach ($queryParams[$this->getPluginNamespace()]['filter'] as $set) { |
||
| 590 | $separator = $this->detectFacetAndValueSeparator((string)$set); |
||
| 591 | [$facetName, $facetValuesString] = explode($separator, $set, 2); |
||
| 592 | $facetValues = [$facetValuesString]; |
||
| 593 | $facetValues = explode($this->urlFacetQueryService->getMultiValueSeparator(), $facetValuesString); |
||
| 594 | |||
| 595 | /** |
||
| 596 | * A facet value could contain the multi value separator. This value is masked in order to |
||
| 597 | * avoid problems during separation of the values (line above). |
||
| 598 | * |
||
| 599 | * After splitting the values, the character inside the value need to be restored |
||
| 600 | * |
||
| 601 | * @see RoutingService::queryParameterFacetsToString |
||
| 602 | */ |
||
| 603 | $facetValues = array_map([$this->urlFacetQueryService, 'decodeSingleValue'], $facetValues); |
||
| 604 | |||
| 605 | foreach ($facetValues as $facetValue) { |
||
| 606 | $newQueryParams[] = $facetName . $separator . $facetValue; |
||
| 607 | } |
||
| 608 | } |
||
| 609 | $queryParams[$this->getPluginNamespace()]['filter'] = array_values($newQueryParams); |
||
| 610 | |||
| 611 | return $this->cleanUpQueryParameters($queryParams); |
||
| 612 | } |
||
| 613 | |||
| 614 | /** |
||
| 615 | * Cleanup the query parameters, to avoid empty solr arguments |
||
| 616 | * |
||
| 617 | * @param array $queryParams |
||
| 618 | * @return array |
||
| 619 | */ |
||
| 620 | public function cleanUpQueryParameters(array $queryParams): array |
||
| 621 | { |
||
| 622 | if (empty($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
| 623 | unset($queryParams[$this->getPluginNamespace()]['filter']); |
||
| 624 | } |
||
| 625 | |||
| 626 | if (empty($queryParams[$this->getPluginNamespace()])) { |
||
| 627 | unset($queryParams[$this->getPluginNamespace()]); |
||
| 628 | } |
||
| 629 | return $queryParams; |
||
| 630 | } |
||
| 631 | |||
| 632 | /** |
||
| 633 | * Builds a string out of multiple facet values |
||
| 634 | * |
||
| 635 | * A facet value could contain the multi value separator. This value have to masked in order to |
||
| 636 | * avoid problems during separation of the values later. |
||
| 637 | * |
||
| 638 | * This mask have to apply before contact the values |
||
| 639 | * |
||
| 640 | * @param array $facets |
||
| 641 | * @return string |
||
| 642 | */ |
||
| 643 | public function queryParameterFacetsToString(array $facets): string |
||
| 644 | { |
||
| 645 | $facets = array_map([$this->urlFacetQueryService, 'encodeSingleValue'], $facets); |
||
| 646 | sort($facets); |
||
| 647 | return implode($this->urlFacetQueryService->getMultiValueSeparator(), $facets); |
||
| 648 | } |
||
| 649 | |||
| 650 | /** |
||
| 651 | * Returns the string which separates the facet from the value |
||
| 652 | * |
||
| 653 | * @param string $facetWithValue |
||
| 654 | * @return string |
||
| 655 | */ |
||
| 656 | public function detectFacetAndValueSeparator(string $facetWithValue): string |
||
| 657 | { |
||
| 658 | $separator = ':'; |
||
| 659 | if (mb_strpos($facetWithValue, '%3A') !== false) { |
||
| 660 | $separator = '%3A'; |
||
| 661 | } |
||
| 662 | |||
| 663 | return $separator; |
||
| 664 | } |
||
| 665 | |||
| 666 | /** |
||
| 667 | * Check if given facet value combination contains a separator |
||
| 668 | * |
||
| 669 | * @param string $facetWithValue |
||
| 670 | * @return bool |
||
| 671 | */ |
||
| 672 | public function containsFacetAndValueSeparator(string $facetWithValue): bool |
||
| 673 | { |
||
| 674 | if (mb_strpos($facetWithValue, ':') === false && mb_strpos($facetWithValue, '%3A') === false) { |
||
| 675 | return false; |
||
| 676 | } |
||
| 677 | |||
| 678 | return true; |
||
| 679 | } |
||
| 680 | |||
| 681 | /** |
||
| 682 | * Cleanup facet values (strip type if needed) |
||
| 683 | * |
||
| 684 | * @param array $facetValues |
||
| 685 | * @return array |
||
| 686 | */ |
||
| 687 | public function cleanupFacetValues(array $facetValues): array |
||
| 688 | { |
||
| 689 | for ($i = 0; $i < count($facetValues); $i++) { |
||
| 690 | if (!$this->containsFacetAndValueSeparator((string)$facetValues[$i])) { |
||
| 691 | continue; |
||
| 692 | } |
||
| 693 | |||
| 694 | $separator = $this->detectFacetAndValueSeparator((string)$facetValues[$i]); |
||
| 695 | [$type, $value] = explode($separator, $facetValues[$i]); |
||
| 696 | |||
| 697 | if ($this->isMappingArgument($type) || $this->isPathArgument($type)) { |
||
| 698 | $facetValues[$i] = $value; |
||
| 699 | } |
||
| 700 | } |
||
| 701 | return $facetValues; |
||
| 702 | } |
||
| 703 | |||
| 704 | /** |
||
| 705 | * Builds a string out of multiple facet values |
||
| 706 | * |
||
| 707 | * @param array $facets |
||
| 708 | * @return string |
||
| 709 | */ |
||
| 710 | public function pathFacetsToString(array $facets): string |
||
| 711 | { |
||
| 712 | $facets = $this->cleanupFacetValues($facets); |
||
| 713 | sort($facets); |
||
| 714 | $facets = array_map([$this->urlFacetPathService, 'applyCharacterMap'], $facets); |
||
| 715 | $facets = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $facets); |
||
| 716 | return implode($this->urlFacetPathService->getMultiValueSeparator(), $facets); |
||
| 717 | } |
||
| 718 | |||
| 719 | /** |
||
| 720 | * Builds a string out of multiple facet values |
||
| 721 | * |
||
| 722 | * @param array $facets |
||
| 723 | * @return string |
||
| 724 | */ |
||
| 725 | public function facetsToString(array $facets): string |
||
| 726 | { |
||
| 727 | $facets = $this->cleanupFacetValues($facets); |
||
| 728 | sort($facets); |
||
| 729 | return implode($this->getDefaultMultiValueSeparator(), $facets); |
||
| 730 | } |
||
| 731 | |||
| 732 | /** |
||
| 733 | * Builds a string out of multiple facet values |
||
| 734 | * |
||
| 735 | * This method is used in two different situation |
||
| 736 | * 1. Middleware: Here the values should not be decoded |
||
| 737 | * 2. Within the event listener CachedPathVariableModifier |
||
| 738 | * |
||
| 739 | * @param string $facets |
||
| 740 | * @param bool $decode |
||
| 741 | * @return array |
||
| 742 | */ |
||
| 743 | public function pathFacetStringToArray(string $facets, bool $decode = true): array |
||
| 744 | { |
||
| 745 | $facetString = $this->urlFacetPathService->applyCharacterMap($facets); |
||
| 746 | $facets = explode($this->urlFacetPathService->getMultiValueSeparator(), $facetString); |
||
| 747 | if (!$decode) { |
||
| 748 | return $facets; |
||
| 749 | } |
||
| 750 | return array_map([$this->urlFacetPathService, 'decodeSingleValue'], $facets); |
||
| 751 | } |
||
| 752 | |||
| 753 | /** |
||
| 754 | * Returns the multi value separator |
||
| 755 | * @return string |
||
| 756 | */ |
||
| 757 | public function getDefaultMultiValueSeparator(): string |
||
| 758 | { |
||
| 759 | return $this->settings['multiValueSeparator'] ?? ','; |
||
| 760 | } |
||
| 761 | |||
| 762 | /** |
||
| 763 | * Find a enhancer configuration by a given page id |
||
| 764 | * |
||
| 765 | * @param int $pageUid |
||
| 766 | * @return array |
||
| 767 | */ |
||
| 768 | public function fetchEnhancerByPageUid(int $pageUid): array |
||
| 769 | { |
||
| 770 | $site = $this->findSiteByUid($pageUid); |
||
| 771 | if ($site instanceof NullSite) { |
||
| 772 | return []; |
||
| 773 | } |
||
| 774 | |||
| 775 | return $this->fetchEnhancerInSiteConfigurationByPageUid( |
||
| 776 | $site, |
||
| 777 | $pageUid |
||
| 778 | ); |
||
| 779 | } |
||
| 780 | |||
| 781 | /** |
||
| 782 | * Returns the route enhancer configuration by given site and page uid |
||
| 783 | * |
||
| 784 | * @param Site $site |
||
| 785 | * @param int $pageUid |
||
| 786 | * @return array |
||
| 787 | */ |
||
| 788 | public function fetchEnhancerInSiteConfigurationByPageUid(Site $site, int $pageUid): array |
||
| 789 | { |
||
| 790 | $configuration = $site->getConfiguration(); |
||
| 791 | if (empty($configuration['routeEnhancers']) || !is_array($configuration['routeEnhancers'])) { |
||
| 792 | return []; |
||
| 793 | } |
||
| 794 | $result = []; |
||
| 795 | foreach ($configuration['routeEnhancers'] as $routing => $settings) { |
||
| 796 | // Not the page we are looking for |
||
| 797 | if (isset($settings['limitToPages']) && |
||
| 798 | is_array($settings['limitToPages']) && |
||
| 799 | !in_array($pageUid, $settings['limitToPages'])) { |
||
| 800 | continue; |
||
| 801 | } |
||
| 802 | |||
| 803 | if (empty($settings) || !isset($settings['type']) || |
||
| 804 | !$this->isRouteEnhancerForSolr((string)$settings['type']) |
||
| 805 | ) { |
||
| 806 | continue; |
||
| 807 | } |
||
| 808 | $result[] = $settings; |
||
| 809 | } |
||
| 810 | |||
| 811 | return $result; |
||
| 812 | } |
||
| 813 | |||
| 814 | /** |
||
| 815 | * Add heading slash to given slug |
||
| 816 | * |
||
| 817 | * @param string $slug |
||
| 818 | * @return string |
||
| 819 | */ |
||
| 820 | public function cleanupHeadingSlash(string $slug): string |
||
| 821 | { |
||
| 822 | if (mb_substr($slug, 0, 1) !== '/') { |
||
| 823 | return '/' . $slug; |
||
| 824 | } else if (mb_substr($slug, 0, 2) === '//') { |
||
| 825 | return mb_substr($slug, 1, mb_strlen($slug) - 1); |
||
| 826 | } |
||
| 827 | |||
| 828 | return $slug; |
||
| 829 | } |
||
| 830 | |||
| 831 | /** |
||
| 832 | * Add heading slash to given slug |
||
| 833 | * |
||
| 834 | * @param string $slug |
||
| 835 | * @return string |
||
| 836 | */ |
||
| 837 | public function addHeadingSlash(string $slug): string |
||
| 838 | { |
||
| 839 | if (mb_substr($slug, 0, 1) === '/') { |
||
| 840 | return $slug; |
||
| 841 | } |
||
| 842 | |||
| 843 | return '/' . $slug; |
||
| 844 | } |
||
| 845 | |||
| 846 | /** |
||
| 847 | * Remove heading slash from given slug |
||
| 848 | * |
||
| 849 | * @param string $slug |
||
| 850 | * @return string |
||
| 851 | */ |
||
| 852 | public function removeHeadingSlash(string $slug): string |
||
| 853 | { |
||
| 854 | if (mb_substr($slug, 0, 1) !== '/') { |
||
| 855 | return $slug; |
||
| 856 | } |
||
| 857 | |||
| 858 | return mb_substr($slug, 1, mb_strlen($slug) - 1); |
||
| 859 | } |
||
| 860 | |||
| 861 | /** |
||
| 862 | * Retrieve the site by given UID |
||
| 863 | * |
||
| 864 | * @param int $pageUid |
||
| 865 | * @return SiteInterface |
||
| 866 | */ |
||
| 867 | public function findSiteByUid(int $pageUid): SiteInterface |
||
| 875 | } |
||
| 876 | } |
||
| 877 | |||
| 878 | /** |
||
| 879 | * @param Site $site |
||
| 880 | * @return PageSlugCandidateProvider |
||
| 881 | */ |
||
| 882 | public function getSlugCandidateProvider(Site $site): PageSlugCandidateProvider |
||
| 883 | { |
||
| 884 | $context = GeneralUtility::makeInstance(Context::class); |
||
| 885 | return GeneralUtility::makeInstance( |
||
| 886 | PageSlugCandidateProvider::class, |
||
| 887 | $context, |
||
| 888 | $site, |
||
| 889 | null |
||
| 890 | ); |
||
| 891 | } |
||
| 892 | |||
| 893 | /** |
||
| 894 | * Convert the base string into a URI object |
||
| 895 | * |
||
| 896 | * @param string $base |
||
| 897 | * @return UriInterface|null |
||
| 898 | */ |
||
| 899 | public function convertStringIntoUri(string $base): ?UriInterface |
||
| 900 | { |
||
| 901 | try { |
||
| 902 | /* @var Uri $uri */ |
||
| 903 | $uri = GeneralUtility::makeInstance( |
||
| 904 | Uri::class, |
||
| 905 | $base |
||
| 906 | ); |
||
| 907 | |||
| 908 | return $uri; |
||
| 909 | } catch (\InvalidArgumentException $argumentException) { |
||
| 910 | return null; |
||
| 911 | } |
||
| 912 | } |
||
| 913 | |||
| 914 | /** |
||
| 915 | * In order to search for a path, a possible language prefix need to remove |
||
| 916 | * |
||
| 917 | * @param SiteLanguage $language |
||
| 918 | * @param string $path |
||
| 919 | * @return string |
||
| 920 | */ |
||
| 921 | public function stripLanguagePrefixFromPath(SiteLanguage $language, string $path): string |
||
| 922 | { |
||
| 923 | if ($language->getBase()->getPath() === '/') { |
||
| 924 | return $path; |
||
| 925 | } |
||
| 926 | |||
| 927 | $pathLength = mb_strlen($language->getBase()->getPath()); |
||
| 928 | |||
| 929 | $path = mb_substr($path, $pathLength, mb_strlen($path) - $pathLength); |
||
| 930 | if (mb_substr($path, 0, 1) !== '/') { |
||
| 931 | $path = '/' . $path; |
||
| 932 | } |
||
| 933 | |||
| 934 | return $path; |
||
| 935 | } |
||
| 936 | |||
| 937 | /** |
||
| 938 | * Enrich the current query Params with data from path information |
||
| 939 | * |
||
| 940 | * @param ServerRequestInterface $request |
||
| 941 | * @param array $arguments |
||
| 942 | * @param array $parameters |
||
| 943 | * @return ServerRequestInterface |
||
| 944 | */ |
||
| 945 | public function addPathArgumentsToQuery( |
||
| 971 | } |
||
| 972 | |||
| 973 | /** |
||
| 974 | * Check if given argument is a mapping argument |
||
| 975 | * |
||
| 976 | * @param string $facetName |
||
| 977 | * @return bool |
||
| 978 | */ |
||
| 979 | public function isMappingArgument(string $facetName): bool |
||
| 987 | } |
||
| 988 | |||
| 989 | /** |
||
| 990 | * Check if given facet type is an path argument |
||
| 991 | * |
||
| 992 | * @param string $facetName |
||
| 993 | * @return bool |
||
| 994 | */ |
||
| 995 | public function isPathArgument(string $facetName): bool |
||
| 996 | { |
||
| 997 | return isset($this->pathArguments[$facetName]); |
||
| 998 | } |
||
| 999 | |||
| 1000 | /** |
||
| 1001 | * @param string $variable |
||
| 1002 | * @return string |
||
| 1003 | */ |
||
| 1004 | public function reviewVariable(string $variable): string |
||
| 1005 | { |
||
| 1006 | if (!$this->containsFacetAndValueSeparator((string)$variable)) { |
||
| 1007 | return $variable; |
||
| 1008 | } |
||
| 1009 | |||
| 1010 | $separator = $this->detectFacetAndValueSeparator((string)$variable); |
||
| 1011 | [$type, $value] = explode($separator, $variable, 2); |
||
| 1012 | |||
| 1013 | return $this->isMappingArgument($type) ? $value : $variable; |
||
| 1014 | } |
||
| 1015 | |||
| 1016 | /** |
||
| 1017 | * Remove type prefix from filter |
||
| 1018 | * |
||
| 1019 | * @param array $variables |
||
| 1020 | * @return array |
||
| 1021 | */ |
||
| 1022 | public function reviseFilterVariables(array $variables): array |
||
| 1023 | { |
||
| 1024 | $newVariables = []; |
||
| 1025 | foreach ($variables as $key => $value) { |
||
| 1026 | $matches = []; |
||
| 1027 | if (!preg_match('/###' . $this->getPluginNamespace() . ':filter:\d+:(.+?)###/', $key, $matches)) { |
||
| 1028 | $newVariables[$key] = $value; |
||
| 1029 | continue; |
||
| 1030 | } |
||
| 1031 | if (!$this->isMappingArgument($matches[1]) && !$this->isPathArgument($matches[1])) { |
||
| 1032 | $newVariables[$key] = $value; |
||
| 1033 | continue; |
||
| 1034 | } |
||
| 1035 | $separator = $this->detectFacetAndValueSeparator((string)$value); |
||
| 1036 | $parts = explode($separator, $value); |
||
| 1037 | |||
| 1038 | do { |
||
| 1039 | if ($parts[0] === $matches[1]) { |
||
| 1040 | array_shift($parts); |
||
| 1041 | } |
||
| 1042 | } while ($parts[0] === $matches[1]); |
||
| 1043 | |||
| 1044 | $newVariables[$key] = implode($separator, $parts); |
||
| 1045 | } |
||
| 1046 | |||
| 1047 | return $newVariables; |
||
| 1048 | } |
||
| 1049 | |||
| 1050 | /** |
||
| 1051 | * Converts path segment information into query parameters |
||
| 1052 | * |
||
| 1053 | * Example: |
||
| 1054 | * /products/household |
||
| 1055 | * |
||
| 1056 | * tx_solr: |
||
| 1057 | * filter: |
||
| 1058 | * - type:household |
||
| 1059 | * |
||
| 1060 | * @param array $queryParams |
||
| 1061 | * @param string $fieldName |
||
| 1062 | * @param array $parameters |
||
| 1063 | * @param array $pathElements |
||
| 1064 | * @return array |
||
| 1065 | */ |
||
| 1066 | protected function processUriPathArgument( |
||
| 1067 | array $queryParams, |
||
| 1068 | string $fieldName, |
||
| 1069 | array $parameters, |
||
| 1070 | array $pathElements |
||
| 1071 | ): array { |
||
| 1072 | $queryKey = array_shift($pathElements); |
||
| 1073 | $queryKey = (string)$queryKey; |
||
| 1074 | |||
| 1075 | $tmpQueryKey = $queryKey; |
||
| 1076 | if (strpos($queryKey, '-') !== false) { |
||
| 1077 | [$tmpQueryKey, $filterName] = explode('-', $tmpQueryKey, 2); |
||
| 1078 | } |
||
| 1079 | if (!isset($queryParams[$tmpQueryKey]) || $queryParams[$tmpQueryKey] === null) { |
||
| 1080 | $queryParams[$tmpQueryKey] = []; |
||
| 1081 | } |
||
| 1082 | |||
| 1083 | if (strpos($queryKey, '-') !== false) { |
||
| 1084 | [$queryKey, $filterName] = explode('-', $queryKey, 2); |
||
| 1085 | // explode multiple values |
||
| 1086 | $values = $this->pathFacetStringToArray($parameters[$fieldName], false); |
||
| 1087 | sort($values); |
||
| 1088 | |||
| 1089 | // @TODO: Support URL data bag |
||
| 1090 | foreach ($values as $value) { |
||
| 1091 | $value = $this->urlFacetPathService->applyCharacterMap($value); |
||
| 1092 | $queryParams[$queryKey][] = $filterName . ':' . $value; |
||
| 1093 | } |
||
| 1094 | } else { |
||
| 1095 | $queryParams[$queryKey] = $this->processUriPathArgument( |
||
| 1096 | $queryParams[$queryKey], |
||
| 1097 | $fieldName, |
||
| 1098 | $parameters, |
||
| 1099 | $pathElements |
||
| 1100 | ); |
||
| 1101 | } |
||
| 1102 | |||
| 1103 | return $queryParams; |
||
| 1104 | } |
||
| 1105 | |||
| 1106 | /** |
||
| 1107 | * Return site matcher |
||
| 1108 | * |
||
| 1109 | * @return SiteMatcher |
||
| 1110 | */ |
||
| 1111 | public function getSiteMatcher(): SiteMatcher |
||
| 1112 | { |
||
| 1113 | return GeneralUtility::makeInstance(SiteMatcher::class, $this->getSiteFinder()); |
||
| 1114 | } |
||
| 1115 | |||
| 1116 | /** |
||
| 1117 | * Returns the site finder |
||
| 1118 | * |
||
| 1119 | * @return SiteFinder|null |
||
| 1120 | */ |
||
| 1121 | protected function getSiteFinder(): ?SiteFinder |
||
| 1124 | } |
||
| 1125 | } |
||
| 1126 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: