Passed
Push — master ( a7bade...f16b47 )
by
unknown
17:39
created

PreviewUriBuilder::buildImmediateActionElement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
dl 0
loc 10
rs 10
c 1
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(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
18
namespace TYPO3\CMS\Backend\Routing;
19
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Http\Uri;
22
use TYPO3\CMS\Core\Page\PageRenderer;
23
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26
/**
27
 * Substitution for `BackendUtility::viewOnClick` and `BackendUtility::getPreviewUrl`.
28
 * Internally `BackendUtility::getPreviewUrl` is still called due to hooks being invoked
29
 * there - in the future it basically aims to be a replacement for mentioned function.
30
 */
31
class PreviewUriBuilder
32
{
33
    public const OPTION_SWITCH_FOCUS = 'switchFocus';
34
    public const OPTION_WINDOW_NAME = 'windowName';
35
    public const OPTION_WINDOW_FEATURES = 'windowFeatures';
36
    public const OPTION_WINDOW_SCOPE = 'windowScope';
37
38
    public const OPTION_WINDOW_SCOPE_LOCAL = 'local';
39
    public const OPTION_WINDOW_SCOPE_GLOBAL = 'global';
40
41
    /**
42
     * @var int
43
     */
44
    protected $pageId;
45
46
    /**
47
     * @var string|null
48
     */
49
    protected $alternativeUri;
50
51
    /**
52
     * @var array|null
53
     */
54
    protected $rootLine;
55
56
    /**
57
     * @var string|null
58
     */
59
    protected $section;
60
61
    /**
62
     * @var string|null
63
     */
64
    protected $additionalQueryParameters;
65
66
    /**
67
     * @var string|null
68
     * @internal Not used, kept for potential compatibility issues
69
     */
70
    protected $backPath;
71
72
    /**
73
     * @var bool
74
     */
75
    protected $moduleLoading = true;
76
77
    /**
78
     * @param int $pageId Page ID to be previewed
79
     * @param string|null $alternativeUri Alternative URL to be used instead of `/index.php?id=`
80
     * @return static
81
     */
82
    public static function create(int $pageId, string $alternativeUri = null): self
83
    {
84
        return GeneralUtility::makeInstance(static::class, $pageId, $alternativeUri);
85
    }
86
87
    /**
88
     * @param int $pageId Page ID to be previewed
89
     * @param string|null $alternativeUri Alternative URL to be used instead of `/index.php?id=`
90
     */
91
    public function __construct(int $pageId, string $alternativeUri = null)
92
    {
93
        $this->pageId = $pageId;
94
        $this->alternativeUri = $alternativeUri;
95
    }
96
97
    /**
98
     * @param bool $moduleLoading whether to enable JavaScript module loading
99
     * @return static
100
     */
101
    public function withModuleLoading(bool $moduleLoading): self
102
    {
103
        if ($this->moduleLoading === $moduleLoading) {
104
            return $this;
105
        }
106
        $target = clone $this;
107
        $target->moduleLoading = $moduleLoading;
108
        return $target;
109
    }
110
111
    /**
112
     * @param array $rootLine (alternative) root-line of pages
113
     * @return static
114
     */
115
    public function withRootLine(array $rootLine): self
116
    {
117
        if ($this->rootLine === $rootLine) {
118
            return $this;
119
        }
120
        $target = clone $this;
121
        $target->rootLine = $rootLine;
122
        return $this;
123
    }
124
125
    /**
126
     * @param string $section particular section (anchor element)
127
     * @return static
128
     */
129
    public function withSection(string $section): self
130
    {
131
        if ($this->section === $section) {
132
            return $this;
133
        }
134
        $target = clone $this;
135
        $target->section = $section;
136
        return $target;
137
    }
138
139
    /**
140
     * @param string $additionalQueryParameters additional URI query parameters
141
     * @return static
142
     */
143
    public function withAdditionalQueryParameters(string $additionalQueryParameters): self
144
    {
145
        if ($this->additionalQueryParameters === $additionalQueryParameters) {
146
            return $this;
147
        }
148
        $target = clone $this;
149
        $target->additionalQueryParameters = $additionalQueryParameters;
150
        return $target;
151
    }
152
153
    /**
154
     * Builds preview URI (still using `BackendUtility::getPreviewUrl`).
155
     *
156
     * @param array|null $options
157
     * @return Uri|null
158
     */
159
    public function buildUri(array $options = null): ?Uri
160
    {
161
        try {
162
            $options = $this->enrichOptions($options);
163
            $switchFocus = $options[self::OPTION_SWITCH_FOCUS] ?? true;
164
            $uriString = BackendUtility::getPreviewUrl(
165
                $this->pageId,
166
                $this->backPath ?? '',
167
                $this->rootLine,
168
                $this->section ?? '',
169
                $this->alternativeUri ?? '',
170
                $this->additionalQueryParameters ?? '',
171
                $switchFocus
172
            );
173
            return GeneralUtility::makeInstance(Uri::class, $uriString);
174
        } catch (UnableToLinkToPageException $exception) {
175
            return null;
176
        }
177
    }
178
179
    /**
180
     * Builds attributes array (e.g. `['dispatch-action' => ...]`).
181
     * CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
182
     *
183
     * @param array|null $options
184
     * @return array|null
185
     */
186
    public function buildDispatcherDataAttributes(array $options = null): ?array
187
    {
188
        if (null === ($attributes = $this->buildAttributes($options))) {
189
            return null;
190
        }
191
        $this->loadActionDispatcher();
192
        return $this->prefixAttributeNames('dispatch-', $attributes);
193
    }
194
195
    /**
196
     * Builds attributes array (e.g. `['data-dispatch-action' => ...]`).
197
     * CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
198
     *
199
     * @param array|null $options
200
     * @return array|null
201
     */
202
    public function buildDispatcherAttributes(array $options = null): ?array
203
    {
204
        if (null === ($attributes = $this->buildAttributes($options))) {
205
            return null;
206
        }
207
        $this->loadActionDispatcher();
208
        return $this->prefixAttributeNames('data-dispatch-', $attributes);
209
    }
210
211
    /**
212
     * Serialized attributes are processed with `htmlspecialchars` and ready to be used.
213
     *
214
     * @param array|null $options
215
     * @return string|null
216
     */
217
    public function serializeDispatcherAttributes(array $options = null): ?string
218
    {
219
        if (null === ($attributes = $this->buildDispatcherAttributes($options))) {
220
            return null;
221
        }
222
        return ' ' . GeneralUtility::implodeAttributes($attributes, true);
223
    }
224
225
    /**
226
     * `<typo3-immediate-action>` does not have a specific meaning and is used to
227
     * expose `data` attributes, see custom element in `ImmediateActionElement.ts`.
228
     *
229
     * @param array|null $options
230
     * @return string|null
231
     */
232
    public function buildImmediateActionElement(array $options = null): ?string
233
    {
234
        if (null === ($attributes = $this->buildAttributes($options))) {
235
            return null;
236
        }
237
        $this->loadImmediateActionElement();
238
        return sprintf(
239
            // `<typo3-immediate-action action="TYPO3.WindowManager.localOpen" args="[...]">`
240
            '<typo3-immediate-action %s></typo3-immediate-action>',
241
            GeneralUtility::implodeAttributes($attributes, true)
242
        );
243
    }
244
245
    protected function buildAttributes(array $options = null): ?array
246
    {
247
        $options = $this->enrichOptions($options);
248
        if (null === ($uri = $this->buildUri($options))) {
249
            return null;
250
        }
251
        $args = [
252
            // target URI
253
            (string)$uri,
254
            // whether to switch focus to that window
255
            $options[self::OPTION_SWITCH_FOCUS],
256
            // name of the window instance for JavaScript references
257
            $options[self::OPTION_WINDOW_NAME],
258
        ];
259
        if (isset($options[self::OPTION_WINDOW_FEATURES])) {
260
            // optional window features (e.g. 'width=500,height=300')
261
            $args[] = $options[self::OPTION_WINDOW_FEATURES];
262
        }
263
        return [
264
            'action' => $options[self::OPTION_WINDOW_SCOPE] === self::OPTION_WINDOW_SCOPE_GLOBAL
265
                ? 'TYPO3.WindowManager.globalOpen'
266
                : 'TYPO3.WindowManager.localOpen',
267
            'args' => json_encode($args),
268
        ];
269
    }
270
271
    /**
272
     * Handles options to used for opening preview URI in a new window/tab.
273
     * + `switchFocus` (bool): whether to focus new window in browser
274
     * + `windowName` (string): name of window for internal reference
275
     * + `windowScope` (string): `local` (current document) `global` (whole backend)
276
     *
277
     * @param array|null $options
278
     * @return array
279
     */
280
    protected function enrichOptions(array $options = null): array
281
    {
282
        return array_merge(
283
            [
284
                self::OPTION_SWITCH_FOCUS => null,
285
                // 'newTYPO3frontendWindow' was used in BackendUtility::viewOnClick
286
                self::OPTION_WINDOW_NAME => 'newTYPO3frontendWindow',
287
                self::OPTION_WINDOW_SCOPE => self::OPTION_WINDOW_SCOPE_LOCAL,
288
            ],
289
            $options ?? []
290
        );
291
    }
292
293
    protected function loadActionDispatcher(): void
294
    {
295
        if (!$this->moduleLoading) {
296
            return;
297
        }
298
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
299
        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ActionDispatcher');
300
    }
301
302
    protected function loadImmediateActionElement(): void
303
    {
304
        if (!$this->moduleLoading) {
305
            return;
306
        }
307
        $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
308
        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Element/ImmediateActionElement');
309
    }
310
311
    protected function prefixAttributeNames(string $prefix, array $attributes): array
312
    {
313
        $attributeNames = array_map(
314
            function (string $name) use ($prefix): string {
315
                return $prefix . $name;
316
            },
317
            array_keys($attributes)
318
        );
319
        return array_combine(
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($at...ay_values($attributes)) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
320
            $attributeNames,
321
            array_values($attributes)
322
        );
323
    }
324
}
325