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