‪TYPO3CMS  11.5
SlugService.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Log\LoggerAwareInterface;
21 use Psr\Log\LoggerAwareTrait;
22 use TYPO3\CMS\Backend\Utility\BackendUtility;
27 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
41 
45 class ‪SlugService implements LoggerAwareInterface
46 {
47  use LoggerAwareTrait;
48 
52  public const ‪CORRELATION_ID_IDENTIFIER = '5d8e6e70';
53 
57  protected ‪$context;
58 
62  protected ‪$site;
63 
67  protected ‪$siteFinder;
68 
72  protected ‪$pageRepository;
73 
75 
79  protected ‪$correlationIdRedirectCreation = '';
80 
84  protected ‪$correlationIdSlugUpdate = '';
85 
89  protected ‪$autoUpdateSlugs;
90 
95 
99  protected ‪$redirectTTL;
100 
104  protected ‪$httpStatusCode;
105 
107 
108  public function ‪__construct(
114  ) {
115  $this->context = ‪$context;
116  $this->siteFinder = ‪$siteFinder;
117  $this->pageRepository = ‪$pageRepository;
118  $this->linkService = ‪$linkService;
119  $this->redirectCacheService = ‪$redirectCacheService;
120  }
121 
122  public function ‪rebuildSlugsForSlugChange(int $pageId, string $currentSlug, string $newSlug, ‪CorrelationId $correlationId): void
123  {
124  $currentPageRecord = BackendUtility::getRecord('pages', $pageId);
125  if ($currentPageRecord === null) {
126  return;
127  }
128  $defaultPageId = (int)$currentPageRecord['sys_language_uid'] > 0 ? (int)$currentPageRecord['l10n_parent'] : $pageId;
129  $this->‪initializeSettings($defaultPageId);
130  if ($this->autoUpdateSlugs || $this->autoCreateRedirects) {
131  $createdRedirect = null;
132  $this->‪createCorrelationIds($pageId, $correlationId);
133  if ($this->autoCreateRedirects) {
134  $createdRedirect = $this->‪createRedirect($currentSlug, $defaultPageId, (int)$currentPageRecord['sys_language_uid'], $pageId);
135  }
136  if ($this->autoUpdateSlugs) {
137  $this->‪checkSubPages($currentPageRecord, $currentSlug, $newSlug);
138  }
139  $this->‪sendNotification();
140  // rebuild caches only for matched source hosts
141  if ($createdRedirect) {
142  $this->redirectCacheService->rebuildForHost($createdRedirect['source_host'] ?: '*');
143  }
144  }
145  }
146 
147  protected function ‪initializeSettings(int $pageId): void
148  {
149  $this->site = $this->siteFinder->getSiteByPageId($pageId);
150  $settings = $this->site->getConfiguration()['settings']['redirects'] ?? [];
151  $this->autoUpdateSlugs = $settings['autoUpdateSlugs'] ?? true;
152  $this->autoCreateRedirects = $settings['autoCreateRedirects'] ?? true;
153  if (!$this->context->getPropertyFromAspect('workspace', 'isLive')) {
154  $this->autoCreateRedirects = false;
155  }
156  $this->redirectTTL = (int)($settings['redirectTTL'] ?? 0);
157  $this->httpStatusCode = (int)($settings['httpStatusCode'] ?? 307);
158  }
159 
160  protected function ‪createCorrelationIds(int $pageId, CorrelationId $correlationId): void
161  {
162  if ($correlationId->getSubject() === null) {
163  $subject = md5('pages:' . $pageId);
164  $correlationId = $correlationId->withSubject($subject);
165  }
166 
167  $this->correlationIdRedirectCreation = $correlationId->withAspects(self::CORRELATION_ID_IDENTIFIER, 'redirect');
168  $this->correlationIdSlugUpdate = $correlationId->withAspects(self::CORRELATION_ID_IDENTIFIER, 'slug');
169  }
170 
174  protected function ‪createRedirect(string $originalSlug, int $pageId, int $languageId, int $pid): array
175  {
176  $siteLanguage = $this->site->getLanguageById($languageId);
177  $basePath = rtrim($siteLanguage->getBase()->getPath(), '/');
178 
180  $date = $this->context->getAspect('date');
181  $endtime = $date->getDateTime()->modify('+' . $this->redirectTTL . ' days');
182  $targetLink = $this->linkService->asString([
183  'type' => 'page',
184  'pageuid' => $pageId,
185  'parameters' => '_language=' . $languageId,
186  ]);
187  $record = [
188  'pid' => $pid,
189  'updatedon' => $date->get('timestamp'),
190  'createdon' => $date->get('timestamp'),
191  'createdby' => $this->context->getPropertyFromAspect('backend.user', 'id'),
192  'deleted' => 0,
193  'disabled' => 0,
194  'starttime' => 0,
195  'endtime' => $this->redirectTTL > 0 ? $endtime->getTimestamp() : 0,
196  'source_host' => $siteLanguage->getBase()->getHost() ?: '*',
197  'source_path' => $basePath . $originalSlug,
198  'is_regexp' => 0,
199  'force_https' => 0,
200  'respect_query_parameters' => 0,
201  'target' => $targetLink,
202  'target_statuscode' => ‪$this->httpStatusCode,
203  'hitcount' => 0,
204  'lasthiton' => 0,
205  'disable_hitcount' => 0,
206  ];
207  //todo use dataHandler to create records
208  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
209  ->getConnectionForTable('sys_redirect');
210  $connection->insert('sys_redirect', $record);
211  $id = (int)$connection->lastInsertId('sys_redirect');
212  $record['uid'] = $id;
213  $this->‪getRecordHistoryStore()->addRecord('sys_redirect', $id, $record, $this->correlationIdRedirectCreation);
214  return $record;
215  }
216 
217  protected function ‪checkSubPages(array $currentPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage): void
218  {
219  $languageUid = (int)$currentPageRecord['sys_language_uid'];
220  // resolveSubPages needs the page id of the default language
221  $pageId = $languageUid === 0 ? (int)$currentPageRecord['uid'] : (int)$currentPageRecord['l10n_parent'];
222  $subPageRecords = $this->‪resolveSubPages($pageId, $languageUid);
223  foreach ($subPageRecords as $subPageRecord) {
224  $newSlug = $this->‪updateSlug($subPageRecord, $oldSlugOfParentPage, $newSlugOfParentPage);
225  if ($newSlug !== null && $this->autoCreateRedirects) {
226  $subPageId = (int)$subPageRecord['sys_language_uid'] === 0 ? (int)$subPageRecord['uid'] : (int)$subPageRecord['l10n_parent'];
227  $this->‪createRedirect($subPageRecord['slug'], $subPageId, $languageUid, $pageId);
228  }
229  }
230  }
231 
232  protected function ‪resolveSubPages(int $id, int $languageUid): array
233  {
234  // First resolve all sub-pages in default language
235  $queryBuilder = $this->‪getQueryBuilderForPages();
236  $subPages = $queryBuilder
237  ->select('*')
238  ->from('pages')
239  ->where(
240  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)),
241  $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT))
242  )
243  ->orderBy('uid', 'ASC')
244  ->executeQuery()
245  ->fetchAllAssociative();
246 
247  // if the language is not the default language, resolve the language related records.
248  if ($languageUid > 0) {
249  $queryBuilder = $this->‪getQueryBuilderForPages();
250  $subPages = $queryBuilder
251  ->select('*')
252  ->from('pages')
253  ->where(
254  $queryBuilder->expr()->in('l10n_parent', $queryBuilder->createNamedParameter(array_column($subPages, 'uid'), Connection::PARAM_INT_ARRAY)),
255  $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($languageUid, ‪Connection::PARAM_INT))
256  )
257  ->orderBy('uid', 'ASC')
258  ->executeQuery()
259  ->fetchAllAssociative();
260  }
261  $results = [];
262  if (!empty($subPages)) {
263  $subPages = $this->pageRepository->getPagesOverlay($subPages, $languageUid);
264  foreach ($subPages as $subPage) {
265  $results[] = $subPage;
266  // resolveSubPages needs the page id of the default language
267  $pageId = $languageUid === 0 ? (int)$subPage['uid'] : (int)$subPage['l10n_parent'];
268  foreach ($this->‪resolveSubPages($pageId, $languageUid) as $page) {
269  $results[] = $page;
270  }
271  }
272  }
273  return $results;
274  }
275 
280  protected function ‪updateSlug(array $subPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage): ?string
281  {
282  if (strpos($subPageRecord['slug'], $oldSlugOfParentPage) !== 0) {
283  return null;
284  }
285 
286  $newSlug = rtrim($newSlugOfParentPage, '/') . '/'
287  . substr($subPageRecord['slug'], strlen(rtrim($oldSlugOfParentPage, '/') . '/'));
288  $state = ‪RecordStateFactory::forName('pages')
289  ->fromArray($subPageRecord, $subPageRecord['pid'], $subPageRecord['uid']);
290  $fieldConfig = ‪$GLOBALS['TCA']['pages']['columns']['slug']['config'] ?? [];
291  $slugHelper = GeneralUtility::makeInstance(SlugHelper::class, 'pages', 'slug', $fieldConfig);
292 
293  if (!$slugHelper->isUniqueInSite($newSlug, $state)) {
294  $newSlug = $slugHelper->buildSlugForUniqueInSite($newSlug, $state);
295  }
296 
297  $this->‪persistNewSlug((int)$subPageRecord['uid'], $newSlug);
298  return $newSlug;
299  }
300 
301  protected function ‪persistNewSlug(int $uid, string $newSlug): void
302  {
303  $this->‪disableHook();
304  $data = [];
305  $data['pages'][$uid]['slug'] = $newSlug;
306  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
307  $dataHandler->start($data, []);
308  $dataHandler->setCorrelationId($this->correlationIdSlugUpdate);
309  $dataHandler->process_datamap();
310  $this->‪enabledHook();
311  }
312 
313  protected function ‪sendNotification(): void
314  {
315  $data = [
316  'componentName' => 'redirects',
317  'eventName' => 'slugChanged',
318  'correlations' => [
319  'correlationIdSlugUpdate' => (string)$this->correlationIdSlugUpdate,
320  'correlationIdRedirectCreation' => (string)‪$this->correlationIdRedirectCreation,
321  ],
322  'autoUpdateSlugs' => (bool)$this->autoUpdateSlugs,
323  'autoCreateRedirects' => (bool)‪$this->autoCreateRedirects,
324  ];
325  BackendUtility::setUpdateSignal('redirects:slugChanged', $data);
326  }
327 
328  protected function ‪getRecordHistoryStore(): ‪RecordHistoryStore
329  {
330  $backendUser = ‪$GLOBALS['BE_USER'];
331  return GeneralUtility::makeInstance(
332  RecordHistoryStore::class,
334  (int)$backendUser->user['uid'],
335  (int)$backendUser->getOriginalUserIdWhenInSwitchUserMode(),
336  $this->context->getPropertyFromAspect('date', 'timestamp'),
337  $backendUser->workspace ?? 0
338  );
339  }
340 
341  protected function ‪getQueryBuilderForPages(): QueryBuilder
342  {
343  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
344  ->getQueryBuilderForTable('pages');
346  $queryBuilder
347  ->getRestrictions()
348  ->removeAll()
349  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
350  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->context->getPropertyFromAspect('workspace', 'id')));
351  return $queryBuilder;
352  }
353 
354  protected function ‪enabledHook(): void
355  {
356  ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects'] =
357  DataHandlerSlugUpdateHook::class;
358  }
359 
360  protected function ‪disableHook(): void
361  {
362  unset(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects']);
363  }
364 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:86
‪TYPO3\CMS\Core\Site\Entity\SiteInterface
Definition: SiteInterface.php:26
‪TYPO3\CMS\Redirects\Service\SlugService\CORRELATION_ID_IDENTIFIER
‪const CORRELATION_ID_IDENTIFIER
Definition: SlugService.php:52
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\USER_BACKEND
‪const USER_BACKEND
Definition: RecordHistoryStore.php:39
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId\getSubject
‪string null getSubject()
Definition: CorrelationId.php:144
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Redirects\Service\SlugService\getRecordHistoryStore
‪getRecordHistoryStore()
Definition: SlugService.php:318
‪TYPO3\CMS\Redirects\Service\SlugService\$redirectCacheService
‪RedirectCacheService $redirectCacheService
Definition: SlugService.php:96
‪TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook
Definition: DataHandlerSlugUpdateHook.php:29
‪TYPO3\CMS\Redirects\Service\SlugService\$autoUpdateSlugs
‪bool $autoUpdateSlugs
Definition: SlugService.php:82
‪TYPO3\CMS\Redirects\Service\SlugService\createRedirect
‪array createRedirect(string $originalSlug, int $pageId, int $languageId, int $pid)
Definition: SlugService.php:164
‪TYPO3\CMS\Redirects\Service\SlugService\$redirectTTL
‪int $redirectTTL
Definition: SlugService.php:90
‪TYPO3\CMS\Redirects\Service\SlugService\$context
‪Context $context
Definition: SlugService.php:56
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Redirects\Service\SlugService\sendNotification
‪sendNotification()
Definition: SlugService.php:303
‪TYPO3\CMS\Redirects\Service\SlugService\resolveSubPages
‪resolveSubPages(int $id, int $languageUid)
Definition: SlugService.php:222
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId\withAspects
‪withAspects(string ... $aspects)
Definition: CorrelationId.php:123
‪TYPO3\CMS\Redirects\Service\SlugService\rebuildSlugsForSlugChange
‪rebuildSlugsForSlugChange(int $pageId, string $currentSlug, string $newSlug, CorrelationId $correlationId)
Definition: SlugService.php:112
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Redirects\Service\SlugService\checkSubPages
‪checkSubPages(array $currentPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage)
Definition: SlugService.php:207
‪TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory
Definition: RecordStateFactory.php:26
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Redirects\Service\RedirectCacheService
Definition: RedirectCacheService.php:34
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId
Definition: CorrelationId.php:29
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId\withSubject
‪withSubject(string $subject)
Definition: CorrelationId.php:113
‪TYPO3\CMS\Core\DataHandling\SlugHelper
Definition: SlugHelper.php:43
‪TYPO3\CMS\Redirects\Service\SlugService\$correlationIdSlugUpdate
‪CorrelationId string $correlationIdSlugUpdate
Definition: SlugService.php:78
‪TYPO3\CMS\Redirects\Service\SlugService\$linkService
‪LinkService $linkService
Definition: SlugService.php:70
‪TYPO3\CMS\Redirects\Service\SlugService\initializeSettings
‪initializeSettings(int $pageId)
Definition: SlugService.php:137
‪TYPO3\CMS\Redirects\Service\SlugService\$httpStatusCode
‪int $httpStatusCode
Definition: SlugService.php:94
‪TYPO3\CMS\Redirects\Service\SlugService\$siteFinder
‪SiteFinder $siteFinder
Definition: SlugService.php:64
‪TYPO3\CMS\Redirects\Service\SlugService\$site
‪SiteInterface $site
Definition: SlugService.php:60
‪TYPO3\CMS\Redirects\Service\SlugService\$pageRepository
‪PageRepository $pageRepository
Definition: SlugService.php:68
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Redirects\Service\SlugService\getQueryBuilderForPages
‪getQueryBuilderForPages()
Definition: SlugService.php:331
‪TYPO3\CMS\Redirects\Service
Definition: IntegrityService.php:18
‪TYPO3\CMS\Redirects\Service\SlugService\$autoCreateRedirects
‪bool $autoCreateRedirects
Definition: SlugService.php:86
‪TYPO3\CMS\Redirects\Service\SlugService\persistNewSlug
‪persistNewSlug(int $uid, string $newSlug)
Definition: SlugService.php:291
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Redirects\Service\SlugService\$correlationIdRedirectCreation
‪CorrelationId string $correlationIdRedirectCreation
Definition: SlugService.php:74
‪TYPO3\CMS\Redirects\Service\SlugService\createCorrelationIds
‪createCorrelationIds(int $pageId, CorrelationId $correlationId)
Definition: SlugService.php:150
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Redirects\Service\SlugService
Definition: SlugService.php:46
‪TYPO3\CMS\Redirects\Service\SlugService\updateSlug
‪updateSlug(array $subPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage)
Definition: SlugService.php:270
‪TYPO3\CMS\Core\Context\DateTimeAspect
Definition: DateTimeAspect.php:35
‪TYPO3\CMS\Redirects\Service\SlugService\disableHook
‪disableHook()
Definition: SlugService.php:350
‪TYPO3\CMS\Redirects\Service\SlugService\__construct
‪__construct(Context $context, SiteFinder $siteFinder, PageRepository $pageRepository, LinkService $linkService, RedirectCacheService $redirectCacheService)
Definition: SlugService.php:98
‪TYPO3\CMS\Redirects\Service\SlugService\enabledHook
‪enabledHook()
Definition: SlugService.php:344
‪TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory\forName
‪static static forName(string $name)
Definition: RecordStateFactory.php:35
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:40