489a7d3b16071d268d34ba6c478bd3053a0b4174
[Packages/TYPO3.CMS.git] / typo3 / sysext / version / Classes / Hook / PreviewHook.php
1 <?php
2 namespace TYPO3\CMS\Version\Hook;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
20 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
21 use TYPO3\CMS\Core\Type\Bitmask\Permission;
22 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\MathUtility;
25
26 /**
27 * Hook for checking if the preview mode is activated
28 * preview mode = show a page of a workspace without having to log in
29 */
30 class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
31 {
32 /**
33 * the GET parameter to be used
34 *
35 * @var string
36 */
37 protected $previewKey = 'ADMCMD_prev';
38
39 /**
40 * instance of the \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController object
41 *
42 * @var \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
43 */
44 protected $tsfeObj;
45
46 /**
47 * preview configuration
48 *
49 * @var array
50 */
51 protected $previewConfiguration = false;
52
53 /**
54 * Defines whether to force read permissions on pages.
55 *
56 * @var bool
57 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
58 */
59 protected $forceReadPermissions = false;
60
61 /**
62 * hook to check if the preview is activated
63 * right now, this hook is called at the end of "$TSFE->connectToDB"
64 *
65 * @param array $params (not needed right now)
66 * @param \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $pObj
67 * @return void
68 */
69 public function checkForPreview($params, &$pObj)
70 {
71 $this->tsfeObj = $pObj;
72 $this->previewConfiguration = $this->getPreviewConfiguration();
73 if (is_array($this->previewConfiguration)) {
74 // In case of a keyword-authenticated preview,
75 // re-initialize the TSFE object:
76 // because the GET variables are taken from the preview
77 // configuration
78 $this->tsfeObj = GeneralUtility::makeInstance(
79 \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class,
80 $GLOBALS['TYPO3_CONF_VARS'],
81 GeneralUtility::_GP('id'),
82 GeneralUtility::_GP('type'),
83 GeneralUtility::_GP('no_cache'),
84 GeneralUtility::_GP('cHash'),
85 null,
86 GeneralUtility::_GP('MP'),
87 GeneralUtility::_GP('RDCT')
88 );
89 $GLOBALS['TSFE'] = $this->tsfeObj;
90 // Configuration after initialization of TSFE object.
91 // Basically this unsets the BE cookie if any and forces
92 // the BE user set according to the preview configuration.
93 // @previouslyknownas TSFE->ADMCMD_preview_postInit
94 // Clear cookies:
95 unset($_COOKIE['be_typo_user']);
96 }
97 }
98
99 /**
100 * hook after the regular BE user has been initialized
101 * if there is no BE user login, but a preview configuration
102 * the BE user of the preview configuration gets initialized
103 *
104 * @param array $params holding the BE_USER object
105 * @param \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $pObj
106 * @return void
107 */
108 public function initializePreviewUser(&$params, &$pObj)
109 {
110 // if there is a valid BE user, and the full workspace should be previewed, the workspacePreview option should be set
111 $workspaceUid = $this->previewConfiguration['fullWorkspace'];
112 $workspaceRecord = null;
113 if ((is_null($params['BE_USER']) || $params['BE_USER'] === false) && $this->previewConfiguration !== false && $this->previewConfiguration['BEUSER_uid'] > 0) {
114 // First initialize a temp user object and resolve usergroup information
115 /** @var FrontendBackendUserAuthentication $tempBackendUser */
116 $tempBackendUser = $this->createFrontendBackendUser();
117 $tempBackendUser->userTS_dontGetCached = 1;
118 $tempBackendUser->setBeUserByUid($this->previewConfiguration['BEUSER_uid']);
119 if ($tempBackendUser->user['uid']) {
120 $tempBackendUser->unpack_uc('');
121 $tempBackendUser->fetchGroupData();
122 // Handle degradation of admin users
123 if ($tempBackendUser->isAdmin() && ExtensionManagementUtility::isLoaded('workspaces')) {
124 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
125 ->getQueryBuilderForTable('sys_workspace');
126
127 $queryBuilder->getRestrictions()
128 ->removeAll()
129 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
130 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
131
132 $workspaceRecord = $queryBuilder
133 ->select('uid', 'adminusers', 'reviewers', 'members', 'db_mountpoints')
134 ->from('sys_workspace')
135 ->where($queryBuilder->expr()->eq('uid', (int)$workspaceUid))
136 ->execute()
137 ->fetch();
138
139 // Either use configured workspace mount or current page id, if admin user does not have any page mounts
140 if (empty($tempBackendUser->groupData['webmounts'])) {
141 $tempBackendUser->groupData['webmounts'] = !empty($workspaceRecord['db_mountpoints']) ? $workspaceRecord['db_mountpoints'] : $pObj->id;
142 }
143 // Force add degraded admin user as member of this workspace
144 $workspaceRecord['members'] = 'be_users_' . $this->previewConfiguration['BEUSER_uid'];
145 // Force read permission for degraded admin user
146 $this->forceReadPermissions = true;
147 }
148 // Store only needed information in the real simulate backend
149 $BE_USER = $this->createFrontendBackendUser();
150 $BE_USER->userTS_dontGetCached = 1;
151 $BE_USER->user = $tempBackendUser->user;
152 $BE_USER->user['admin'] = 0;
153 $BE_USER->groupData['webmounts'] = $tempBackendUser->groupData['webmounts'];
154 $BE_USER->groupList = $tempBackendUser->groupList;
155 $BE_USER->userGroups = $tempBackendUser->userGroups;
156 $BE_USER->userGroupsUID = $tempBackendUser->userGroupsUID;
157 $pObj->beUserLogin = true;
158 } else {
159 $BE_USER = null;
160 $pObj->beUserLogin = false;
161 }
162 unset($tempBackendUser);
163 $params['BE_USER'] = $BE_USER;
164 }
165 if ($pObj->beUserLogin
166 && is_object($params['BE_USER'])
167 && MathUtility::canBeInterpretedAsInteger($workspaceUid)
168 ) {
169 if ($workspaceUid == 0
170 || $workspaceUid >= -1
171 && $params['BE_USER']->checkWorkspace($workspaceRecord ?: $workspaceUid)
172 && $params['BE_USER']->isInWebMount($pObj->id)
173 ) {
174 // Check Access to workspace. Live (0) is OK to preview for all.
175 $pObj->workspacePreview = (int)$workspaceUid;
176 } else {
177 // No preview, will default to "Live" at the moment
178 $pObj->workspacePreview = -99;
179 }
180 }
181 }
182
183 /**
184 * Overrides the page permission clause in case an admin
185 * user has been degraded to a regular user without any user
186 * group assignments. This method is used as hook callback.
187 *
188 * @param array $parameters
189 * @return string
190 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
191 */
192 public function overridePagePermissionClause(array $parameters)
193 {
194 $clause = $parameters['currentClause'];
195 if ($parameters['perms'] & 1 && $this->forceReadPermissions) {
196 $clause = ' 1=1';
197 }
198 return $clause;
199 }
200
201 /**
202 * Overrides the row permission value in case an admin
203 * user has been degraded to a regular user without any user
204 * group assignments. This method is used as hook callback.
205 *
206 * @param array $parameters
207 * @return int
208 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::calcPerms
209 */
210 public function overridePermissionCalculation(array $parameters)
211 {
212 $permissions = $parameters['outputPermissions'];
213 if (!($permissions & Permission::PAGE_SHOW) && $this->forceReadPermissions) {
214 $permissions |= Permission::PAGE_SHOW;
215 }
216 return $permissions;
217 }
218
219 /**
220 * Looking for an ADMCMD_prev code, looks it up if found and returns configuration data.
221 * Background: From the backend a request to the frontend to show a page, possibly with
222 * workspace preview can be "recorded" and associated with a keyword.
223 * When the frontend is requested with this keyword the associated request parameters are
224 * restored from the database AND the backend user is loaded - only for that request.
225 * The main point is that a special URL valid for a limited time,
226 * eg. http://localhost/typo3site/index.php?ADMCMD_prev=035d9bf938bd23cb657735f68a8cedbf will
227 * open up for a preview that doesn't require login. Thus it's useful for sending in an email
228 * to someone without backend account.
229 * This can also be used to generate previews of hidden pages, start/endtimes, usergroups and
230 * those other settings from the Admin Panel - just not implemented yet.
231 *
232 * @throws \Exception
233 * @return array Preview configuration array from sys_preview record.
234 */
235 public function getPreviewConfiguration()
236 {
237 $inputCode = $this->getPreviewInputCode();
238 // If input code is available and shall not be ignored, look up the settings
239 if ($inputCode && $inputCode !== 'IGNORE') {
240 // "log out"
241 if ($inputCode == 'LOGOUT') {
242 setcookie($this->previewKey, '', 0, GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
243 if ($this->tsfeObj->TYPO3_CONF_VARS['FE']['workspacePreviewLogoutTemplate']) {
244 $templateFile = PATH_site . $this->tsfeObj->TYPO3_CONF_VARS['FE']['workspacePreviewLogoutTemplate'];
245 if (@is_file($templateFile)) {
246 $message = file_get_contents($templateFile);
247 } else {
248 $message = '<strong>ERROR!</strong><br>Template File "'
249 . $this->tsfeObj->TYPO3_CONF_VARS['FE']['workspacePreviewLogoutTemplate']
250 . '" configured with $TYPO3_CONF_VARS["FE"]["workspacePreviewLogoutTemplate"] not found. Please contact webmaster about this problem.';
251 }
252 } else {
253 $message = 'You logged out from Workspace preview mode. Click this link to <a href="%1$s">go back to the website</a>';
254 }
255 $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GET('returnUrl'));
256 die(sprintf($message, htmlspecialchars(preg_replace('/\\&?' . $this->previewKey . '=[[:alnum:]]+/', '', $returnUrl))));
257 }
258 // Look for keyword configuration record:
259 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
260 ->getQueryBuilderForTable('sys_preview');
261
262 $previewData = $queryBuilder
263 ->select('*')
264 ->from('sys_preview')
265 ->where(
266 $queryBuilder->expr()->eq('keyword', $queryBuilder->createNamedParameter($inputCode)),
267 $queryBuilder->expr()->gt('endtime', (int)$GLOBALS['EXEC_TIME'])
268 )
269 ->setMaxResults(1)
270 ->execute()
271 ->fetch();
272
273 // Get: Backend login status, Frontend login status
274 // - Make sure to remove fe/be cookies (temporarily);
275 // BE already done in ADMCMD_preview_postInit()
276 if (is_array($previewData)) {
277 if (empty(GeneralUtility::_POST())) {
278 // Unserialize configuration:
279 $previewConfig = unserialize($previewData['config']);
280 // For full workspace preview we only ADD a get variable
281 // to set the preview of the workspace - so all other Get
282 // vars are accepted. Hope this is not a security problem.
283 // Still posting is not allowed and even if a backend user
284 // get initialized it shouldn't lead to situations where
285 // users can use those credentials.
286 if ($previewConfig['fullWorkspace']) {
287 // Set the workspace preview value:
288 GeneralUtility::_GETset($previewConfig['fullWorkspace'], 'ADMCMD_previewWS');
289 // If ADMCMD_prev is set the $inputCode value cannot come
290 // from a cookie and we set that cookie here. Next time it will
291 // be found from the cookie if ADMCMD_prev is not set again...
292 if (GeneralUtility::_GP($this->previewKey)) {
293 // Lifetime is 1 hour, does it matter much?
294 // Requires the user to click the link from their email again if it expires.
295 setcookie($this->previewKey, GeneralUtility::_GP($this->previewKey), 0, GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
296 }
297 return $previewConfig;
298 } elseif (GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'index.php?' . $this->previewKey . '=' . $inputCode === GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) {
299 // Set GET variables
300 $GET_VARS = '';
301 parse_str($previewConfig['getVars'], $GET_VARS);
302 GeneralUtility::_GETset($GET_VARS);
303 // Return preview keyword configuration
304 return $previewConfig;
305 } else {
306 // This check is to prevent people from setting additional
307 // GET vars via realurl or other URL path based ways of passing parameters.
308 throw new \Exception(htmlspecialchars('Request URL did not match "'
309 . GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'index.php?' . $this->previewKey . '='
310 . $inputCode . '"', 1294585190));
311 }
312 } else {
313 throw new \Exception('POST requests are incompatible with keyword preview.', 1294585191);
314 }
315 } else {
316 throw new \Exception('ADMCMD command could not be executed! (No keyword configuration found)', 1294585192);
317 }
318 }
319 return false;
320 }
321
322 /**
323 * returns the input code value from the admin command variable
324 *
325 * @return string Input code
326 */
327 protected function getPreviewInputCode()
328 {
329 $inputCode = GeneralUtility::_GP($this->previewKey);
330 // If no inputcode and a cookie is set, load input code from cookie:
331 if (!$inputCode && $_COOKIE[$this->previewKey]) {
332 $inputCode = $_COOKIE[$this->previewKey];
333 }
334 return $inputCode;
335 }
336
337 /**
338 * Set preview keyword, eg:
339 * $previewUrl = GeneralUtility::getIndpEnv('TYPO3_SITE_URL').'index.php?ADMCMD_prev='.$this->compilePreviewKeyword('id='.$pageId.'&L='.$language.'&ADMCMD_view=1&ADMCMD_editIcons=1&ADMCMD_previewWS='.$this->workspace, $GLOBALS['BE_USER']->user['uid'], 120);
340 *
341 * @todo for sys_preview:
342 * - Add a comment which can be shown to previewer in frontend in some way (plus maybe ability to write back, take other action?)
343 * - Add possibility for the preview keyword to work in the backend as well: So it becomes a quick way to a certain action of sorts?
344 *
345 * @param string $getVarsStr Get variables to preview, eg. 'id=1150&L=0&ADMCMD_view=1&ADMCMD_editIcons=1&ADMCMD_previewWS=8'
346 * @param string $backendUserUid 32 byte MD5 hash keyword for the URL: "?ADMCMD_prev=[keyword]
347 * @param int $ttl Time-To-Live for keyword
348 * @param int|NULL $fullWorkspace Which workspace to preview. Workspace UID, -1 or >0. If set, the getVars is ignored in the frontend, so that string can be empty
349 * @return string Returns keyword to use in URL for ADMCMD_prev=
350 */
351 public function compilePreviewKeyword($getVarsStr, $backendUserUid, $ttl = 172800, $fullWorkspace = null)
352 {
353 $fieldData = array(
354 'keyword' => md5(uniqid(microtime(), true)),
355 'tstamp' => $GLOBALS['EXEC_TIME'],
356 'endtime' => $GLOBALS['EXEC_TIME'] + $ttl,
357 'config' => serialize(array(
358 'fullWorkspace' => $fullWorkspace,
359 'getVars' => $getVarsStr,
360 'BEUSER_uid' => $backendUserUid
361 ))
362 );
363 GeneralUtility::makeInstance(ConnectionPool::class)
364 ->getConnectionForTable('sys_preview')
365 ->insert(
366 'sys_preview',
367 $fieldData
368 );
369
370 return $fieldData['keyword'];
371 }
372
373 /**
374 * easy function to just return the number of hours
375 * a preview link is valid, based on the TSconfig value "options.workspaces.previewLinkTTLHours"
376 * by default, it's 48hs
377 *
378 * @return int The hours as a number
379 */
380 public function getPreviewLinkLifetime()
381 {
382 $ttlHours = (int)$GLOBALS['BE_USER']->getTSConfigVal('options.workspaces.previewLinkTTLHours');
383 return $ttlHours ? $ttlHours : 24 * 2;
384 }
385
386 /**
387 * @return FrontendBackendUserAuthentication
388 */
389 protected function createFrontendBackendUser()
390 {
391 return GeneralUtility::makeInstance(
392 FrontendBackendUserAuthentication::class
393 );
394 }
395 }