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