[TASK] Deprecate -1 in language-related calls of PageRepository
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Page / PageRepository.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Page;
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 Psr\Log\LoggerAwareInterface;
18 use Psr\Log\LoggerAwareTrait;
19 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
20 use TYPO3\CMS\Core\Context\Context;
21 use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
22 use TYPO3\CMS\Core\Context\LanguageAspect;
23 use TYPO3\CMS\Core\Context\UserAspect;
24 use TYPO3\CMS\Core\Database\Connection;
25 use TYPO3\CMS\Core\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Database\Query\QueryHelper;
27 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
29 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
30 use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
31 use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
32 use TYPO3\CMS\Core\Resource\FileRepository;
33 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
34 use TYPO3\CMS\Core\Utility\GeneralUtility;
35 use TYPO3\CMS\Core\Utility\RootlineUtility;
36 use TYPO3\CMS\Core\Versioning\VersionState;
37
38 /**
39 * Page functions, a lot of sql/pages-related functions
40 *
41 * Mainly used in the frontend but also in some cases in the backend. It's
42 * important to set the right $where_hid_del in the object so that the
43 * functions operate properly
44 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::fetch_the_id()
45 */
46 class PageRepository implements LoggerAwareInterface
47 {
48 use LoggerAwareTrait;
49 use PublicPropertyDeprecationTrait;
50
51 /**
52 * List of all deprecated public properties
53 * @var array
54 */
55 protected $deprecatedPublicProperties = [
56 'versioningPreview' => 'Using $versioningPreview of class PageRepository is discouraged, just use versioningWorkspaceId to determine if a workspace should be previewed.',
57 'workspaceCache' => 'Using $workspaceCache of class PageRepository from the outside is discouraged, as this only reflects a local runtime cache.',
58 'error_getRootLine' => 'Using $error_getRootLine of class PageRepository from the outside is deprecated as this property only exists for legacy reasons.',
59 'error_getRootLine_failPid' => 'Using $error_getRootLine_failPid of class PageRepository from the outside is deprecated as this property only exists for legacy reasons.',
60 'sys_language_uid' => 'Using $sys_language_uid of class PageRepository from the outside is deprecated as this information is now stored within the Context Language Aspect given by the constructor.',
61 ];
62
63 /**
64 * This is not the final clauses. There will normally be conditions for the
65 * hidden, starttime and endtime fields as well. You MUST initialize the object
66 * by the init() function
67 *
68 * @var string
69 */
70 public $where_hid_del = ' AND pages.deleted=0';
71
72 /**
73 * Clause for fe_group access
74 *
75 * @var string
76 */
77 public $where_groupAccess = '';
78
79 /**
80 * @var int
81 * @deprecated will be removed in TYPO3 v10, all occurrences should be replaced with the language->id() aspect property in TYPO3 v10.0
82 * However, the usage within the class is kept as the property could be overwritten by third-party classes
83 */
84 protected $sys_language_uid = 0;
85
86 /**
87 * If TRUE, versioning preview of other record versions is allowed. THIS MUST
88 * ONLY BE SET IF the page is not cached and truly previewed by a backend
89 * user!!!
90 *
91 * @var bool
92 * @deprecated since TYPO3 v9.3, will be removed in TYPO3 v10. As $versioningWorkspaceId now indicates what records to fetch.
93 */
94 protected $versioningPreview = false;
95
96 /**
97 * Workspace ID for preview
98 * If > 0, versioning preview of other record versions is allowed. THIS MUST
99 * ONLY BE SET IF the page is not cached and truly previewed by a backend
100 * user!
101 *
102 * @var int
103 */
104 public $versioningWorkspaceId = 0;
105
106 /**
107 * @var array
108 */
109 protected $workspaceCache = [];
110
111 /**
112 * Error string set by getRootLine()
113 *
114 * @var string
115 */
116 protected $error_getRootLine = '';
117
118 /**
119 * Error uid set by getRootLine()
120 *
121 * @var int
122 */
123 protected $error_getRootLine_failPid = 0;
124
125 /**
126 * @var array
127 */
128 protected $cache_getPage = [];
129
130 /**
131 * @var array
132 */
133 protected $cache_getPage_noCheck = [];
134
135 /**
136 * @var array
137 */
138 protected $cache_getPageIdFromAlias = [];
139
140 /**
141 * @var array
142 */
143 protected $cache_getMountPointInfo = [];
144
145 /**
146 * @var array
147 */
148 protected $tableNamesAllowedOnRootLevel = [
149 'sys_file_metadata',
150 'sys_category',
151 ];
152
153 /**
154 * Computed properties that are added to database rows.
155 *
156 * @var array
157 */
158 protected $computedPropertyNames = [
159 '_LOCALIZED_UID',
160 '_MP_PARAM',
161 '_ORIG_uid',
162 '_ORIG_pid',
163 '_PAGES_OVERLAY',
164 '_PAGES_OVERLAY_UID',
165 '_PAGES_OVERLAY_LANGUAGE',
166 ];
167
168 /**
169 * Named constants for "magic numbers" of the field doktype
170 */
171 const DOKTYPE_DEFAULT = 1;
172 const DOKTYPE_LINK = 3;
173 const DOKTYPE_SHORTCUT = 4;
174 const DOKTYPE_BE_USER_SECTION = 6;
175 const DOKTYPE_MOUNTPOINT = 7;
176 const DOKTYPE_SPACER = 199;
177 const DOKTYPE_SYSFOLDER = 254;
178 const DOKTYPE_RECYCLER = 255;
179
180 /**
181 * Named constants for "magic numbers" of the field shortcut_mode
182 */
183 const SHORTCUT_MODE_NONE = 0;
184 const SHORTCUT_MODE_FIRST_SUBPAGE = 1;
185 const SHORTCUT_MODE_RANDOM_SUBPAGE = 2;
186 const SHORTCUT_MODE_PARENT_PAGE = 3;
187
188 /**
189 * @var Context
190 */
191 protected $context;
192
193 /**
194 * PageRepository constructor to set the base context, this will effectively remove the necessity for
195 * setting properties from the outside.
196 *
197 * @param Context $context
198 */
199 public function __construct(Context $context = null)
200 {
201 $this->context = $context ?? GeneralUtility::makeInstance(Context::class);
202 $this->versioningWorkspaceId = $this->context->getPropertyFromAspect('workspace', 'id');
203 // Only set up the where clauses for pages when TCA is set. This usually happens only in tests.
204 // Once all tests are written very well, this can be removed again
205 if (isset($GLOBALS['TCA']['pages'])) {
206 $this->init($this->context->getPropertyFromAspect('visibility', 'includeHiddenPages'));
207 $this->where_groupAccess = $this->getMultipleGroupsWhereClause('pages.fe_group', 'pages');
208 $this->sys_language_uid = (int)$this->context->getPropertyFromAspect('language', 'id', 0);
209 }
210 }
211
212 /**
213 * init() MUST be run directly after creating a new template-object
214 * This sets the internal variable $this->where_hid_del to the correct where
215 * clause for page records taking deleted/hidden/starttime/endtime/t3ver_state
216 * into account
217 *
218 * @param bool $show_hidden If $show_hidden is TRUE, the hidden-field is ignored!! Normally this should be FALSE. Is used for previewing.
219 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::fetch_the_id(), \TYPO3\CMS\Tstemplate\Controller\TemplateAnalyzerModuleFunctionController::initialize_editor()
220 */
221 public function init($show_hidden)
222 {
223 $this->where_groupAccess = '';
224
225 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
226 ->getQueryBuilderForTable('pages')
227 ->expr();
228 if ($this->versioningWorkspaceId) {
229 // For version previewing, make sure that enable-fields are not
230 // de-selecting hidden pages - we need versionOL() to unset them only
231 // if the overlay record instructs us to.
232 // Clear where_hid_del and restrict to live and current workspaces
233 $this->where_hid_del = ' AND ' . $expressionBuilder->andX(
234 $expressionBuilder->eq('pages.deleted', 0),
235 $expressionBuilder->orX(
236 $expressionBuilder->eq('pages.t3ver_wsid', 0),
237 $expressionBuilder->eq('pages.t3ver_wsid', (int)$this->versioningWorkspaceId)
238 ),
239 $expressionBuilder->lt('pages.doktype', 200)
240 );
241 } else {
242 // add starttime / endtime, and check for hidden/deleted
243 // Filter out new/deleted place-holder pages in case we are NOT in a
244 // versioning preview (that means we are online!)
245 $this->where_hid_del = ' AND ' . (string)$expressionBuilder->andX(
246 QueryHelper::stripLogicalOperatorPrefix(
247 $this->enableFields('pages', $show_hidden, ['fe_group' => true], true)
248 ),
249 $expressionBuilder->lt('pages.doktype', 200)
250 );
251 }
252 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] ?? false)) {
253 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] as $classRef) {
254 $hookObject = GeneralUtility::makeInstance($classRef);
255 if (!$hookObject instanceof PageRepositoryInitHookInterface) {
256 throw new \UnexpectedValueException($classRef . ' must implement interface ' . PageRepositoryInitHookInterface::class, 1379579812);
257 }
258 $hookObject->init_postProcess($this);
259 }
260 }
261 }
262
263 /**************************
264 *
265 * Selecting page records
266 *
267 **************************/
268
269 /**
270 * Loads the full page record for the given page ID.
271 *
272 * The page record is either served from a first-level cache or loaded from the
273 * database. If no page can be found, an empty array is returned.
274 *
275 * Language overlay and versioning overlay are applied. Mount Point
276 * handling is not done, an overlaid Mount Point is not replaced.
277 *
278 * The result is conditioned by the public properties where_groupAccess
279 * and where_hid_del that are preset by the init() method.
280 *
281 * @see PageRepository::where_groupAccess
282 * @see PageRepository::where_hid_del
283 *
284 * By default the usergroup access check is enabled. Use the second method argument
285 * to disable the usergroup access check.
286 *
287 * The given UID can be preprocessed by registering a hook class that is
288 * implementing the PageRepositoryGetPageHookInterface into the configuration array
289 * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'].
290 *
291 * @param int $uid The page id to look up
292 * @param bool $disableGroupAccessCheck set to true to disable group access check
293 * @return array The resulting page record with overlays or empty array
294 * @throws \UnexpectedValueException
295 * @see PageRepository::getPage_noCheck()
296 */
297 public function getPage($uid, $disableGroupAccessCheck = false)
298 {
299 // Hook to manipulate the page uid for special overlay handling
300 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'] ?? [] as $className) {
301 $hookObject = GeneralUtility::makeInstance($className);
302 if (!$hookObject instanceof PageRepositoryGetPageHookInterface) {
303 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetPageHookInterface::class, 1251476766);
304 }
305 $hookObject->getPage_preProcess($uid, $disableGroupAccessCheck, $this);
306 }
307 $cacheKey = md5(
308 implode(
309 '-',
310 [
311 $disableGroupAccessCheck ? '' : $this->where_groupAccess,
312 $this->where_hid_del,
313 $this->sys_language_uid
314 ]
315 )
316 );
317 if (is_array($this->cache_getPage[$uid][$cacheKey])) {
318 return $this->cache_getPage[$uid][$cacheKey];
319 }
320 $result = [];
321 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
322 $queryBuilder->getRestrictions()->removeAll();
323 $queryBuilder->select('*')
324 ->from('pages')
325 ->where(
326 $queryBuilder->expr()->eq('uid', (int)$uid),
327 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del)
328 );
329
330 if (!$disableGroupAccessCheck) {
331 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess));
332 }
333
334 $row = $queryBuilder->execute()->fetch();
335 if ($row) {
336 $this->versionOL('pages', $row);
337 if (is_array($row)) {
338 $result = $this->getPageOverlay($row);
339 }
340 }
341 $this->cache_getPage[$uid][$cacheKey] = $result;
342 return $result;
343 }
344
345 /**
346 * Return the $row for the page with uid = $uid WITHOUT checking for
347 * ->where_hid_del (start- and endtime or hidden). Only "deleted" is checked!
348 *
349 * @param int $uid The page id to look up
350 * @return array The page row with overlaid localized fields. Empty array if no page.
351 * @see getPage()
352 */
353 public function getPage_noCheck($uid)
354 {
355 if ($this->cache_getPage_noCheck[$uid]) {
356 return $this->cache_getPage_noCheck[$uid];
357 }
358
359 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
360 $queryBuilder->getRestrictions()
361 ->removeAll()
362 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
363 $row = $queryBuilder->select('*')
364 ->from('pages')
365 ->where($queryBuilder->expr()->eq('uid', (int)$uid))
366 ->execute()
367 ->fetch();
368
369 $result = [];
370 if ($row) {
371 $this->versionOL('pages', $row);
372 if (is_array($row)) {
373 $result = $this->getPageOverlay($row);
374 }
375 }
376 $this->cache_getPage_noCheck[$uid] = $result;
377 return $result;
378 }
379
380 /**
381 * Returns the $row of the first web-page in the tree (for the default menu...)
382 *
383 * @param int $uid The page id for which to fetch first subpages (PID)
384 * @return mixed If found: The page record (with overlaid localized fields, if any). If NOT found: blank value (not array!)
385 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::fetch_the_id()
386 */
387 public function getFirstWebPage($uid)
388 {
389 $output = '';
390 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
391 $queryBuilder->getRestrictions()->removeAll();
392 $row = $queryBuilder->select('*')
393 ->from('pages')
394 ->where(
395 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
396 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
397 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess)
398 )
399 ->orderBy('sorting')
400 ->setMaxResults(1)
401 ->execute()
402 ->fetch();
403
404 if ($row) {
405 $this->versionOL('pages', $row);
406 if (is_array($row)) {
407 $output = $this->getPageOverlay($row);
408 }
409 }
410 return $output;
411 }
412
413 /**
414 * Returns a pagerow for the page with alias $alias
415 *
416 * @param string $alias The alias to look up the page uid for.
417 * @return int Returns page uid (int) if found, otherwise 0 (zero)
418 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::checkAndSetAlias(), ContentObjectRenderer::typoLink()
419 */
420 public function getPageIdFromAlias($alias)
421 {
422 $alias = strtolower($alias);
423 if ($this->cache_getPageIdFromAlias[$alias]) {
424 return $this->cache_getPageIdFromAlias[$alias];
425 }
426 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
427 $queryBuilder->getRestrictions()
428 ->removeAll()
429 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
430
431 $row = $queryBuilder->select('uid')
432 ->from('pages')
433 ->where(
434 $queryBuilder->expr()->eq('alias', $queryBuilder->createNamedParameter($alias, \PDO::PARAM_STR)),
435 // "AND pid>=0" because of versioning (means that aliases sent MUST be online!)
436 $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
437 )
438 ->setMaxResults(1)
439 ->execute()
440 ->fetch();
441
442 if ($row) {
443 $this->cache_getPageIdFromAlias[$alias] = $row['uid'];
444 return $row['uid'];
445 }
446 $this->cache_getPageIdFromAlias[$alias] = 0;
447 return 0;
448 }
449
450 /**
451 * Master helper method to overlay a record to a language.
452 *
453 * Be aware that for pages the languageId is taken, and for all other records the contentId.
454 * This might change through a feature switch in the future.
455 *
456 * @param string $table the name of the table, should be a TCA table with localization enabled
457 * @param array $row the current (full-fletched) record.
458 * @return array|null
459 */
460 public function getLanguageOverlay(string $table, array $row)
461 {
462 // table is not localizable, so return directly
463 if (!isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
464 return $row;
465 }
466 try {
467 /** @var LanguageAspect $languageAspect */
468 $languageAspect = $this->context->getAspect('language');
469 if ($languageAspect->doOverlays()) {
470 if ($table === 'pages') {
471 return $this->getPageOverlay($row, $languageAspect->getId());
472 }
473 return $this->getRecordOverlay(
474 $table,
475 $row,
476 $languageAspect->getContentId(),
477 $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_MIXED ? '1' : 'hideNonTranslated'
478 );
479 }
480 } catch (AspectNotFoundException $e) {
481 // no overlays
482 }
483 return $row;
484 }
485
486 /**
487 * Returns the relevant page overlay record fields
488 *
489 * @param mixed $pageInput If $pageInput is an integer, it's the pid of the pageOverlay record and thus the page overlay record is returned. If $pageInput is an array, it's a page-record and based on this page record the language record is found and OVERLAID before the page record is returned.
490 * @param int $languageUid Language UID if you want to set an alternative value to $this->sys_language_uid which is default. Should be >=0
491 * @throws \UnexpectedValueException
492 * @return array Page row which is overlaid with language_overlay record (or the overlay record alone)
493 */
494 public function getPageOverlay($pageInput, $languageUid = null)
495 {
496 if ($languageUid === -1) {
497 trigger_error('Calling getPageOverlay() with "-1" as languageId is discouraged and will be unsupported in TYPO3 v10.0. Omit the parameter or use "null" instead.', E_USER_DEPRECATED);
498 $languageUid = null;
499 }
500 $rows = $this->getPagesOverlay([$pageInput], $languageUid);
501 // Always an array in return
502 return $rows[0] ?? [];
503 }
504
505 /**
506 * Returns the relevant page overlay record fields
507 *
508 * @param array $pagesInput Array of integers or array of arrays. If each value is an integer, it's the pids of the pageOverlay records and thus the page overlay records are returned. If each value is an array, it's page-records and based on this page records the language records are found and OVERLAID before the page records are returned.
509 * @param int $languageUid Language UID if you want to set an alternative value to $this->sys_language_uid which is default. Should be >=0
510 * @throws \UnexpectedValueException
511 * @return array Page rows which are overlaid with language_overlay record.
512 * If the input was an array of integers, missing records are not
513 * included. If the input were page rows, untranslated pages
514 * are returned.
515 */
516 public function getPagesOverlay(array $pagesInput, $languageUid = null)
517 {
518 if (empty($pagesInput)) {
519 return [];
520 }
521 // Initialize:
522 if ($languageUid === null) {
523 $languageUid = $this->sys_language_uid;
524 } elseif ($languageUid < 0) {
525 trigger_error('Calling getPagesOverlay() with "-1" as languageId is discouraged and will be unsupported in TYPO3 v10.0. Omit the parameter or use "null" instead.', E_USER_DEPRECATED);
526 $languageUid = $this->sys_language_uid;
527 }
528 $row = null;
529 foreach ($pagesInput as &$origPage) {
530 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPageOverlay'] ?? [] as $className) {
531 $hookObject = GeneralUtility::makeInstance($className);
532 if (!$hookObject instanceof PageRepositoryGetPageOverlayHookInterface) {
533 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetPageOverlayHookInterface::class, 1269878881);
534 }
535 $hookObject->getPageOverlay_preProcess($origPage, $languageUid, $this);
536 }
537 }
538 unset($origPage);
539 // If language UID is different from zero, do overlay:
540 if ($languageUid) {
541 $page_ids = [];
542
543 $origPage = reset($pagesInput);
544 foreach ($pagesInput as $origPage) {
545 if (is_array($origPage)) {
546 // Was the whole record
547 $page_ids[] = $origPage['uid'];
548 } else {
549 // Was the id
550 $page_ids[] = $origPage;
551 }
552 }
553 // NOTE regarding the query restrictions
554 // Currently the showHiddenRecords of TSFE set will allow
555 // page translation records to be selected as they are
556 // child-records of a page.
557 // However you may argue that the showHiddenField flag should
558 // determine this. But that's not how it's done right now.
559 // Selecting overlay record:
560 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
561 ->getQueryBuilderForTable('pages');
562 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
563 $result = $queryBuilder->select('*')
564 ->from('pages')
565 ->where(
566 $queryBuilder->expr()->in(
567 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'],
568 $queryBuilder->createNamedParameter($page_ids, Connection::PARAM_INT_ARRAY)
569 ),
570 $queryBuilder->expr()->eq(
571 $GLOBALS['TCA']['pages']['ctrl']['languageField'],
572 $queryBuilder->createNamedParameter($languageUid, \PDO::PARAM_INT)
573 )
574 )
575 ->execute();
576
577 $overlays = [];
578 while ($row = $result->fetch()) {
579 $this->versionOL('pages', $row);
580 if (is_array($row)) {
581 $row['_PAGES_OVERLAY'] = true;
582 $row['_PAGES_OVERLAY_UID'] = $row['uid'];
583 $row['_PAGES_OVERLAY_LANGUAGE'] = $languageUid;
584 $origUid = $row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
585 // Unset vital fields that are NOT allowed to be overlaid:
586 unset($row['uid']);
587 unset($row['pid']);
588 $overlays[$origUid] = $row;
589 }
590 }
591 }
592 // Create output:
593 $pagesOutput = [];
594 foreach ($pagesInput as $key => $origPage) {
595 if (is_array($origPage)) {
596 $pagesOutput[$key] = $origPage;
597 if (isset($overlays[$origPage['uid']])) {
598 // Overwrite the original field with the overlay
599 foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) {
600 if ($fieldName !== 'uid' && $fieldName !== 'pid') {
601 $pagesOutput[$key][$fieldName] = $fieldValue;
602 }
603 }
604 }
605 } else {
606 if (isset($overlays[$origPage])) {
607 $pagesOutput[$key] = $overlays[$origPage];
608 }
609 }
610 }
611 return $pagesOutput;
612 }
613
614 /**
615 * Creates language-overlay for records in general (where translation is found
616 * in records from the same table)
617 *
618 * @param string $table Table name
619 * @param array $row Record to overlay. Must contain uid, pid and $table]['ctrl']['languageField']
620 * @param int $sys_language_content Pointer to the sys_language uid for content on the site.
621 * @param string $OLmode Overlay mode. If "hideNonTranslated" then records without translation will not be returned un-translated but unset (and return value is FALSE)
622 * @throws \UnexpectedValueException
623 * @return mixed Returns the input record, possibly overlaid with a translation. But if $OLmode is "hideNonTranslated" then it will return FALSE if no translation is found.
624 */
625 public function getRecordOverlay($table, $row, $sys_language_content, $OLmode = '')
626 {
627 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] ?? [] as $className) {
628 $hookObject = GeneralUtility::makeInstance($className);
629 if (!$hookObject instanceof PageRepositoryGetRecordOverlayHookInterface) {
630 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetRecordOverlayHookInterface::class, 1269881658);
631 }
632 $hookObject->getRecordOverlay_preProcess($table, $row, $sys_language_content, $OLmode, $this);
633 }
634 if ($row['uid'] > 0 && ($row['pid'] > 0 || in_array($table, $this->tableNamesAllowedOnRootLevel, true))) {
635 if ($GLOBALS['TCA'][$table] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
636 // Return record for ALL languages untouched
637 // TODO: Fix call stack to prevent this situation in the first place
638 if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] !== -1) {
639 // Will not be able to work with other tables (Just didn't implement it yet;
640 // Requires a scan over all tables [ctrl] part for first FIND the table that
641 // carries localization information for this table (which could even be more
642 // than a single table) and then use that. Could be implemented, but obviously
643 // takes a little more....) Will try to overlay a record only if the
644 // sys_language_content value is larger than zero.
645 if ($sys_language_content > 0) {
646 // Must be default language, otherwise no overlaying
647 if ((int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
648 // Select overlay record:
649 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
650 ->getQueryBuilderForTable($table);
651 $queryBuilder->setRestrictions(
652 GeneralUtility::makeInstance(FrontendRestrictionContainer::class)
653 );
654 $olrow = $queryBuilder->select('*')
655 ->from($table)
656 ->where(
657 $queryBuilder->expr()->eq(
658 'pid',
659 $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
660 ),
661 $queryBuilder->expr()->eq(
662 $GLOBALS['TCA'][$table]['ctrl']['languageField'],
663 $queryBuilder->createNamedParameter($sys_language_content, \PDO::PARAM_INT)
664 ),
665 $queryBuilder->expr()->eq(
666 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
667 $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT)
668 )
669 )
670 ->setMaxResults(1)
671 ->execute()
672 ->fetch();
673
674 $this->versionOL($table, $olrow);
675 // Merge record content by traversing all fields:
676 if (is_array($olrow)) {
677 if (isset($olrow['_ORIG_uid'])) {
678 $row['_ORIG_uid'] = $olrow['_ORIG_uid'];
679 }
680 if (isset($olrow['_ORIG_pid'])) {
681 $row['_ORIG_pid'] = $olrow['_ORIG_pid'];
682 }
683 foreach ($row as $fN => $fV) {
684 if ($fN !== 'uid' && $fN !== 'pid' && isset($olrow[$fN])) {
685 $row[$fN] = $olrow[$fN];
686 } elseif ($fN === 'uid') {
687 $row['_LOCALIZED_UID'] = $olrow['uid'];
688 }
689 }
690 } elseif ($OLmode === 'hideNonTranslated' && (int)$row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] === 0) {
691 // Unset, if non-translated records should be hidden. ONLY done if the source
692 // record really is default language and not [All] in which case it is allowed.
693 unset($row);
694 }
695 } elseif ($sys_language_content != $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']]) {
696 unset($row);
697 }
698 } else {
699 // When default language is displayed, we never want to return a record carrying
700 // another language!
701 if ($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
702 unset($row);
703 }
704 }
705 }
706 }
707 }
708 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] ?? [] as $className) {
709 $hookObject = GeneralUtility::makeInstance($className);
710 if (!$hookObject instanceof PageRepositoryGetRecordOverlayHookInterface) {
711 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetRecordOverlayHookInterface::class, 1269881659);
712 }
713 $hookObject->getRecordOverlay_postProcess($table, $row, $sys_language_content, $OLmode, $this);
714 }
715 return $row;
716 }
717
718 /************************************************
719 *
720 * Page related: Menu, Domain record, Root line
721 *
722 ************************************************/
723
724 /**
725 * Returns an array with page rows for subpages of a certain page ID. This is used for menus in the frontend.
726 * If there are mount points in overlay mode the _MP_PARAM field is set to the correct MPvar.
727 *
728 * If the $pageId being input does in itself require MPvars to define a correct
729 * rootline these must be handled externally to this function.
730 *
731 * @param int|int[] $pageId The page id (or array of page ids) for which to fetch subpages (PID)
732 * @param string $fields List of fields to select. Default is "*" = all
733 * @param string $sortField The field to sort by. Default is "sorting
734 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%blabla%'" for instance.
735 * @param bool $checkShortcuts Check if shortcuts exist, checks by default
736 * @return array Array with key/value pairs; keys are page-uid numbers. values are the corresponding page records (with overlaid localized fields, if any)
737 * @see self::getPageShortcut(), \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::makeMenu()
738 */
739 public function getMenu($pageId, $fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true)
740 {
741 return $this->getSubpagesForPages((array)$pageId, $fields, $sortField, $additionalWhereClause, $checkShortcuts);
742 }
743
744 /**
745 * Returns an array with page-rows for pages with uid in $pageIds.
746 *
747 * This is used for menus. If there are mount points in overlay mode
748 * the _MP_PARAM field is set to the correct MPvar.
749 *
750 * @param int[] $pageIds Array of page ids to fetch
751 * @param string $fields List of fields to select. Default is "*" = all
752 * @param string $sortField The field to sort by. Default is "sorting"
753 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%blabla%'" for instance.
754 * @param bool $checkShortcuts Check if shortcuts exist, checks by default
755 * @return array Array with key/value pairs; keys are page-uid numbers. values are the corresponding page records (with overlaid localized fields, if any)
756 */
757 public function getMenuForPages(array $pageIds, $fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true)
758 {
759 return $this->getSubpagesForPages($pageIds, $fields, $sortField, $additionalWhereClause, $checkShortcuts, false);
760 }
761
762 /**
763 * Loads page records either by PIDs or by UIDs.
764 *
765 * By default the subpages of the given page IDs are loaded (as the method name suggests). If $parentPages is set
766 * to FALSE, the page records for the given page IDs are loaded directly.
767 *
768 * Concerning the rationale, please see these two other methods:
769 *
770 * @see PageRepository::getMenu()
771 * @see PageRepository::getMenuForPages()
772 *
773 * Version and language overlay are applied to the loaded records.
774 *
775 * If a record is a mount point in overlay mode, the the overlaying page record is returned in place of the
776 * record. The record is enriched by the field _MP_PARAM containing the mount point mapping for the mount
777 * point.
778 *
779 * The query can be customized by setting fields, sorting and additional WHERE clauses. If additional WHERE
780 * clauses are given, the clause must start with an operator, i.e: "AND title like '%blabla%'".
781 *
782 * The keys of the returned page records are the page UIDs.
783 *
784 * CAUTION: In case of an overlaid mount point, it is the original UID.
785 *
786 * @param int[] $pageIds PIDs or UIDs to load records for
787 * @param string $fields fields to select
788 * @param string $sortField the field to sort by
789 * @param string $additionalWhereClause optional additional WHERE clause
790 * @param bool $checkShortcuts whether to check if shortcuts exist
791 * @param bool $parentPages Switch to load pages (false) or child pages (true).
792 * @return array page records
793 *
794 * @see self::getPageShortcut()
795 * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::makeMenu()
796 */
797 protected function getSubpagesForPages(
798 array $pageIds,
799 string $fields = '*',
800 string $sortField = 'sorting',
801 string $additionalWhereClause = '',
802 bool $checkShortcuts = true,
803 bool $parentPages = true
804 ): array {
805 $relationField = $parentPages ? 'pid' : 'uid';
806 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
807 $queryBuilder->getRestrictions()->removeAll();
808
809 $res = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
810 ->from('pages')
811 ->where(
812 $queryBuilder->expr()->in(
813 $relationField,
814 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
815 ),
816 $queryBuilder->expr()->eq(
817 $GLOBALS['TCA']['pages']['ctrl']['languageField'],
818 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
819 ),
820 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
821 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
822 QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
823 );
824
825 if (!empty($sortField)) {
826 $orderBy = QueryHelper::parseOrderBy($sortField);
827 foreach ($orderBy as $order) {
828 $res->orderBy(...$order);
829 }
830 }
831 $result = $res->execute();
832
833 $pages = [];
834 while ($page = $result->fetch()) {
835 $originalUid = $page['uid'];
836
837 // Versioning Preview Overlay
838 $this->versionOL('pages', $page, true);
839 // Skip if page got disabled due to version overlay
840 // (might be delete or move placeholder)
841 if (empty($page)) {
842 continue;
843 }
844
845 // Add a mount point parameter if needed
846 $page = $this->addMountPointParameterToPage((array)$page);
847
848 // If shortcut, look up if the target exists and is currently visible
849 if ($checkShortcuts) {
850 $page = $this->checkValidShortcutOfPage((array)$page, $additionalWhereClause);
851 }
852
853 // If the page still is there, we add it to the output
854 if (!empty($page)) {
855 $pages[$originalUid] = $page;
856 }
857 }
858
859 // Finally load language overlays
860 return $this->getPagesOverlay($pages);
861 }
862
863 /**
864 * Replaces the given page record with mounted page if required
865 *
866 * If the given page record is a mount point in overlay mode, the page
867 * record is replaced by the record of the overlaying page. The overlay
868 * record is enriched by setting the mount point mapping into the field
869 * _MP_PARAM as string for example '23-14'.
870 *
871 * In all other cases the given page record is returned as is.
872 *
873 * @todo Find a better name. The current doesn't hit the point.
874 *
875 * @param array $page The page record to handle.
876 * @return array The given page record or it's replacement.
877 */
878 protected function addMountPointParameterToPage(array $page): array
879 {
880 if (empty($page)) {
881 return [];
882 }
883
884 // $page MUST have "uid", "pid", "doktype", "mount_pid", "mount_pid_ol" fields in it
885 $mountPointInfo = $this->getMountPointInfo($page['uid'], $page);
886
887 // There is a valid mount point in overlay mode.
888 if (is_array($mountPointInfo) && $mountPointInfo['overlay']) {
889
890 // Using "getPage" is OK since we need the check for enableFields AND for type 2
891 // of mount pids we DO require a doktype < 200!
892 $mountPointPage = $this->getPage($mountPointInfo['mount_pid']);
893
894 if (!empty($mountPointPage)) {
895 $page = $mountPointPage;
896 $page['_MP_PARAM'] = $mountPointInfo['MPvar'];
897 } else {
898 $page = [];
899 }
900 }
901 return $page;
902 }
903
904 /**
905 * If shortcut, look up if the target exists and is currently visible
906 *
907 * @param array $page The page to check
908 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%blabla%'" for instance.
909 * @return array
910 */
911 protected function checkValidShortcutOfPage(array $page, $additionalWhereClause)
912 {
913 if (empty($page)) {
914 return [];
915 }
916
917 $dokType = (int)$page['doktype'];
918 $shortcutMode = (int)$page['shortcut_mode'];
919
920 if ($dokType === self::DOKTYPE_SHORTCUT && ($page['shortcut'] || $shortcutMode)) {
921 if ($shortcutMode === self::SHORTCUT_MODE_NONE) {
922 // No shortcut_mode set, so target is directly set in $page['shortcut']
923 $searchField = 'uid';
924 $searchUid = (int)$page['shortcut'];
925 } elseif ($shortcutMode === self::SHORTCUT_MODE_FIRST_SUBPAGE || $shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) {
926 // Check subpages - first subpage or random subpage
927 $searchField = 'pid';
928 // If a shortcut mode is set and no valid page is given to select subpags
929 // from use the actual page.
930 $searchUid = (int)$page['shortcut'] ?: $page['uid'];
931 } elseif ($shortcutMode === self::SHORTCUT_MODE_PARENT_PAGE) {
932 // Shortcut to parent page
933 $searchField = 'uid';
934 $searchUid = $page['pid'];
935 } else {
936 $searchField = '';
937 $searchUid = 0;
938 }
939
940 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
941 $queryBuilder->getRestrictions()->removeAll();
942 $count = $queryBuilder->count('uid')
943 ->from('pages')
944 ->where(
945 $queryBuilder->expr()->eq(
946 $searchField,
947 $queryBuilder->createNamedParameter($searchUid, \PDO::PARAM_INT)
948 ),
949 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
950 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
951 QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
952 )
953 ->execute()
954 ->fetchColumn();
955
956 if (!$count) {
957 $page = [];
958 }
959 } elseif ($dokType === self::DOKTYPE_SHORTCUT) {
960 // Neither shortcut target nor mode is set. Remove the page from the menu.
961 $page = [];
962 }
963 return $page;
964 }
965
966 /**
967 * Get page shortcut; Finds the records pointed to by input value $SC (the shortcut value)
968 *
969 * @param int $shortcutFieldValue The value of the "shortcut" field from the pages record
970 * @param int $shortcutMode The shortcut mode: 1 will select first subpage, 2 a random subpage, 3 the parent page; default is the page pointed to by $SC
971 * @param int $thisUid The current page UID of the page which is a shortcut
972 * @param int $iteration Safety feature which makes sure that the function is calling itself recursively max 20 times (since this function can find shortcuts to other shortcuts to other shortcuts...)
973 * @param array $pageLog An array filled with previous page uids tested by the function - new page uids are evaluated against this to avoid going in circles.
974 * @param bool $disableGroupCheck If true, the group check is disabled when fetching the target page (needed e.g. for menu generation)
975 *
976 * @throws \RuntimeException
977 * @throws ShortcutTargetPageNotFoundException
978 * @return mixed Returns the page record of the page that the shortcut pointed to.
979 * @access private
980 * @see getPageAndRootline()
981 */
982 public function getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration = 20, $pageLog = [], $disableGroupCheck = false)
983 {
984 $idArray = GeneralUtility::intExplode(',', $shortcutFieldValue);
985 // Find $page record depending on shortcut mode:
986 switch ($shortcutMode) {
987 case self::SHORTCUT_MODE_FIRST_SUBPAGE:
988 case self::SHORTCUT_MODE_RANDOM_SUBPAGE:
989 $pageArray = $this->getMenu($idArray[0] ?: $thisUid, '*', 'sorting', 'AND pages.doktype<199 AND pages.doktype!=' . self::DOKTYPE_BE_USER_SECTION);
990 $pO = 0;
991 if ($shortcutMode == self::SHORTCUT_MODE_RANDOM_SUBPAGE && !empty($pageArray)) {
992 $pO = (int)rand(0, count($pageArray) - 1);
993 }
994 $c = 0;
995 $page = [];
996 foreach ($pageArray as $pV) {
997 if ($c === $pO) {
998 $page = $pV;
999 break;
1000 }
1001 $c++;
1002 }
1003 if (empty($page)) {
1004 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a subpage. However, this page has no accessible subpages.';
1005 throw new ShortcutTargetPageNotFoundException($message, 1301648328);
1006 }
1007 break;
1008 case self::SHORTCUT_MODE_PARENT_PAGE:
1009 $parent = $this->getPage($idArray[0] ?: $thisUid, $disableGroupCheck);
1010 $page = $this->getPage($parent['pid'], $disableGroupCheck);
1011 if (empty($page)) {
1012 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to its parent page. However, the parent page is not accessible.';
1013 throw new ShortcutTargetPageNotFoundException($message, 1301648358);
1014 }
1015 break;
1016 default:
1017 $page = $this->getPage($idArray[0], $disableGroupCheck);
1018 if (empty($page)) {
1019 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a page, which is not accessible (ID ' . $idArray[0] . ').';
1020 throw new ShortcutTargetPageNotFoundException($message, 1301648404);
1021 }
1022 }
1023 // Check if short cut page was a shortcut itself, if so look up recursively:
1024 if ($page['doktype'] == self::DOKTYPE_SHORTCUT) {
1025 if (!in_array($page['uid'], $pageLog) && $iteration > 0) {
1026 $pageLog[] = $page['uid'];
1027 $page = $this->getPageShortcut($page['shortcut'], $page['shortcut_mode'], $page['uid'], $iteration - 1, $pageLog, $disableGroupCheck);
1028 } else {
1029 $pageLog[] = $page['uid'];
1030 $message = 'Page shortcuts were looping in uids ' . implode(',', $pageLog) . '...!';
1031 $this->logger->error($message);
1032 throw new \RuntimeException($message, 1294587212);
1033 }
1034 }
1035 // Return resulting page:
1036 return $page;
1037 }
1038
1039 /**
1040 * Will find the page carrying the domain record matching the input domain.
1041 *
1042 * @param string $domain Domain name to search for. Eg. "www.typo3.com". Typical the HTTP_HOST value.
1043 * @param string $path Path for the current script in domain. Eg. "/somedir/subdir". Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('SCRIPT_NAME')
1044 * @param string $request_uri Request URI: Used to get parameters from if they should be appended. Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI')
1045 * @return mixed If found, returns integer with page UID where found. Otherwise blank. Might exit if location-header is sent, see description.
1046 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::findDomainRecord()
1047 * @deprecated will be removed in TYPO3 v10.0.
1048 */
1049 public function getDomainStartPage($domain, $path = '', $request_uri = '')
1050 {
1051 trigger_error('This method will be removed in TYPO3 v10.0. As the SiteResolver middleware resolves the domain start page.', E_USER_DEPRECATED);
1052 $domain = explode(':', $domain);
1053 $domain = strtolower(preg_replace('/\\.$/', '', $domain[0]));
1054 // Removing extra trailing slashes
1055 $path = trim(preg_replace('/\\/[^\\/]*$/', '', $path));
1056 // Appending to domain string
1057 $domain .= $path;
1058 $domain = preg_replace('/\\/*$/', '', $domain);
1059 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1060 $queryBuilder->getRestrictions()->removeAll();
1061 $row = $queryBuilder
1062 ->select(
1063 'pages.uid'
1064 )
1065 ->from('pages')
1066 ->from('sys_domain')
1067 ->where(
1068 $queryBuilder->expr()->eq('pages.uid', $queryBuilder->quoteIdentifier('sys_domain.pid')),
1069 $queryBuilder->expr()->eq(
1070 'sys_domain.hidden',
1071 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1072 ),
1073 $queryBuilder->expr()->orX(
1074 $queryBuilder->expr()->eq(
1075 'sys_domain.domainName',
1076 $queryBuilder->createNamedParameter($domain, \PDO::PARAM_STR)
1077 ),
1078 $queryBuilder->expr()->eq(
1079 'sys_domain.domainName',
1080 $queryBuilder->createNamedParameter($domain . '/', \PDO::PARAM_STR)
1081 )
1082 ),
1083 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
1084 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess)
1085 )
1086 ->setMaxResults(1)
1087 ->execute()
1088 ->fetch();
1089
1090 if (!$row) {
1091 return '';
1092 }
1093 return $row['uid'];
1094 }
1095
1096 /**
1097 * Returns array with fields of the pages from here ($uid) and back to the root
1098 *
1099 * NOTICE: This function only takes deleted pages into account! So hidden,
1100 * starttime and endtime restricted pages are included no matter what.
1101 *
1102 * Further: If any "recycler" page is found (doktype=255) then it will also block
1103 * for the rootline)
1104 *
1105 * If you want more fields in the rootline records than default such can be added
1106 * by listing them in $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields']
1107 *
1108 * @param int $uid The page uid for which to seek back to the page tree root.
1109 * @param string $MP Commalist of MountPoint parameters, eg. "1-2,3-4" etc. Normally this value comes from the GET var, MP
1110 * @param bool $ignoreMPerrors If set, some errors related to Mount Points in root line are ignored.
1111 * @throws \Exception
1112 * @throws \RuntimeException
1113 * @return array Array with page records from the root line as values. The array is ordered with the outer records first and root record in the bottom. The keys are numeric but in reverse order. So if you traverse/sort the array by the numeric keys order you will get the order from root and out. If an error is found (like eternal looping or invalid mountpoint) it will return an empty array.
1114 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getPageAndRootline()
1115 */
1116 public function getRootLine($uid, $MP = '', $ignoreMPerrors = null)
1117 {
1118 if ($ignoreMPerrors !== null) {
1119 trigger_error('The third argument in PageRepository::getRootline() will be removed in TYPO3 v10.0. Use a try/catch block around this method to catch any mount point errors, if necessary', E_USER_DEPRECATED);
1120 } else {
1121 $ignoreMPerrors = false;
1122 }
1123 $rootline = GeneralUtility::makeInstance(RootlineUtility::class, $uid, $MP, $this->context);
1124 try {
1125 return $rootline->get();
1126 } catch (\RuntimeException $ex) {
1127 if ($ignoreMPerrors) {
1128 $this->error_getRootLine = $ex->getMessage();
1129 if (substr($this->error_getRootLine, -7) === 'uid -1.') {
1130 $this->error_getRootLine_failPid = -1;
1131 }
1132 return [];
1133 }
1134 if ($ex->getCode() === 1343589451) {
1135 /** @see \TYPO3\CMS\Core\Utility\RootlineUtility::getRecordArray */
1136 return [];
1137 }
1138 throw $ex;
1139 }
1140 }
1141
1142 /**
1143 * Returns the redirect URL for the input page row IF the doktype is set to 3.
1144 *
1145 * @param array $pagerow The page row to return URL type for
1146 * @return string|bool The URL from based on the data from "pages:url". False if not found.
1147 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::initializeRedirectUrlHandlers()
1148 */
1149 public function getExtURL($pagerow)
1150 {
1151 if ((int)$pagerow['doktype'] === self::DOKTYPE_LINK) {
1152 $redirectTo = $pagerow['url'];
1153 $uI = parse_url($redirectTo);
1154 // If relative path, prefix Site URL
1155 // If it's a valid email without protocol, add "mailto:"
1156 if (!($uI['scheme'] ?? false)) {
1157 if (GeneralUtility::validEmail($redirectTo)) {
1158 $redirectTo = 'mailto:' . $redirectTo;
1159 } elseif ($redirectTo[0] !== '/') {
1160 $redirectTo = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . $redirectTo;
1161 }
1162 }
1163 return $redirectTo;
1164 }
1165 return false;
1166 }
1167
1168 /**
1169 * Returns a MountPoint array for the specified page
1170 *
1171 * Does a recursive search if the mounted page should be a mount page
1172 * itself.
1173 *
1174 * Note:
1175 *
1176 * Recursive mount points are not supported by all parts of the core.
1177 * The usage is discouraged. They may be removed from this method.
1178 *
1179 * @see: https://decisions.typo3.org/t/supporting-or-prohibiting-recursive-mount-points/165/3
1180 *
1181 * An array will be returned if mount pages are enabled, the correct
1182 * doktype (7) is set for page and there IS a mount_pid with a valid
1183 * record.
1184 *
1185 * The optional page record must contain at least uid, pid, doktype,
1186 * mount_pid,mount_pid_ol. If it is not supplied it will be looked up by
1187 * the system at additional costs for the lookup.
1188 *
1189 * Returns FALSE if no mount point was found, "-1" if there should have been
1190 * one, but no connection to it, otherwise an array with information
1191 * about mount pid and modes.
1192 *
1193 * @param int $pageId Page id to do the lookup for.
1194 * @param array|bool $pageRec Optional page record for the given page.
1195 * @param array $prevMountPids Internal register to prevent lookup cycles.
1196 * @param int $firstPageUid The first page id.
1197 * @return mixed Mount point array or failure flags (-1, false).
1198 * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject
1199 */
1200 public function getMountPointInfo($pageId, $pageRec = false, $prevMountPids = [], $firstPageUid = 0)
1201 {
1202 $result = false;
1203 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1204 if (isset($this->cache_getMountPointInfo[$pageId])) {
1205 return $this->cache_getMountPointInfo[$pageId];
1206 }
1207 // Get pageRec if not supplied:
1208 if (!is_array($pageRec)) {
1209 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1210 $queryBuilder->getRestrictions()
1211 ->removeAll()
1212 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1213
1214 $pageRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state')
1215 ->from('pages')
1216 ->where(
1217 $queryBuilder->expr()->eq(
1218 'uid',
1219 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
1220 ),
1221 $queryBuilder->expr()->neq(
1222 'doktype',
1223 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
1224 )
1225 )
1226 ->execute()
1227 ->fetch();
1228
1229 // Only look for version overlay if page record is not supplied; This assumes
1230 // that the input record is overlaid with preview version, if any!
1231 $this->versionOL('pages', $pageRec);
1232 }
1233 // Set first Page uid:
1234 if (!$firstPageUid) {
1235 $firstPageUid = $pageRec['uid'];
1236 }
1237 // Look for mount pid value plus other required circumstances:
1238 $mount_pid = (int)$pageRec['mount_pid'];
1239 if (is_array($pageRec) && (int)$pageRec['doktype'] === self::DOKTYPE_MOUNTPOINT && $mount_pid > 0 && !in_array($mount_pid, $prevMountPids, true)) {
1240 // Get the mount point record (to verify its general existence):
1241 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1242 $queryBuilder->getRestrictions()
1243 ->removeAll()
1244 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1245
1246 $mountRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state')
1247 ->from('pages')
1248 ->where(
1249 $queryBuilder->expr()->eq(
1250 'uid',
1251 $queryBuilder->createNamedParameter($mount_pid, \PDO::PARAM_INT)
1252 ),
1253 $queryBuilder->expr()->neq(
1254 'doktype',
1255 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
1256 )
1257 )
1258 ->execute()
1259 ->fetch();
1260
1261 $this->versionOL('pages', $mountRec);
1262 if (is_array($mountRec)) {
1263 // Look for recursive mount point:
1264 $prevMountPids[] = $mount_pid;
1265 $recursiveMountPid = $this->getMountPointInfo($mount_pid, $mountRec, $prevMountPids, $firstPageUid);
1266 // Return mount point information:
1267 $result = $recursiveMountPid ?: [
1268 'mount_pid' => $mount_pid,
1269 'overlay' => $pageRec['mount_pid_ol'],
1270 'MPvar' => $mount_pid . '-' . $firstPageUid,
1271 'mount_point_rec' => $pageRec,
1272 'mount_pid_rec' => $mountRec
1273 ];
1274 } else {
1275 // Means, there SHOULD have been a mount point, but there was none!
1276 $result = -1;
1277 }
1278 }
1279 }
1280 $this->cache_getMountPointInfo[$pageId] = $result;
1281 return $result;
1282 }
1283
1284 /********************************
1285 *
1286 * Selecting records in general
1287 *
1288 ********************************/
1289
1290 /**
1291 * Checks if a record exists and is accessible.
1292 * The row is returned if everything's OK.
1293 *
1294 * @param string $table The table name to search
1295 * @param int $uid The uid to look up in $table
1296 * @param bool|int $checkPage If checkPage is set, it's also required that the page on which the record resides is accessible
1297 * @return array|int Returns array (the record) if OK, otherwise blank/0 (zero)
1298 */
1299 public function checkRecord($table, $uid, $checkPage = 0)
1300 {
1301 $uid = (int)$uid;
1302 if (is_array($GLOBALS['TCA'][$table]) && $uid > 0) {
1303 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1304 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1305 $row = $queryBuilder->select('*')
1306 ->from($table)
1307 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
1308 ->execute()
1309 ->fetch();
1310
1311 if ($row) {
1312 $this->versionOL($table, $row);
1313 if (is_array($row)) {
1314 if ($checkPage) {
1315 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1316 ->getQueryBuilderForTable('pages');
1317 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1318 $numRows = (int)$queryBuilder->count('*')
1319 ->from('pages')
1320 ->where(
1321 $queryBuilder->expr()->eq(
1322 'uid',
1323 $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT)
1324 )
1325 )
1326 ->execute()
1327 ->fetchColumn();
1328 if ($numRows > 0) {
1329 return $row;
1330 }
1331 return 0;
1332 }
1333 return $row;
1334 }
1335 }
1336 }
1337 return 0;
1338 }
1339
1340 /**
1341 * Returns record no matter what - except if record is deleted
1342 *
1343 * @param string $table The table name to search
1344 * @param int $uid The uid to look up in $table
1345 * @param string $fields The fields to select, default is "*
1346 * @param bool $noWSOL If set, no version overlay is applied
1347 * @return mixed Returns array (the record) if found, otherwise blank/0 (zero)
1348 * @see getPage_noCheck()
1349 */
1350 public function getRawRecord($table, $uid, $fields = '*', $noWSOL = null)
1351 {
1352 $uid = (int)$uid;
1353 if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]) && $uid > 0) {
1354 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1355 $queryBuilder->getRestrictions()
1356 ->removeAll()
1357 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1358 $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
1359 ->from($table)
1360 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
1361 ->execute()
1362 ->fetch();
1363
1364 if ($row) {
1365 if ($noWSOL !== null) {
1366 trigger_error('The fourth parameter of PageRepository->getRawRecord() has been deprecated, use a SQL statement directly. The parameter will be removed in TYPO3 v10.', E_USER_DEPRECATED);
1367 }
1368 // @deprecated - remove this if-clause in TYPO3 v10
1369 if (!$noWSOL) {
1370 $this->versionOL($table, $row);
1371 }
1372 if (is_array($row)) {
1373 return $row;
1374 }
1375 }
1376 }
1377 return 0;
1378 }
1379
1380 /**
1381 * Selects records based on matching a field (ei. other than UID) with a value
1382 *
1383 * @param string $theTable The table name to search, eg. "pages" or "tt_content
1384 * @param string $theField The fieldname to match, eg. "uid" or "alias
1385 * @param string $theValue The value that fieldname must match, eg. "123" or "frontpage
1386 * @param string $whereClause Optional additional WHERE clauses put in the end of the query. DO NOT PUT IN GROUP BY, ORDER BY or LIMIT!
1387 * @param string $groupBy Optional GROUP BY field(s). If none, supply blank string.
1388 * @param string $orderBy Optional ORDER BY field(s). If none, supply blank string.
1389 * @param string $limit Optional LIMIT value ([begin,]max). If none, supply blank string.
1390 * @return mixed Returns array (the record) if found, otherwise nothing (void)
1391 */
1392 public function getRecordsByField($theTable, $theField, $theValue, $whereClause = '', $groupBy = '', $orderBy = '', $limit = '')
1393 {
1394 if (is_array($GLOBALS['TCA'][$theTable])) {
1395 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($theTable);
1396 $queryBuilder->getRestrictions()
1397 ->removeAll()
1398 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1399
1400 $queryBuilder->select('*')
1401 ->from($theTable)
1402 ->where($queryBuilder->expr()->eq($theField, $queryBuilder->createNamedParameter($theValue)));
1403
1404 if ($whereClause !== '') {
1405 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($whereClause));
1406 }
1407
1408 if ($groupBy !== '') {
1409 $queryBuilder->groupBy(QueryHelper::parseGroupBy($groupBy));
1410 }
1411
1412 if ($orderBy !== '') {
1413 foreach (QueryHelper::parseOrderBy($orderBy) as $orderPair) {
1414 list($fieldName, $order) = $orderPair;
1415 $queryBuilder->addOrderBy($fieldName, $order);
1416 }
1417 }
1418
1419 if ($limit !== '') {
1420 if (strpos($limit, ',')) {
1421 $limitOffsetAndMax = GeneralUtility::intExplode(',', $limit);
1422 $queryBuilder->setFirstResult((int)$limitOffsetAndMax[0]);
1423 $queryBuilder->setMaxResults((int)$limitOffsetAndMax[1]);
1424 } else {
1425 $queryBuilder->setMaxResults((int)$limit);
1426 }
1427 }
1428
1429 $rows = $queryBuilder->execute()->fetchAll();
1430
1431 if (!empty($rows)) {
1432 return $rows;
1433 }
1434 }
1435 return null;
1436 }
1437
1438 /********************************
1439 *
1440 * Standard clauses
1441 *
1442 ********************************/
1443
1444 /**
1445 * Returns the "AND NOT deleted" clause for the tablename given IF
1446 * $GLOBALS['TCA'] configuration points to such a field.
1447 *
1448 * @param string $table Tablename
1449 * @return string
1450 * @see enableFields()
1451 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10, use QueryBuilders' Restrictions directly instead.
1452 */
1453 public function deleteClause($table)
1454 {
1455 trigger_error('The delete clause can be applied via the DeletedRestrictions via QueryBuilder, this method will be removed in TYPO3 v10.0', E_USER_DEPRECATED);
1456 return $GLOBALS['TCA'][$table]['ctrl']['delete'] ? ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0' : '';
1457 }
1458
1459 /**
1460 * Returns a part of a WHERE clause which will filter out records with start/end
1461 * times or hidden/fe_groups fields set to values that should de-select them
1462 * according to the current time, preview settings or user login. Definitely a
1463 * frontend function.
1464 *
1465 * Is using the $GLOBALS['TCA'] arrays "ctrl" part where the key "enablefields"
1466 * determines for each table which of these features applies to that table.
1467 *
1468 * @param string $table Table name found in the $GLOBALS['TCA'] array
1469 * @param int $show_hidden If $show_hidden is set (0/1), any hidden-fields in records are ignored. NOTICE: If you call this function, consider what to do with the show_hidden parameter. Maybe it should be set? See ContentObjectRenderer->enableFields where it's implemented correctly.
1470 * @param array $ignore_array Array you can pass where keys can be "disabled", "starttime", "endtime", "fe_group" (keys from "enablefields" in TCA) and if set they will make sure that part of the clause is not added. Thus disables the specific part of the clause. For previewing etc.
1471 * @param bool $noVersionPreview If set, enableFields will be applied regardless of any versioning preview settings which might otherwise disable enableFields
1472 * @throws \InvalidArgumentException
1473 * @return string The clause starting like " AND ...=... AND ...=...
1474 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::enableFields()
1475 */
1476 public function enableFields($table, $show_hidden = -1, $ignore_array = [], $noVersionPreview = false)
1477 {
1478 if ($show_hidden === -1) {
1479 // If show_hidden was not set from outside, use the current context
1480 $show_hidden = (int)$this->context->getPropertyFromAspect('visibility', $table === 'pages' ? 'includeHiddenPages' : 'includeHiddenContent', false);
1481 }
1482 // If show_hidden was not changed during the previous evaluation, do it here.
1483 $ctrl = $GLOBALS['TCA'][$table]['ctrl'] ?? null;
1484 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1485 ->getQueryBuilderForTable($table)
1486 ->expr();
1487 $constraints = [];
1488 if (is_array($ctrl)) {
1489 // Delete field check:
1490 if ($ctrl['delete']) {
1491 $constraints[] = $expressionBuilder->eq($table . '.' . $ctrl['delete'], 0);
1492 }
1493 if ($ctrl['versioningWS']) {
1494 if (!$this->versioningWorkspaceId) {
1495 // Filter out placeholder records (new/moved/deleted items)
1496 // in case we are NOT in a versioning preview (that means we are online!)
1497 $constraints[] = $expressionBuilder->lte(
1498 $table . '.t3ver_state',
1499 new VersionState(VersionState::DEFAULT_STATE)
1500 );
1501 } elseif ($table !== 'pages') {
1502 // show only records of live and of the current workspace
1503 // in case we are in a versioning preview
1504 $constraints[] = $expressionBuilder->orX(
1505 $expressionBuilder->eq($table . '.t3ver_wsid', 0),
1506 $expressionBuilder->eq($table . '.t3ver_wsid', (int)$this->versioningWorkspaceId)
1507 );
1508 }
1509
1510 // Filter out versioned records
1511 if (!$noVersionPreview && empty($ignore_array['pid'])) {
1512 $constraints[] = $expressionBuilder->neq($table . '.pid', -1);
1513 }
1514 }
1515
1516 // Enable fields:
1517 if (is_array($ctrl['enablecolumns'])) {
1518 // In case of versioning-preview, enableFields are ignored (checked in
1519 // versionOL())
1520 if (!$this->versioningWorkspaceId || !$ctrl['versioningWS'] || $noVersionPreview) {
1521 if (($ctrl['enablecolumns']['disabled'] ?? false) && !$show_hidden && !($ignore_array['disabled'] ?? false)) {
1522 $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
1523 $constraints[] = $expressionBuilder->eq($field, 0);
1524 }
1525 if (($ctrl['enablecolumns']['starttime'] ?? false) && !($ignore_array['starttime'] ?? false)) {
1526 $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
1527 $constraints[] = $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']);
1528 }
1529 if (($ctrl['enablecolumns']['endtime'] ?? false) && !($ignore_array['endtime'] ?? false)) {
1530 $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
1531 $constraints[] = $expressionBuilder->orX(
1532 $expressionBuilder->eq($field, 0),
1533 $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
1534 );
1535 }
1536 if ($ctrl['enablecolumns']['fe_group'] && !$ignore_array['fe_group']) {
1537 $field = $table . '.' . $ctrl['enablecolumns']['fe_group'];
1538 $constraints[] = QueryHelper::stripLogicalOperatorPrefix(
1539 $this->getMultipleGroupsWhereClause($field, $table)
1540 );
1541 }
1542 // Call hook functions for additional enableColumns
1543 // It is used by the extension ingmar_accessctrl which enables assigning more
1544 // than one usergroup to content and page records
1545 $_params = [
1546 'table' => $table,
1547 'show_hidden' => $show_hidden,
1548 'ignore_array' => $ignore_array,
1549 'ctrl' => $ctrl
1550 ];
1551 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'] ?? [] as $_funcRef) {
1552 $constraints[] = QueryHelper::stripLogicalOperatorPrefix(
1553 GeneralUtility::callUserFunction($_funcRef, $_params, $this)
1554 );
1555 }
1556 }
1557 }
1558 } else {
1559 throw new \InvalidArgumentException('There is no entry in the $TCA array for the table "' . $table . '". This means that the function enableFields() is ' . 'called with an invalid table name as argument.', 1283790586);
1560 }
1561
1562 return empty($constraints) ? '' : ' AND ' . $expressionBuilder->andX(...$constraints);
1563 }
1564
1565 /**
1566 * Creating where-clause for checking group access to elements in enableFields
1567 * function
1568 *
1569 * @param string $field Field with group list
1570 * @param string $table Table name
1571 * @return string AND sql-clause
1572 * @see enableFields()
1573 */
1574 public function getMultipleGroupsWhereClause($field, $table)
1575 {
1576 if (!$this->context->hasAspect('frontend.user')) {
1577 return '';
1578 }
1579 /** @var UserAspect $userAspect */
1580 $userAspect = $this->context->getAspect('frontend.user');
1581 $memberGroups = $userAspect->getGroupIds();
1582
1583 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1584 ->getQueryBuilderForTable($table)
1585 ->expr();
1586 $orChecks = [];
1587 // If the field is empty, then OK
1588 $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal(''));
1589 // If the field is NULL, then OK
1590 $orChecks[] = $expressionBuilder->isNull($field);
1591 // If the field contains zero, then OK
1592 $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal('0'));
1593 foreach ($memberGroups as $value) {
1594 $orChecks[] = $expressionBuilder->inSet($field, $expressionBuilder->literal($value));
1595 }
1596
1597 return' AND (' . $expressionBuilder->orX(...$orChecks) . ')';
1598 }
1599
1600 /**********************
1601 *
1602 * Versioning Preview
1603 *
1604 **********************/
1605
1606 /**
1607 * Finding online PID for offline version record
1608 *
1609 * ONLY active when backend user is previewing records. MUST NEVER affect a site
1610 * served which is not previewed by backend users!!!
1611 *
1612 * Will look if the "pid" value of the input record is -1 (it is an offline
1613 * version) and if the table supports versioning; if so, it will translate the -1
1614 * PID into the PID of the original record.
1615 *
1616 * Used whenever you are tracking something back, like making the root line.
1617 *
1618 * Principle; Record offline! => Find online?
1619 *
1620 * @param string $table Table name
1621 * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid" and "t3ver_wsid" is nice and will save you a DB query.
1622 * @see BackendUtility::fixVersioningPid(), versionOL(), getRootLine()
1623 */
1624 public function fixVersioningPid($table, &$rr)
1625 {
1626 if ($this->versioningWorkspaceId && is_array($rr) && (int)$rr['pid'] === -1 && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1627 $oid = 0;
1628 $wsid = 0;
1629 // Check values for t3ver_oid and t3ver_wsid:
1630 if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid'])) {
1631 // If "t3ver_oid" is already a field, just set this:
1632 $oid = $rr['t3ver_oid'];
1633 $wsid = $rr['t3ver_wsid'];
1634 } else {
1635 // Otherwise we have to expect "uid" to be in the record and look up based
1636 // on this:
1637 $uid = (int)$rr['uid'];
1638 if ($uid > 0) {
1639 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1640 $queryBuilder->getRestrictions()
1641 ->removeAll()
1642 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1643 $newPidRec = $queryBuilder->select('t3ver_oid', 't3ver_wsid')
1644 ->from($table)
1645 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
1646 ->execute()
1647 ->fetch();
1648
1649 if (is_array($newPidRec)) {
1650 $oid = $newPidRec['t3ver_oid'];
1651 $wsid = $newPidRec['t3ver_wsid'];
1652 }
1653 }
1654 }
1655 // If workspace ids matches and ID of current online version is found, look up
1656 // the PID value of that:
1657 if ($oid && ((int)$this->versioningWorkspaceId === 0 && $this->checkWorkspaceAccess($wsid) || (int)$wsid === (int)$this->versioningWorkspaceId)) {
1658 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1659 $queryBuilder->getRestrictions()
1660 ->removeAll()
1661 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1662 $oidRec = $queryBuilder->select('pid')
1663 ->from($table)
1664 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($oid, \PDO::PARAM_INT)))
1665 ->execute()
1666 ->fetch();
1667
1668 if (is_array($oidRec)) {
1669 // SWAP uid as well? Well no, because when fixing a versioning PID happens it is
1670 // assumed that this is a "branch" type page and therefore the uid should be
1671 // kept (like in versionOL()). However if the page is NOT a branch version it
1672 // should not happen - but then again, direct access to that uid should not
1673 // happen!
1674 $rr['_ORIG_pid'] = $rr['pid'];
1675 $rr['pid'] = $oidRec['pid'];
1676 }
1677 }
1678 }
1679 // Changing PID in case of moving pointer:
1680 if ($movePlhRec = $this->getMovePlaceholder($table, $rr['uid'], 'pid')) {
1681 $rr['pid'] = $movePlhRec['pid'];
1682 }
1683 }
1684
1685 /**
1686 * Versioning Preview Overlay
1687 *
1688 * ONLY active when backend user is previewing records. MUST NEVER affect a site
1689 * served which is not previewed by backend users!!!
1690 *
1691 * Generally ALWAYS used when records are selected based on uid or pid. If
1692 * records are selected on other fields than uid or pid (eg. "email = ....") then
1693 * usage might produce undesired results and that should be evaluated on
1694 * individual basis.
1695 *
1696 * Principle; Record online! => Find offline?
1697 *
1698 * @param string $table Table name
1699 * @param array $row Record array passed by reference. As minimum, the "uid", "pid" and "t3ver_state" fields must exist! The record MAY be set to FALSE in which case the calling function should act as if the record is forbidden to access!
1700 * @param bool $unsetMovePointers If set, the $row is cleared in case it is a move-pointer. This is only for preview of moved records (to remove the record from the original location so it appears only in the new location)
1701 * @param bool $bypassEnableFieldsCheck Unless this option is TRUE, the $row is unset if enablefields for BOTH the version AND the online record deselects it. This is because when versionOL() is called it is assumed that the online record is already selected with no regards to it's enablefields. However, after looking for a new version the online record enablefields must ALSO be evaluated of course. This is done all by this function!
1702 * @see fixVersioningPid(), BackendUtility::workspaceOL()
1703 */
1704 public function versionOL($table, &$row, $unsetMovePointers = false, $bypassEnableFieldsCheck = false)
1705 {
1706 if ($this->versioningWorkspaceId && is_array($row)) {
1707 // will overlay any movePlhOL found with the real record, which in turn
1708 // will be overlaid with its workspace version if any.
1709 $movePldSwap = $this->movePlhOL($table, $row);
1710 // implode(',',array_keys($row)) = Using fields from original record to make
1711 // sure no additional fields are selected. This is best for eg. getPageOverlay()
1712 // Computed properties are excluded since those would lead to SQL errors.
1713 $fieldNames = implode(',', array_keys($this->purgeComputedProperties($row)));
1714 if ($wsAlt = $this->getWorkspaceVersionOfRecord($this->versioningWorkspaceId, $table, $row['uid'], $fieldNames, $bypassEnableFieldsCheck)) {
1715 if (is_array($wsAlt)) {
1716 // Always fix PID (like in fixVersioningPid() above). [This is usually not
1717 // the important factor for versioning OL]
1718 // Keep the old (-1) - indicates it was a version...
1719 $wsAlt['_ORIG_pid'] = $wsAlt['pid'];
1720 // Set in the online versions PID.
1721 $wsAlt['pid'] = $row['pid'];
1722 // For versions of single elements or page+content, preserve online UID and PID
1723 // (this will produce true "overlay" of element _content_, not any references)
1724 // For page+content the "_ORIG_uid" should actually be used as PID for selection.
1725 $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
1726 $wsAlt['uid'] = $row['uid'];
1727 // Translate page alias as well so links are pointing to the _online_ page:
1728 if ($table === 'pages') {
1729 $wsAlt['alias'] = $row['alias'];
1730 }
1731 // Changing input record to the workspace version alternative:
1732 $row = $wsAlt;
1733 // Check if it is deleted/new
1734 $rowVersionState = VersionState::cast($row['t3ver_state']);
1735 if (
1736 $rowVersionState->equals(VersionState::NEW_PLACEHOLDER)
1737 || $rowVersionState->equals(VersionState::DELETE_PLACEHOLDER)
1738 ) {
1739 // Unset record if it turned out to be deleted in workspace
1740 $row = false;
1741 }
1742 // Check if move-pointer in workspace (unless if a move-placeholder is the
1743 // reason why it appears!):
1744 // You have to specifically set $unsetMovePointers in order to clear these
1745 // because it is normally a display issue if it should be shown or not.
1746 if (
1747 (
1748 $rowVersionState->equals(VersionState::MOVE_POINTER)
1749 && !$movePldSwap
1750 ) && $unsetMovePointers
1751 ) {
1752 // Unset record if it turned out to be deleted in workspace
1753 $row = false;
1754 }
1755 } else {
1756 // No version found, then check if t3ver_state = VersionState::NEW_PLACEHOLDER
1757 // (online version is dummy-representation)
1758 // Notice, that unless $bypassEnableFieldsCheck is TRUE, the $row is unset if
1759 // enablefields for BOTH the version AND the online record deselects it. See
1760 // note for $bypassEnableFieldsCheck
1761 /** @var \TYPO3\CMS\Core\Versioning\VersionState $versionState */
1762 $versionState = VersionState::cast($row['t3ver_state']);
1763 if ($wsAlt <= -1 || $versionState->indicatesPlaceholder()) {
1764 // Unset record if it turned out to be "hidden"
1765 $row = false;
1766 }
1767 }
1768 }
1769 }
1770 }
1771
1772 /**
1773 * Checks if record is a move-placeholder
1774 * (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so it will set $row to be
1775 * the pointed-to live record (and return TRUE) Used from versionOL
1776 *
1777 * @param string $table Table name
1778 * @param array $row Row (passed by reference) - only online records...
1779 * @return bool TRUE if overlay is made.
1780 * @see BackendUtility::movePlhOl()
1781 */
1782 public function movePlhOL($table, &$row)
1783 {
1784 if (!empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
1785 && (int)VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER)
1786 ) {
1787 $moveID = 0;
1788 // If t3ver_move_id is not found, then find it (but we like best if it is here)
1789 if (!isset($row['t3ver_move_id'])) {
1790 if ((int)$row['uid'] > 0) {
1791 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1792 $queryBuilder->getRestrictions()
1793 ->removeAll()
1794 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1795 $moveIDRec = $queryBuilder->select('t3ver_move_id')
1796 ->from($table)
1797 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT)))
1798 ->execute()
1799 ->fetch();
1800
1801 if (is_array($moveIDRec)) {
1802 $moveID = $moveIDRec['t3ver_move_id'];
1803 }
1804 }
1805 } else {
1806 $moveID = $row['t3ver_move_id'];
1807 }
1808 // Find pointed-to record.
1809 if ($moveID) {
1810 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1811 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1812 $origRow = $queryBuilder->select(...array_keys($this->purgeComputedProperties($row)))
1813 ->from($table)
1814 ->where(
1815 $queryBuilder->expr()->eq(
1816 'uid',
1817 $queryBuilder->createNamedParameter($moveID, \PDO::PARAM_INT)
1818 )
1819 )
1820 ->setMaxResults(1)
1821 ->execute()
1822 ->fetch();
1823
1824 if ($origRow) {
1825 $row = $origRow;
1826 return true;
1827 }
1828 }
1829 }
1830 return false;
1831 }
1832
1833 /**
1834 * Returns move placeholder of online (live) version
1835 *
1836 * @param string $table Table name
1837 * @param int $uid Record UID of online version
1838 * @param string $fields Field list, default is *
1839 * @return array If found, the record, otherwise nothing.
1840 * @see BackendUtility::getMovePlaceholder()
1841 */
1842 public function getMovePlaceholder($table, $uid, $fields = '*')
1843 {
1844 $workspace = (int)$this->versioningWorkspaceId;
1845 if (!empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $workspace !== 0) {
1846 // Select workspace version of record:
1847 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1848 $queryBuilder->getRestrictions()
1849 ->removeAll()
1850 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1851
1852 $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
1853 ->from($table)
1854 ->where(
1855 $queryBuilder->expr()->neq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
1856 $queryBuilder->expr()->eq(
1857 't3ver_state',
1858 $queryBuilder->createNamedParameter(
1859 (string)VersionState::cast(VersionState::MOVE_PLACEHOLDER),
1860 \PDO::PARAM_INT
1861 )
1862 ),
1863 $queryBuilder->expr()->eq(
1864 't3ver_move_id',
1865 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1866 ),
1867 $queryBuilder->expr()->eq(
1868 't3ver_wsid',
1869 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
1870 )
1871 )
1872 ->setMaxResults(1)
1873 ->execute()
1874 ->fetch();
1875
1876 if (is_array($row)) {
1877 return $row;
1878 }
1879 }
1880 return false;
1881 }
1882
1883 /**
1884 * Select the version of a record for a workspace
1885 *
1886 * @param int $workspace Workspace ID
1887 * @param string $table Table name to select from
1888 * @param int $uid Record uid for which to find workspace version.
1889 * @param string $fields Field list to select
1890 * @param bool $bypassEnableFieldsCheck If TRUE, enablefields are not checked for.
1891 * @return mixed If found, return record, otherwise other value: Returns 1 if version was sought for but not found, returns -1/-2 if record (offline/online) existed but had enableFields that would disable it. Returns FALSE if not in workspace or no versioning for record. Notice, that the enablefields of the online record is also tested.
1892 * @see BackendUtility::getWorkspaceVersionOfRecord()
1893 */
1894 public function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*', $bypassEnableFieldsCheck = false)
1895 {
1896 if ($workspace !== 0 && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])) {
1897 $workspace = (int)$workspace;
1898 $uid = (int)$uid;
1899 // Select workspace version of record, only testing for deleted.
1900 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1901 $queryBuilder->getRestrictions()
1902 ->removeAll()
1903 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1904
1905 $newrow = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
1906 ->from($table)
1907 ->where(
1908 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
1909 $queryBuilder->expr()->eq(
1910 't3ver_oid',
1911 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1912 ),
1913 $queryBuilder->expr()->eq(
1914 't3ver_wsid',
1915 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
1916 )
1917 )
1918 ->setMaxResults(1)
1919 ->execute()
1920 ->fetch();
1921
1922 // If version found, check if it could have been selected with enableFields on
1923 // as well:
1924 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1925 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1926 // Remove the frontend workspace restriction because we are testing a version record
1927 $queryBuilder->getRestrictions()->removeByType(FrontendWorkspaceRestriction::class);
1928 $queryBuilder->select('uid')
1929 ->from($table)
1930 ->setMaxResults(1);
1931
1932 if (is_array($newrow)) {
1933 $queryBuilder->where(
1934 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)),
1935 $queryBuilder->expr()->eq(
1936 't3ver_oid',
1937 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
1938 ),
1939 $queryBuilder->expr()->eq(
1940 't3ver_wsid',
1941 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT)
1942 )
1943 );
1944 if ($bypassEnableFieldsCheck || $queryBuilder->execute()->fetchColumn()) {
1945 // Return offline version, tested for its enableFields.
1946 return $newrow;
1947 }
1948 // Return -1 because offline version was de-selected due to its enableFields.
1949 return -1;
1950 }
1951 // OK, so no workspace version was found. Then check if online version can be
1952 // selected with full enable fields and if so, return 1:
1953 $queryBuilder->where(
1954 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
1955 );
1956 if ($bypassEnableFieldsCheck || $queryBuilder->execute()->fetchColumn()) {
1957 // Means search was done, but no version found.
1958 return 1;
1959 }
1960 // Return -2 because the online record was de-selected due to its enableFields.
1961 return -2;
1962 }
1963 // No look up in database because versioning not enabled / or workspace not
1964 // offline
1965 return false;
1966 }
1967
1968 /**
1969 * Checks if user has access to workspace.
1970 *
1971 * @param int $wsid Workspace ID
1972 * @return bool true if the backend user has access to a certain workspace
1973 */
1974 public function checkWorkspaceAccess($wsid)
1975 {
1976 if (!$this->getBackendUser() || !ExtensionManagementUtility::isLoaded('workspaces')) {
1977 return false;
1978 }
1979 if (!isset($this->workspaceCache[$wsid])) {
1980 $this->workspaceCache[$wsid] = $this->getBackendUser()->checkWorkspace($wsid);
1981 }
1982 return (string)$this->workspaceCache[$wsid]['_ACCESS'] !== '';
1983 }
1984
1985 /**
1986 * Gets file references for a given record field.
1987 *
1988 * @param string $tableName Name of the table
1989 * @param string $fieldName Name of the field
1990 * @param array $element The parent element referencing to files
1991 * @return array
1992 */
1993 public function getFileReferences($tableName, $fieldName, array $element)
1994 {
1995 /** @var $fileRepository FileRepository */
1996 $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
1997 $currentId = !empty($element['uid']) ? $element['uid'] : 0;
1998
1999 // Fetch the references of the default element
2000 try {
2001 $references = $fileRepository->findByRelation($tableName, $fieldName, $currentId);
2002 } catch (FileDoesNotExistException $e) {
2003 /**
2004 * We just catch the exception here
2005 * Reasoning: There is nothing an editor or even admin could do
2006 */
2007 return [];
2008 } catch (\InvalidArgumentException $e) {
2009 /**
2010 * The storage does not exist anymore
2011 * Log the exception message for admins as they maybe can restore the storage
2012 */
2013 $logMessage = $e->getMessage() . ' (table: "' . $tableName . '", fieldName: "' . $fieldName . '", currentId: ' . $currentId . ')';
2014 $this->logger->error($logMessage, ['exception' => $e]);
2015 return [];
2016 }
2017
2018 $localizedId = null;
2019 if (isset($element['_LOCALIZED_UID'])) {
2020 $localizedId = $element['_LOCALIZED_UID'];
2021 } elseif (isset($element['_PAGES_OVERLAY_UID'])) {
2022 $localizedId = $element['_PAGES_OVERLAY_UID'];
2023 }
2024
2025 $isTableLocalizable = (
2026 !empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])
2027 && !empty($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
2028 );
2029 if ($isTableLocalizable && $localizedId !== null) {
2030 $localizedReferences = $fileRepository->findByRelation($tableName, $fieldName, $localizedId);
2031 $references = $localizedReferences;
2032 }
2033
2034 return $references;
2035 }
2036
2037 /**
2038 * Purges computed properties from database rows,
2039 * such as _ORIG_uid or _ORIG_pid for instance.
2040 *
2041 * @param array $row
2042 * @return array
2043 */
2044 protected function purgeComputedProperties(array $row)
2045 {
2046 foreach ($this->computedPropertyNames as $computedPropertyName) {
2047 if (array_key_exists($computedPropertyName, $row)) {
2048 unset($row[$computedPropertyName]);
2049 }
2050 }
2051 return $row;
2052 }
2053
2054 /**
2055 * Returns the current BE user.
2056 *
2057 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2058 */
2059 protected function getBackendUser()
2060 {
2061 return $GLOBALS['BE_USER'];
2062 }
2063 }