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