2 namespace TYPO3\CMS\Workspaces\Hook
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
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\GeneralUtility
;
23 use TYPO3\CMS\Core\Utility\MathUtility
;
26 * Hook for checking if the preview mode is activated
27 * preview mode = show a page of a workspace without having to log in
29 class PreviewHook
implements \TYPO3\CMS\Core\SingletonInterface
32 * the GET parameter to be used
36 protected $previewKey = 'ADMCMD_prev';
39 * instance of the \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController object
41 * @var \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
46 * preview configuration
50 protected $previewConfiguration = false
;
53 * Defines whether to force read permissions on pages.
56 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
58 protected $forceReadPermissions = false
;
61 * hook to check if the preview is activated
62 * right now, this hook is called at the end of "$TSFE->connectToDB"
64 * @param array $params (not needed right now)
65 * @param \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $pObj
67 public function checkForPreview($params, &$pObj)
69 $this->tsfeObj
= $pObj;
70 $this->previewConfiguration
= $this->getPreviewConfiguration();
71 if (is_array($this->previewConfiguration
)) {
72 // In case of a keyword-authenticated preview,
73 // re-initialize the TSFE object:
74 // because the GET variables are taken from the preview
76 $this->tsfeObj
= GeneralUtility
::makeInstance(
77 \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
::class,
79 GeneralUtility
::_GP('id'),
80 GeneralUtility
::_GP('type'),
81 GeneralUtility
::_GP('no_cache'),
82 GeneralUtility
::_GP('cHash'),
84 GeneralUtility
::_GP('MP'),
85 GeneralUtility
::_GP('RDCT')
87 $GLOBALS['TSFE'] = $this->tsfeObj
;
88 // Configuration after initialization of TSFE object.
89 // Basically this unsets the BE cookie if any and forces
90 // the BE user set according to the preview configuration.
91 // @previouslyknownas TSFE->ADMCMD_preview_postInit
93 unset($_COOKIE['be_typo_user']);
98 * hook after the regular BE user has been initialized
99 * if there is no BE user login, but a preview configuration
100 * the BE user of the preview configuration gets initialized
102 * @param array $params holding the BE_USER object
103 * @param \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $pObj
105 public function initializePreviewUser(&$params, &$pObj)
107 // if there is a valid BE user, and the full workspace should be previewed, the workspacePreview option should be set
108 $workspaceUid = $this->previewConfiguration
['fullWorkspace'];
109 $workspaceRecord = null
;
110 if ((is_null($params['BE_USER']) ||
$params['BE_USER'] === false
) && $this->previewConfiguration
!== false
&& $this->previewConfiguration
['BEUSER_uid'] > 0) {
111 // First initialize a temp user object and resolve usergroup information
112 /** @var FrontendBackendUserAuthentication $tempBackendUser */
113 $tempBackendUser = $this->createFrontendBackendUser();
114 $tempBackendUser->userTS_dontGetCached
= 1;
115 $tempBackendUser->setBeUserByUid($this->previewConfiguration
['BEUSER_uid']);
116 if ($tempBackendUser->user
['uid']) {
117 $tempBackendUser->unpack_uc();
118 $tempBackendUser->fetchGroupData();
119 // Handle degradation of admin users
120 if ($tempBackendUser->isAdmin()) {
121 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)
122 ->getQueryBuilderForTable('sys_workspace');
124 $queryBuilder->getRestrictions()
126 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
127 ->add(GeneralUtility
::makeInstance(RootLevelRestriction
::class));
129 $workspaceRecord = $queryBuilder
130 ->select('uid', 'adminusers', 'reviewers', 'members', 'db_mountpoints')
131 ->from('sys_workspace')
133 $queryBuilder->expr()->eq(
135 $queryBuilder->createNamedParameter($workspaceUid, \PDO
::PARAM_INT
)
141 // Either use configured workspace mount or current page id, if admin user does not have any page mounts
142 if (empty($tempBackendUser->groupData
['webmounts'])) {
143 $tempBackendUser->groupData
['webmounts'] = !empty($workspaceRecord['db_mountpoints']) ?
$workspaceRecord['db_mountpoints'] : $pObj->id
;
145 // Force add degraded admin user as member of this workspace
146 $workspaceRecord['members'] = 'be_users_' . $this->previewConfiguration
['BEUSER_uid'];
147 // Force read permission for degraded admin user
148 $this->forceReadPermissions
= true
;
150 // Store only needed information in the real simulate backend
151 $BE_USER = $this->createFrontendBackendUser();
152 $BE_USER->userTS_dontGetCached
= 1;
153 $BE_USER->user
= $tempBackendUser->user
;
154 $BE_USER->user
['admin'] = 0;
155 $BE_USER->groupData
['webmounts'] = $tempBackendUser->groupData
['webmounts'];
156 $BE_USER->groupList
= $tempBackendUser->groupList
;
157 $BE_USER->userGroups
= $tempBackendUser->userGroups
;
158 $BE_USER->userGroupsUID
= $tempBackendUser->userGroupsUID
;
159 $pObj->beUserLogin
= true
;
162 $pObj->beUserLogin
= false
;
164 unset($tempBackendUser);
165 $params['BE_USER'] = $BE_USER;
167 if ($pObj->beUserLogin
168 && is_object($params['BE_USER'])
169 && MathUtility
::canBeInterpretedAsInteger($workspaceUid)
171 if ($workspaceUid == 0
172 ||
$workspaceUid >= -1
173 && $params['BE_USER']->checkWorkspace($workspaceRecord ?
: $workspaceUid)
174 && $params['BE_USER']->isInWebMount($pObj->id
)
176 // Check Access to workspace. Live (0) is OK to preview for all.
177 $pObj->workspacePreview
= (int)$workspaceUid;
179 // No preview, will default to "Live" at the moment
180 $pObj->workspacePreview
= -99;
186 * Overrides the page permission clause in case an admin
187 * user has been degraded to a regular user without any user
188 * group assignments. This method is used as hook callback.
190 * @param array $parameters
192 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
194 public function overridePagePermissionClause(array $parameters)
196 $clause = $parameters['currentClause'];
197 if ($parameters['perms'] & 1 && $this->forceReadPermissions
) {
204 * Overrides the row permission value in case an admin
205 * user has been degraded to a regular user without any user
206 * group assignments. This method is used as hook callback.
208 * @param array $parameters
210 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::calcPerms
212 public function overridePermissionCalculation(array $parameters)
214 $permissions = $parameters['outputPermissions'];
215 if (!($permissions & Permission
::PAGE_SHOW
) && $this->forceReadPermissions
) {
216 $permissions |
= Permission
::PAGE_SHOW
;
222 * Looking for an ADMCMD_prev code, looks it up if found and returns configuration data.
223 * Background: From the backend a request to the frontend to show a page, possibly with
224 * workspace preview can be "recorded" and associated with a keyword.
225 * When the frontend is requested with this keyword the associated request parameters are
226 * restored from the database AND the backend user is loaded - only for that request.
227 * The main point is that a special URL valid for a limited time,
228 * eg. http://localhost/typo3site/index.php?ADMCMD_prev=035d9bf938bd23cb657735f68a8cedbf will
229 * open up for a preview that doesn't require login. Thus it's useful for sending in an email
230 * to someone without backend account.
231 * This can also be used to generate previews of hidden pages, start/endtimes, usergroups and
232 * those other settings from the Admin Panel - just not implemented yet.
235 * @return array Preview configuration array from sys_preview record.
237 public function getPreviewConfiguration()
239 $inputCode = $this->getPreviewInputCode();
240 // If input code is available and shall not be ignored, look up the settings
241 if ($inputCode && $inputCode !== 'IGNORE') {
243 if ($inputCode === 'LOGOUT') {
244 setcookie($this->previewKey
, '', 0, GeneralUtility
::getIndpEnv('TYPO3_SITE_PATH'));
245 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']) {
246 $templateFile = PATH_site
. $GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate'];
247 if (@is_file
($templateFile)) {
248 $message = file_get_contents($templateFile);
250 $message = '<strong>ERROR!</strong><br>Template File "'
251 . $GLOBALS['TYPO3_CONF_VARS']['FE']['workspacePreviewLogoutTemplate']
252 . '" configured with $TYPO3_CONF_VARS["FE"]["workspacePreviewLogoutTemplate"] not found. Please contact webmaster about this problem.';
255 $message = 'You logged out from Workspace preview mode. Click this link to <a href="%1$s">go back to the website</a>';
257 $returnUrl = GeneralUtility
::sanitizeLocalUrl(GeneralUtility
::_GET('returnUrl'));
258 die(sprintf($message, htmlspecialchars(preg_replace('/\\&?' . $this->previewKey
. '=[[:alnum:]]+/', '', $returnUrl))));
260 // Look for keyword configuration record:
261 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)
262 ->getQueryBuilderForTable('sys_preview');
264 $previewData = $queryBuilder
266 ->from('sys_preview')
268 $queryBuilder->expr()->eq(
270 $queryBuilder->createNamedParameter($inputCode, \PDO
::PARAM_STR
)
272 $queryBuilder->expr()->gt(
274 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO
::PARAM_INT
)
281 // Get: Backend login status, Frontend login status
282 // - Make sure to remove fe/be cookies (temporarily);
283 // BE already done in ADMCMD_preview_postInit()
284 if (is_array($previewData)) {
285 if (empty(GeneralUtility
::_POST())) {
286 // Unserialize configuration:
287 $previewConfig = unserialize($previewData['config']);
288 // For full workspace preview we only ADD a get variable
289 // to set the preview of the workspace - so all other Get
290 // vars are accepted. Hope this is not a security problem.
291 // Still posting is not allowed and even if a backend user
292 // get initialized it shouldn't lead to situations where
293 // users can use those credentials.
294 if ($previewConfig['fullWorkspace']) {
295 // Set the workspace preview value:
296 GeneralUtility
::_GETset($previewConfig['fullWorkspace'], 'ADMCMD_previewWS');
297 // If ADMCMD_prev is set the $inputCode value cannot come
298 // from a cookie and we set that cookie here. Next time it will
299 // be found from the cookie if ADMCMD_prev is not set again...
300 if (GeneralUtility
::_GP($this->previewKey
)) {
301 // Lifetime is 1 hour, does it matter much?
302 // Requires the user to click the link from their email again if it expires.
303 setcookie($this->previewKey
, GeneralUtility
::_GP($this->previewKey
), 0, GeneralUtility
::getIndpEnv('TYPO3_SITE_PATH'), null
, null
, true
);
305 return $previewConfig;
307 if (GeneralUtility
::getIndpEnv('TYPO3_SITE_URL') . 'index.php?' . $this->previewKey
. '=' . $inputCode === GeneralUtility
::getIndpEnv('TYPO3_REQUEST_URL')) {
310 parse_str($previewConfig['getVars'], $GET_VARS);
311 GeneralUtility
::_GETset($GET_VARS);
312 // Return preview keyword configuration
313 return $previewConfig;
315 // This check is to prevent people from setting additional
316 // GET vars via realurl or other URL path based ways of passing parameters.
317 throw new \
Exception(htmlspecialchars('Request URL did not match "'
318 . GeneralUtility
::getIndpEnv('TYPO3_SITE_URL') . 'index.php?' . $this->previewKey
. '='
319 . $inputCode . '"', 1294585190));
321 throw new \
Exception('POST requests are incompatible with keyword preview.', 1294585191);
323 throw new \
Exception('ADMCMD command could not be executed! (No keyword configuration found)', 1294585192);
329 * returns the input code value from the admin command variable
331 * @return string Input code
333 protected function getPreviewInputCode()
335 $inputCode = GeneralUtility
::_GP($this->previewKey
);
336 // If no inputcode and a cookie is set, load input code from cookie:
337 if (!$inputCode && $_COOKIE[$this->previewKey
]) {
338 $inputCode = $_COOKIE[$this->previewKey
];
344 * Set preview keyword, eg:
345 * $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);
347 * @todo for sys_preview:
348 * - Add a comment which can be shown to previewer in frontend in some way (plus maybe ability to write back, take other action?)
349 * - 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?
351 * @param string $getVarsStr Get variables to preview, eg. 'id=1150&L=0&ADMCMD_view=1&ADMCMD_editIcons=1&ADMCMD_previewWS=8'
352 * @param string $backendUserUid 32 byte MD5 hash keyword for the URL: "?ADMCMD_prev=[keyword]
353 * @param int $ttl Time-To-Live for keyword
354 * @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
355 * @return string Returns keyword to use in URL for ADMCMD_prev=
357 public function compilePreviewKeyword($getVarsStr, $backendUserUid, $ttl = 172800, $fullWorkspace = null
)
360 'keyword' => md5(uniqid(microtime(), true
)),
361 'tstamp' => $GLOBALS['EXEC_TIME'],
362 'endtime' => $GLOBALS['EXEC_TIME'] +
$ttl,
363 'config' => serialize([
364 'fullWorkspace' => $fullWorkspace,
365 'getVars' => $getVarsStr,
366 'BEUSER_uid' => $backendUserUid
369 GeneralUtility
::makeInstance(ConnectionPool
::class)
370 ->getConnectionForTable('sys_preview')
376 return $fieldData['keyword'];
380 * easy function to just return the number of hours
381 * a preview link is valid, based on the TSconfig value "options.workspaces.previewLinkTTLHours"
382 * by default, it's 48hs
384 * @return int The hours as a number
386 public function getPreviewLinkLifetime()
388 $ttlHours = (int)$GLOBALS['BE_USER']->getTSConfigVal('options.workspaces.previewLinkTTLHours');
389 return $ttlHours ?
$ttlHours : 24 * 2;
393 * @return FrontendBackendUserAuthentication
395 protected function createFrontendBackendUser()
397 return GeneralUtility
::makeInstance(
398 FrontendBackendUserAuthentication
::class