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

PreviewUriBuilder::getLivePageUid()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
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\Workspaces\Preview;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Http\Message\UriInterface;
22
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
23
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder as BackendPreviewUriBuilder;
24
use TYPO3\CMS\Backend\Routing\UriBuilder;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27
use TYPO3\CMS\Core\Database\ConnectionPool;
28
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
29
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
30
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
31
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
32
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
33
use TYPO3\CMS\Core\Site\SiteFinder;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Core\Utility\StringUtility;
36
use TYPO3\CMS\Workspaces\Event\RetrievedPreviewUrlEvent;
37
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
38
39
/**
40
 * Create links to pages when in a workspace for previewing purposes
41
 *
42
 * @internal
43
 */
44
class PreviewUriBuilder
45
{
46
    /**
47
     * @var array
48
     */
49
    protected $pageCache = [];
50
51
    /**
52
     * @var WorkspaceService
53
     */
54
    protected $workspaceService;
55
56
    /**
57
     * @var EventDispatcherInterface
58
     */
59
    protected $eventDispatcher;
60
61
    /**
62
     * @var int
63
     */
64
    protected $previewLinkLifetime;
65
66
    public function __construct(EventDispatcherInterface $eventDispatcher, WorkspaceService $workspaceService)
67
    {
68
        $this->eventDispatcher = $eventDispatcher;
69
        $this->workspaceService = $workspaceService;
70
        $this->previewLinkLifetime = $this->workspaceService->getPreviewLinkLifetime();
71
    }
72
73
    /**
74
     * Generates a workspace preview link.
75
     *
76
     * @param int $uid The ID of the record to be linked
77
     * @param int $languageId the language to link to
78
     * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
79
     */
80
    public function buildUriForPage(int $uid, int $languageId = 0): string
81
    {
82
        $previewKeyword = $this->compilePreviewKeyword(
83
            $this->previewLinkLifetime * 3600,
84
            $this->workspaceService->getCurrentWorkspace()
85
        );
86
87
        $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
88
        try {
89
            $site = $siteFinder->getSiteByPageId($uid);
90
            try {
91
                $language = $site->getLanguageById($languageId);
92
            } catch (\InvalidArgumentException $e) {
93
                $language = $site->getDefaultLanguage();
94
            }
95
            $uri = $site->getRouter()->generateUri($uid, ['ADMCMD_prev' => $previewKeyword, '_language' => $language], '');
96
            return (string)$uri;
97
        } catch (SiteNotFoundException | InvalidRouteArgumentsException $e) {
98
            throw new UnableToLinkToPageException('The page ' . $uid . ' had no proper connection to a site, no link could be built.', 1559794916);
99
        }
100
    }
101
102
    /**
103
     * Generate workspace preview links for all available languages of a page
104
     *
105
     * @param int $pageId
106
     * @return array
107
     */
108
    public function buildUrisForAllLanguagesOfPage(int $pageId): array
109
    {
110
        $previewLanguages = $this->getAvailableLanguages($pageId);
111
        $previewLinks = [];
112
113
        foreach ($previewLanguages as $languageUid => $language) {
114
            $previewLinks[$language] = $this->buildUriForPage($pageId, $languageUid);
115
        }
116
117
        return $previewLinks;
118
    }
119
120
    /**
121
     * Generates a workspace split-bar preview link.
122
     *
123
     * @param int $uid The ID of the record to be linked
124
     * @return UriInterface
125
     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
126
     */
127
    public function buildUriForWorkspaceSplitPreview(int $uid): UriInterface
128
    {
129
        // In case a $pageUid is submitted we need to make sure it points to a live-page
130
        if ($uid > 0) {
131
            $uid = $this->getLivePageUid($uid);
132
        }
133
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
134
        return $uriBuilder->buildUriFromRoute(
135
            'workspace_previewcontrols',
136
            ['id' => $uid],
137
            UriBuilder::ABSOLUTE_URL
138
        );
139
    }
140
141
    /**
142
     * Generates a view URI for an element.
143
     *
144
     * @param string $table Table to be used
145
     * @param int $uid Uid of the version(!) record
146
     * @param array $liveRecord Optional live record data
147
     * @param array $versionRecord Optional version record data
148
     * @return string
149
     */
150
    public function buildUriForElement(string $table, int $uid, array $liveRecord = null, array $versionRecord = null): string
151
    {
152
        $previewUri = $this->createPreviewUriForElement($table, $uid, $liveRecord, $versionRecord);
153
        $event = new RetrievedPreviewUrlEvent($table, $uid, $previewUri, [
154
            'liveRecord' => $liveRecord,
155
            'versionRecord' => $versionRecord,
156
        ]);
157
        /** @var RetrievedPreviewUrlEvent $event */
158
        $event = $this->eventDispatcher->dispatch($event);
159
        $previewUri = (string)($event->getPreviewUri() ?? '');
160
        return $previewUri;
161
    }
162
163
    protected function createPreviewUriForElement(string $table, int $uid, array $liveRecord = null, array $versionRecord = null): ?UriInterface
164
    {
165
        if ($table === 'pages') {
166
            return BackendPreviewUriBuilder::create((int)BackendUtility::getLiveVersionIdOfRecord('pages', $uid))
167
                ->buildUri();
168
        }
169
170
        if ($liveRecord === null) {
171
            $liveRecord = BackendUtility::getLiveVersionOfRecord($table, $uid);
172
        }
173
        if ($versionRecord === null) {
174
            $versionRecord = BackendUtility::getRecord($table, $uid);
175
        }
176
177
        // Directly use pid value and consider move placeholders
178
        $previewPageId = (empty($versionRecord['pid']) ? $liveRecord['pid'] : $versionRecord['pid']);
179
        $additionalParameters = '&previewWS=' . $versionRecord['t3ver_wsid'];
180
        // Add language parameter if record is a localization
181
        if (BackendUtility::isTableLocalizable($table)) {
182
            $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
183
            if ($versionRecord[$languageField] > 0) {
184
                $additionalParameters .= '&L=' . $versionRecord[$languageField];
185
            }
186
        }
187
188
        $pageTsConfig = BackendUtility::getPagesTSconfig($previewPageId);
189
        // Directly use determined direct page id
190
        if ($table === 'tt_content') {
191
            return BackendPreviewUriBuilder::create($previewPageId)
192
                ->withAdditionalQueryParameters($additionalParameters)
193
                ->buildUri();
194
        }
195
        if (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table]) || !empty($pageTsConfig['options.']['workspaces.']['previewPageId'])) {
196
            // Analyze Page TSconfig options.workspaces.previewPageId
197
            if (!empty($pageTsConfig['options.']['workspaces.']['previewPageId.'][$table])) {
198
                $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId.'][$table];
199
            } else {
200
                $previewConfiguration = $pageTsConfig['options.']['workspaces.']['previewPageId'];
201
            }
202
            // Extract possible settings (e.g. "field:pid")
203
            [$previewKey, $previewValue] = explode(':', $previewConfiguration, 2);
204
            if ($previewKey === 'field') {
205
                $previewPageId = (int)$liveRecord[$previewValue];
206
            } else {
207
                $previewPageId = (int)$previewConfiguration;
208
            }
209
            return BackendPreviewUriBuilder::create($previewPageId)
210
                ->withAdditionalQueryParameters($additionalParameters)
211
                ->buildUri();
212
        }
213
        return null;
214
    }
215
216
    /**
217
     * Adds an entry to the sys_preview database table and return the preview keyword.
218
     *
219
     * @param int $ttl Time-To-Live for keyword
220
     * @param int|null $workspaceId Which workspace ID to preview.
221
     * @return string Returns keyword to use in URL for ADMCMD_prev=, a 32 byte MD5 hash keyword for the URL: "?ADMCMD_prev=[keyword]
222
     */
223
    protected function compilePreviewKeyword(int $ttl = 172800, int $workspaceId = null): string
224
    {
225
        $keyword = md5(StringUtility::getUniqueId());
226
        GeneralUtility::makeInstance(ConnectionPool::class)
227
            ->getConnectionForTable('sys_preview')
228
            ->insert(
229
                'sys_preview',
230
                [
231
                    'keyword' => $keyword,
232
                    'tstamp' => $GLOBALS['EXEC_TIME'],
233
                    'endtime' => $GLOBALS['EXEC_TIME'] + $ttl,
234
                    'config' => json_encode([
235
                        'fullWorkspace' => $workspaceId
236
                    ])
237
                ]
238
            );
239
240
        return $keyword;
241
    }
242
243
    /**
244
     * Find the Live-Uid for a given page,
245
     * the results are cached at run-time to avoid too many database-queries
246
     *
247
     * @throws \InvalidArgumentException
248
     * @param int $uid
249
     * @return int
250
     */
251
    protected function getLivePageUid(int $uid): int
252
    {
253
        if (!isset($this->pageCache[$uid])) {
254
            $pageRecord = BackendUtility::getRecord('pages', $uid);
255
            if (is_array($pageRecord)) {
256
                $this->pageCache[$uid] = $pageRecord['t3ver_oid'] ? (int)$pageRecord['t3ver_oid'] : $uid;
257
            } else {
258
                throw new \InvalidArgumentException('uid is supposed to point to an existing page - given value was: ' . $uid, 1290628113);
259
            }
260
        }
261
        return $this->pageCache[$uid];
262
    }
263
264
    /**
265
     * Get the available languages of a certain page, including language=0 if the user has access to it.
266
     *
267
     * @param int $pageId
268
     * @return array assoc array with the languageId as key and the languageTitle as value
269
     */
270
    protected function getAvailableLanguages(int $pageId): array
271
    {
272
        $languageOptions = [];
273
        $translationConfigurationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
274
        $systemLanguages = $translationConfigurationProvider->getSystemLanguages($pageId);
275
276
        if ($this->getBackendUser()->checkLanguageAccess(0)) {
277
            // Use configured label for default language
278
            $languageOptions[0] = $systemLanguages[0]['title'];
279
        }
280
281
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
282
            ->getQueryBuilderForTable('pages');
283
        $queryBuilder->getRestrictions()
284
            ->removeAll()
285
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
286
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->getBackendUser()->workspace));
287
288
        $result = $queryBuilder->select('sys_language_uid')
289
            ->from('pages')
290
            ->where(
291
                $queryBuilder->expr()->eq(
292
                    $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
293
                    $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
294
                )
295
            )
296
            ->execute();
297
298
        while ($row = $result->fetch()) {
299
            $languageId = (int)$row['sys_language_uid'];
300
            // Only add links to active languages the user has access to
301
            if (isset($systemLanguages[$languageId]) && $this->getBackendUser()->checkLanguageAccess($languageId)) {
302
                $languageOptions[$languageId] = $systemLanguages[$languageId]['title'];
303
            }
304
        }
305
306
        return $languageOptions;
307
    }
308
309
    /**
310
     * @return BackendUserAuthentication
311
     */
312
    protected function getBackendUser(): BackendUserAuthentication
313
    {
314
        return $GLOBALS['BE_USER'];
315
    }
316
}
317