1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Extbase\Mvc\Web\Routing;
19
20use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
21use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
22use TYPO3\CMS\Core\Utility\ArrayUtility;
23use TYPO3\CMS\Core\Utility\GeneralUtility;
24use TYPO3\CMS\Core\Utility\HttpUtility;
25use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
27use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
28use TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException;
29use TYPO3\CMS\Extbase\Mvc\Request;
30use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
31use TYPO3\CMS\Extbase\Service\EnvironmentService;
32use TYPO3\CMS\Extbase\Service\ExtensionService;
33use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
34
35/**
36 * An URI Builder
37 */
38class UriBuilder
39{
40    /**
41     * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
42     */
43    protected $configurationManager;
44
45    /**
46     * @var \TYPO3\CMS\Extbase\Service\ExtensionService
47     */
48    protected $extensionService;
49
50    /**
51     * An instance of \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
52     *
53     * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
54     */
55    protected $contentObject;
56
57    /**
58     * @var Request|null
59     */
60    protected $request;
61
62    /**
63     * @var array
64     */
65    protected $arguments = [];
66
67    /**
68     * Arguments which have been used for building the last URI
69     *
70     * @var array
71     */
72    protected $lastArguments = [];
73
74    /**
75     * @var string
76     */
77    protected $section = '';
78
79    /**
80     * @var bool
81     */
82    protected $createAbsoluteUri = false;
83
84    /**
85     * @var string
86     */
87    protected $absoluteUriScheme;
88
89    /**
90     * @var bool
91     */
92    protected $addQueryString = false;
93
94    /**
95     * @var string
96     */
97    protected $addQueryStringMethod = '';
98
99    /**
100     * @var array
101     */
102    protected $argumentsToBeExcludedFromQueryString = [];
103
104    /**
105     * @var bool
106     */
107    protected $linkAccessRestrictedPages = false;
108
109    /**
110     * @var int|null
111     */
112    protected $targetPageUid;
113
114    /**
115     * @var int
116     */
117    protected $targetPageType = 0;
118
119    /**
120     * @var string
121     */
122    protected $language;
123
124    /**
125     * @var bool
126     */
127    protected $noCache = false;
128
129    /**
130     * @var string
131     */
132    protected $format = '';
133
134    /**
135     * @var string|null
136     */
137    protected $argumentPrefix;
138
139    /**
140     * @var \TYPO3\CMS\Extbase\Service\EnvironmentService
141     */
142    protected $environmentService;
143
144    /**
145     * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
146     * @internal only to be used within Extbase, not part of TYPO3 Core API.
147     */
148    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
149    {
150        $this->configurationManager = $configurationManager;
151    }
152
153    /**
154     * @param \TYPO3\CMS\Extbase\Service\ExtensionService $extensionService
155     * @internal only to be used within Extbase, not part of TYPO3 Core API.
156     */
157    public function injectExtensionService(ExtensionService $extensionService): void
158    {
159        $this->extensionService = $extensionService;
160    }
161
162    /**
163     * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
164     * @internal only to be used within Extbase, not part of TYPO3 Core API.
165     */
166    public function injectEnvironmentService(EnvironmentService $environmentService): void
167    {
168        $this->environmentService = $environmentService;
169    }
170
171    /**
172     * Life-cycle method that is called by the DI container as soon as this object is completely built
173     * @internal only to be used within Extbase, not part of TYPO3 Core API.
174     */
175    public function initializeObject(): void
176    {
177        $this->contentObject = $this->configurationManager->getContentObject()
178            ?? GeneralUtility::makeInstance(ContentObjectRenderer::class);
179    }
180
181    /**
182     * Sets the current request
183     *
184     * @param Request $request
185     * @return static the current UriBuilder to allow method chaining
186     * @internal only to be used within Extbase, not part of TYPO3 Core API.
187     */
188    public function setRequest(Request $request): UriBuilder
189    {
190        $this->request = $request;
191        return $this;
192    }
193
194    /**
195     * @return Request|null
196     * @internal only to be used within Extbase, not part of TYPO3 Core API.
197     */
198    public function getRequest(): ?Request
199    {
200        return $this->request;
201    }
202
203    /**
204     * Additional query parameters.
205     * If you want to "prefix" arguments, you can pass in multidimensional arrays:
206     * array('prefix1' => array('foo' => 'bar')) gets "&prefix1[foo]=bar"
207     *
208     * @param array $arguments
209     * @return static the current UriBuilder to allow method chaining
210     */
211    public function setArguments(array $arguments): UriBuilder
212    {
213        $this->arguments = $arguments;
214        return $this;
215    }
216
217    /**
218     * @return array
219     * @internal
220     */
221    public function getArguments(): array
222    {
223        return $this->arguments;
224    }
225
226    /**
227     * If specified, adds a given HTML anchor to the URI (#...)
228     *
229     * @param string $section
230     * @return static the current UriBuilder to allow method chaining
231     */
232    public function setSection(string $section): UriBuilder
233    {
234        $this->section = $section;
235        return $this;
236    }
237
238    /**
239     * @return string
240     * @internal
241     */
242    public function getSection(): string
243    {
244        return $this->section;
245    }
246
247    /**
248     * Specifies the format of the target (e.g. "html" or "xml")
249     *
250     * @param string $format
251     * @return static the current UriBuilder to allow method chaining
252     */
253    public function setFormat(string $format): UriBuilder
254    {
255        $this->format = $format;
256        return $this;
257    }
258
259    /**
260     * @return string
261     * @internal
262     */
263    public function getFormat(): string
264    {
265        return $this->format;
266    }
267
268    /**
269     * If set, the URI is prepended with the current base URI. Defaults to FALSE.
270     *
271     * @param bool $createAbsoluteUri
272     * @return static the current UriBuilder to allow method chaining
273     */
274    public function setCreateAbsoluteUri(bool $createAbsoluteUri): UriBuilder
275    {
276        $this->createAbsoluteUri = $createAbsoluteUri;
277        return $this;
278    }
279
280    /**
281     * @return bool
282     * @internal
283     */
284    public function getCreateAbsoluteUri(): bool
285    {
286        return $this->createAbsoluteUri;
287    }
288
289    /**
290     * @return string|null
291     * @internal only to be used within Extbase, not part of TYPO3 Core API.
292     */
293    public function getAbsoluteUriScheme(): ?string
294    {
295        return $this->absoluteUriScheme;
296    }
297
298    /**
299     * Sets the scheme that should be used for absolute URIs in FE mode
300     *
301     * @param string $absoluteUriScheme the scheme to be used for absolute URIs
302     * @return static the current UriBuilder to allow method chaining
303     */
304    public function setAbsoluteUriScheme(string $absoluteUriScheme): UriBuilder
305    {
306        $this->absoluteUriScheme = $absoluteUriScheme;
307        return $this;
308    }
309
310    /**
311     * Enforces a URI / link to a page to a specific language (or use "current")
312     * @param string|null $language
313     * @return UriBuilder
314     */
315    public function setLanguage(?string $language): UriBuilder
316    {
317        $this->language = $language;
318        return $this;
319    }
320
321    public function getLanguage(): ?string
322    {
323        return $this->language;
324    }
325
326    /**
327     * If set, the current query parameters will be merged with $this->arguments. Defaults to FALSE.
328     *
329     * @param bool $addQueryString
330     * @return static the current UriBuilder to allow method chaining
331     * @see https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Typolink.html#addquerystring
332     */
333    public function setAddQueryString(bool $addQueryString): UriBuilder
334    {
335        $this->addQueryString = $addQueryString;
336        return $this;
337    }
338
339    /**
340     * @return bool
341     * @internal
342     */
343    public function getAddQueryString(): bool
344    {
345        return $this->addQueryString;
346    }
347
348    /**
349     * Sets the method to get the addQueryString parameters. Defaults to an empty string
350     * which results in using GeneralUtility::_GET(). Possible values are
351     *
352     * + ''      -> uses GeneralUtility::_GET()
353     * + '0'     -> uses GeneralUtility::_GET()
354     * + 'GET'   -> uses GeneralUtility::_GET()
355     * + '<any>' -> uses parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'))
356     *              (<any> refers to literally everything else than previously mentioned values)
357     *
358     * @param string $addQueryStringMethod
359     * @return static the current UriBuilder to allow method chaining
360     * @see https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Typolink.html#addquerystring
361     */
362    public function setAddQueryStringMethod(string $addQueryStringMethod): UriBuilder
363    {
364        if ($addQueryStringMethod === 'POST') {
365            trigger_error('Assigning addQueryStringMethod = POST is not supported anymore since TYPO3 v10.0.', E_USER_WARNING);
366            $addQueryStringMethod = null;
367        } elseif ($addQueryStringMethod === 'GET,POST' || $addQueryStringMethod === 'POST,GET') {
368            trigger_error('Assigning addQueryStringMethod = GET,POST or POST,GET is not supported anymore since TYPO3 v10.0 - falling back to GET.', E_USER_WARNING);
369            $addQueryStringMethod = 'GET';
370        }
371        $this->addQueryStringMethod = $addQueryStringMethod;
372        return $this;
373    }
374
375    /**
376     * @return string
377     * @internal
378     */
379    public function getAddQueryStringMethod(): string
380    {
381        return $this->addQueryStringMethod;
382    }
383
384    /**
385     * A list of arguments to be excluded from the query parameters
386     * Only active if addQueryString is set
387     *
388     * @param array $argumentsToBeExcludedFromQueryString
389     * @return static the current UriBuilder to allow method chaining
390     * @see https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Typolink.html#addquerystring
391     * @see setAddQueryString()
392     */
393    public function setArgumentsToBeExcludedFromQueryString(array $argumentsToBeExcludedFromQueryString): UriBuilder
394    {
395        $this->argumentsToBeExcludedFromQueryString = $argumentsToBeExcludedFromQueryString;
396        return $this;
397    }
398
399    /**
400     * @return array
401     * @internal
402     */
403    public function getArgumentsToBeExcludedFromQueryString(): array
404    {
405        return $this->argumentsToBeExcludedFromQueryString;
406    }
407
408    /**
409     * Specifies the prefix to be used for all arguments.
410     *
411     * @param string $argumentPrefix
412     * @return static the current UriBuilder to allow method chaining
413     */
414    public function setArgumentPrefix(string $argumentPrefix): UriBuilder
415    {
416        $this->argumentPrefix = $argumentPrefix;
417        return $this;
418    }
419
420    /**
421     * @return string|null
422     * @internal only to be used within Extbase, not part of TYPO3 Core API.
423     */
424    public function getArgumentPrefix(): ?string
425    {
426        return $this->argumentPrefix;
427    }
428
429    /**
430     * If set, URIs for pages without access permissions will be created
431     *
432     * @param bool $linkAccessRestrictedPages
433     * @return static the current UriBuilder to allow method chaining
434     */
435    public function setLinkAccessRestrictedPages(bool $linkAccessRestrictedPages): UriBuilder
436    {
437        $this->linkAccessRestrictedPages = $linkAccessRestrictedPages;
438        return $this;
439    }
440
441    /**
442     * @return bool
443     * @internal
444     */
445    public function getLinkAccessRestrictedPages(): bool
446    {
447        return $this->linkAccessRestrictedPages;
448    }
449
450    /**
451     * Uid of the target page
452     *
453     * @param int $targetPageUid
454     * @return static the current UriBuilder to allow method chaining
455     */
456    public function setTargetPageUid(int $targetPageUid): UriBuilder
457    {
458        $this->targetPageUid = $targetPageUid;
459        return $this;
460    }
461
462    /**
463     * returns $this->targetPageUid.
464     *
465     * @return int|null
466     * @internal
467     */
468    public function getTargetPageUid(): ?int
469    {
470        return $this->targetPageUid;
471    }
472
473    /**
474     * Sets the page type of the target URI. Defaults to 0
475     *
476     * @param int $targetPageType
477     * @return static the current UriBuilder to allow method chaining
478     */
479    public function setTargetPageType(int $targetPageType): UriBuilder
480    {
481        $this->targetPageType = $targetPageType;
482        return $this;
483    }
484
485    /**
486     * @return int
487     * @internal only to be used within Extbase, not part of TYPO3 Core API.
488     */
489    public function getTargetPageType(): int
490    {
491        return $this->targetPageType;
492    }
493
494    /**
495     * by default FALSE; if TRUE, &no_cache=1 will be appended to the URI
496     *
497     * @param bool $noCache
498     * @return static the current UriBuilder to allow method chaining
499     */
500    public function setNoCache(bool $noCache): UriBuilder
501    {
502        $this->noCache = $noCache;
503        return $this;
504    }
505
506    /**
507     * @return bool
508     * @internal
509     */
510    public function getNoCache(): bool
511    {
512        return $this->noCache;
513    }
514
515    /**
516     * by default TRUE; if FALSE, no cHash parameter will be appended to the URI
517     * If noCache is set, this setting will be ignored.
518     *
519     * @return static the current UriBuilder to allow method chaining
520     */
521    public function setUseCacheHash(): UriBuilder
522    {
523        trigger_error('Calling UriBuilder->setUseCacheHash() will be removed in TYPO3 v11.0. TYPO3 Core routing is taking care of handling this argument. Simply remove the line to avoid the notice.', E_USER_DEPRECATED);
524        return $this;
525    }
526
527    /**
528     * @return bool
529     * @internal
530     */
531    public function getUseCacheHash(): bool
532    {
533        trigger_error('Calling UriBuilder->getUseCacheHash() will be removed in TYPO3 v11.0. TYPO3 Core routing is taking care of handling this argument. Simply remove the line to avoid the notice.', E_USER_DEPRECATED);
534        return true;
535    }
536
537    /**
538     * Returns the arguments being used for the last URI being built.
539     * This is only set after build() / uriFor() has been called.
540     *
541     * @return array The last arguments
542     * @internal only to be used within Extbase, not part of TYPO3 Core API.
543     */
544    public function getLastArguments(): array
545    {
546        return $this->lastArguments;
547    }
548
549    /**
550     * Resets all UriBuilder options to their default value
551     *
552     * @return static the current UriBuilder to allow method chaining
553     */
554    public function reset(): UriBuilder
555    {
556        $this->arguments = [];
557        $this->section = '';
558        $this->format = '';
559        $this->language = null;
560        $this->createAbsoluteUri = false;
561        $this->addQueryString = false;
562        $this->addQueryStringMethod = '';
563        $this->argumentsToBeExcludedFromQueryString = [];
564        $this->linkAccessRestrictedPages = false;
565        $this->targetPageUid = null;
566        $this->targetPageType = 0;
567        $this->noCache = false;
568        $this->argumentPrefix = null;
569        $this->absoluteUriScheme = null;
570        /*
571         * $this->request MUST NOT be reset here because the request is actually a hard dependency and not part
572         * of the internal state of this object.
573         * todo: consider making the request a constructor dependency or get rid of it's usage
574         */
575        return $this;
576    }
577
578    /**
579     * Creates an URI used for linking to an Extbase action.
580     * Works in Frontend and Backend mode of TYPO3.
581     *
582     * @param string|null $actionName Name of the action to be called
583     * @param array|null $controllerArguments Additional query parameters. Will be "namespaced" and merged with $this->arguments.
584     * @param string|null $controllerName Name of the target controller. If not set, current ControllerName is used.
585     * @param string|null $extensionName Name of the target extension, without underscores. If not set, current ExtensionName is used.
586     * @param string|null $pluginName Name of the target plugin. If not set, current PluginName is used.
587     * @return string the rendered URI
588     * @see build()
589     */
590    public function uriFor(
591        ?string $actionName = null,
592        ?array $controllerArguments = null,
593        ?string $controllerName = null,
594        ?string $extensionName = null,
595        ?string $pluginName = null
596    ): string {
597        $controllerArguments = $controllerArguments ?? [];
598
599        if ($actionName !== null) {
600            $controllerArguments['action'] = $actionName;
601        }
602        if ($controllerName !== null) {
603            $controllerArguments['controller'] = $controllerName;
604        } else {
605            $controllerArguments['controller'] = $this->request->getControllerName();
606        }
607        if ($extensionName === null) {
608            $extensionName = $this->request->getControllerExtensionName();
609        }
610        if ($pluginName === null && $this->environmentService->isEnvironmentInFrontendMode()) {
611            $pluginName = $this->extensionService->getPluginNameByAction($extensionName, $controllerArguments['controller'], $controllerArguments['action'] ?? null);
612        }
613        if ($pluginName === null) {
614            $pluginName = $this->request->getPluginName();
615        }
616        if ($this->environmentService->isEnvironmentInFrontendMode() && $this->configurationManager->isFeatureEnabled('skipDefaultArguments')) {
617            $controllerArguments = $this->removeDefaultControllerAndAction($controllerArguments, $extensionName, $pluginName);
618        }
619        if ($this->targetPageUid === null && $this->environmentService->isEnvironmentInFrontendMode()) {
620            $this->targetPageUid = $this->extensionService->getTargetPidByPlugin($extensionName, $pluginName);
621        }
622        if ($this->format !== '') {
623            $controllerArguments['format'] = $this->format;
624        }
625        if ($this->argumentPrefix !== null) {
626            $prefixedControllerArguments = [$this->argumentPrefix => $controllerArguments];
627        } else {
628            $pluginNamespace = $this->extensionService->getPluginNamespace($extensionName, $pluginName);
629            $prefixedControllerArguments = [$pluginNamespace => $controllerArguments];
630        }
631        ArrayUtility::mergeRecursiveWithOverrule($this->arguments, $prefixedControllerArguments);
632        return $this->build();
633    }
634
635    /**
636     * This removes controller and/or action arguments from given controllerArguments
637     * if they are equal to the default controller/action of the target plugin.
638     * Note: This is only active in FE mode and if feature "skipDefaultArguments" is enabled
639     *
640     * @see \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface::isFeatureEnabled()
641     * @param array $controllerArguments the current controller arguments to be modified
642     * @param string $extensionName target extension name
643     * @param string $pluginName target plugin name
644     * @return array
645     */
646    protected function removeDefaultControllerAndAction(array $controllerArguments, string $extensionName, string $pluginName): array
647    {
648        $defaultControllerName = $this->extensionService->getDefaultControllerNameByPlugin($extensionName, $pluginName);
649        if (isset($controllerArguments['action'])) {
650            $defaultActionName = $this->extensionService->getDefaultActionNameByPluginAndController($extensionName, $pluginName, $controllerArguments['controller']);
651            if ($controllerArguments['action'] === $defaultActionName) {
652                unset($controllerArguments['action']);
653            }
654        }
655        if ($controllerArguments['controller'] === $defaultControllerName) {
656            unset($controllerArguments['controller']);
657        }
658        return $controllerArguments;
659    }
660
661    /**
662     * Builds the URI
663     * Depending on the current context this calls buildBackendUri() or buildFrontendUri()
664     *
665     * @return string The URI
666     * @see buildBackendUri()
667     * @see buildFrontendUri()
668     */
669    public function build(): string
670    {
671        if ($this->environmentService->isEnvironmentInBackendMode()) {
672            return $this->buildBackendUri();
673        }
674        return $this->buildFrontendUri();
675    }
676
677    /**
678     * Builds the URI, backend flavour
679     * The resulting URI is relative and starts with "index.php".
680     * The settings pageUid, pageType, noCache & linkAccessRestrictedPages
681     * will be ignored in the backend.
682     *
683     * @return string The URI
684     * @internal only to be used within Extbase, not part of TYPO3 Core API.
685     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getQueryArguments
686     */
687    public function buildBackendUri(): string
688    {
689        $arguments = [];
690        if ($this->addQueryString === true) {
691            if ($this->addQueryStringMethod === '' || $this->addQueryStringMethod === '0' || $this->addQueryStringMethod === 'GET') {
692                $arguments = GeneralUtility::_GET();
693            } else {
694                // Explode GET vars recursively
695                parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $arguments);
696            }
697            foreach ($this->argumentsToBeExcludedFromQueryString as $argumentToBeExcluded) {
698                $argumentArrayToBeExcluded = [];
699                parse_str($argumentToBeExcluded, $argumentArrayToBeExcluded);
700                $arguments = ArrayUtility::arrayDiffKeyRecursive($arguments, $argumentArrayToBeExcluded);
701            }
702        } else {
703            $id = GeneralUtility::_GP('id');
704            $route = GeneralUtility::_GP('route');
705            if ($id !== null) {
706                $arguments['id'] = $id;
707            }
708            if ($route !== null) {
709                $arguments['route'] = $route;
710            }
711        }
712        ArrayUtility::mergeRecursiveWithOverrule($arguments, $this->arguments);
713        $arguments = $this->convertDomainObjectsToIdentityArrays($arguments);
714        $this->lastArguments = $arguments;
715        $routeName = $arguments['route'] ?? null;
716        unset($arguments['route'], $arguments['token']);
717        $backendUriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
718        try {
719            if ($this->createAbsoluteUri) {
720                $uri = (string)$backendUriBuilder->buildUriFromRoutePath($routeName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
721            } else {
722                $uri = (string)$backendUriBuilder->buildUriFromRoutePath($routeName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_PATH);
723            }
724        } catch (ResourceNotFoundException $e) {
725            try {
726                if ($this->createAbsoluteUri) {
727                    $uri = (string)$backendUriBuilder->buildUriFromRoute($routeName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_URL);
728                } else {
729                    $uri = (string)$backendUriBuilder->buildUriFromRoute($routeName, $arguments, \TYPO3\CMS\Backend\Routing\UriBuilder::ABSOLUTE_PATH);
730                }
731            } catch (RouteNotFoundException $e) {
732                $uri = '';
733            }
734        }
735        if ($this->section !== '') {
736            $uri .= '#' . $this->section;
737        }
738        return $uri;
739    }
740
741    /**
742     * Builds the URI, frontend flavour
743     *
744     * @return string The URI
745     * @see buildTypolinkConfiguration()
746     * @internal only to be used within Extbase, not part of TYPO3 Core API.
747     */
748    public function buildFrontendUri(): string
749    {
750        $typolinkConfiguration = $this->buildTypolinkConfiguration();
751        if ($this->createAbsoluteUri === true) {
752            $typolinkConfiguration['forceAbsoluteUrl'] = true;
753            if ($this->absoluteUriScheme !== null) {
754                $typolinkConfiguration['forceAbsoluteUrl.']['scheme'] = $this->absoluteUriScheme;
755            }
756        }
757        // Other than stated in the doc block, typoLink_URL does not always return a string
758        // Thus, we explicitly cast to string here.
759        $uri = (string)$this->contentObject->typoLink_URL($typolinkConfiguration);
760        return $uri;
761    }
762
763    /**
764     * Builds a TypoLink configuration array from the current settings
765     *
766     * @return array typolink configuration array
767     * @see https://docs.typo3.org/m/typo3/reference-typoscript/master/en-us/Functions/Typolink.html
768     */
769    protected function buildTypolinkConfiguration(): array
770    {
771        $typolinkConfiguration = [];
772        $typolinkConfiguration['parameter'] = $this->targetPageUid ?? $GLOBALS['TSFE']->id;
773        if ($this->targetPageType !== 0) {
774            $typolinkConfiguration['parameter'] .= ',' . $this->targetPageType;
775        } elseif ($this->format !== '') {
776            $targetPageType = $this->extensionService->getTargetPageTypeByFormat($this->request->getControllerExtensionName(), $this->format);
777            $typolinkConfiguration['parameter'] .= ',' . $targetPageType;
778        }
779        if (!empty($this->arguments)) {
780            $arguments = $this->convertDomainObjectsToIdentityArrays($this->arguments);
781            $this->lastArguments = $arguments;
782            $typolinkConfiguration['additionalParams'] = HttpUtility::buildQueryString($arguments, '&');
783        }
784        if ($this->addQueryString === true) {
785            $typolinkConfiguration['addQueryString'] = 1;
786            if (!empty($this->argumentsToBeExcludedFromQueryString)) {
787                $typolinkConfiguration['addQueryString.'] = [
788                    'exclude' => implode(',', $this->argumentsToBeExcludedFromQueryString)
789                ];
790            }
791            if ($this->addQueryStringMethod !== '') {
792                $typolinkConfiguration['addQueryString.']['method'] = $this->addQueryStringMethod;
793            }
794        }
795        if ($this->language !== null) {
796            $typolinkConfiguration['language'] = $this->language;
797        }
798
799        if ($this->noCache === true) {
800            $typolinkConfiguration['no_cache'] = 1;
801        }
802        if ($this->section !== '') {
803            $typolinkConfiguration['section'] = $this->section;
804        }
805        if ($this->linkAccessRestrictedPages === true) {
806            $typolinkConfiguration['linkAccessRestrictedPages'] = 1;
807        }
808        return $typolinkConfiguration;
809    }
810
811    /**
812     * Recursively iterates through the specified arguments and turns instances of type \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
813     * into an arrays containing the uid of the domain object.
814     *
815     * @param array $arguments The arguments to be iterated
816     * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException
817     * @return array The modified arguments array
818     */
819    protected function convertDomainObjectsToIdentityArrays(array $arguments): array
820    {
821        foreach ($arguments as $argumentKey => $argumentValue) {
822            // if we have a LazyLoadingProxy here, make sure to get the real instance for further processing
823            if ($argumentValue instanceof LazyLoadingProxy) {
824                $argumentValue = $argumentValue->_loadRealInstance();
825                // also update the value in the arguments array, because the lazyLoaded object could be
826                // hidden and thus the $argumentValue would be NULL.
827                $arguments[$argumentKey] = $argumentValue;
828            }
829            if ($argumentValue instanceof \Iterator) {
830                $argumentValue = $this->convertIteratorToArray($argumentValue);
831            }
832            if ($argumentValue instanceof AbstractDomainObject) {
833                if ($argumentValue->getUid() !== null) {
834                    $arguments[$argumentKey] = $argumentValue->getUid();
835                } elseif ($argumentValue instanceof AbstractValueObject) {
836                    $arguments[$argumentKey] = $this->convertTransientObjectToArray($argumentValue);
837                } else {
838                    throw new InvalidArgumentValueException('Could not serialize Domain Object ' . get_class($argumentValue) . '. It is neither an Entity with identity properties set, nor a Value Object.', 1260881688);
839                }
840            } elseif (is_array($argumentValue)) {
841                $arguments[$argumentKey] = $this->convertDomainObjectsToIdentityArrays($argumentValue);
842            }
843        }
844        return $arguments;
845    }
846
847    /**
848     * @param \Iterator $iterator
849     * @return array
850     */
851    protected function convertIteratorToArray(\Iterator $iterator): array
852    {
853        if (method_exists($iterator, 'toArray')) {
854            $array = $iterator->toArray();
855        } else {
856            $array = iterator_to_array($iterator);
857        }
858        return $array;
859    }
860
861    /**
862     * Converts a given object recursively into an array.
863     *
864     * @param \TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject $object
865     * @return array
866     * @todo Refactor this into convertDomainObjectsToIdentityArrays()
867     * @internal only to be used within Extbase, not part of TYPO3 Core API.
868     */
869    public function convertTransientObjectToArray(AbstractDomainObject $object): array
870    {
871        $result = [];
872        foreach ($object->_getProperties() as $propertyName => $propertyValue) {
873            if ($propertyValue instanceof \Iterator) {
874                $propertyValue = $this->convertIteratorToArray($propertyValue);
875            }
876            if ($propertyValue instanceof AbstractDomainObject) {
877                if ($propertyValue->getUid() !== null) {
878                    $result[$propertyName] = $propertyValue->getUid();
879                } else {
880                    $result[$propertyName] = $this->convertTransientObjectToArray($propertyValue);
881                }
882            } elseif (is_array($propertyValue)) {
883                $result[$propertyName] = $this->convertDomainObjectsToIdentityArrays($propertyValue);
884            } else {
885                $result[$propertyName] = $propertyValue;
886            }
887        }
888        return $result;
889    }
890}
891