‪TYPO3CMS  10.4
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 Doctrine\DBAL\Connection;
21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerAwareTrait;
43 
47 class ‪SlugService implements LoggerAwareInterface
48 {
49  use LoggerAwareTrait;
50 
54  public const ‪CORRELATION_ID_IDENTIFIER = '5d8e6e70';
55 
59  protected ‪$context;
60 
64  protected ‪$languageService;
65 
69  protected ‪$site;
70 
74  protected ‪$siteFinder;
75 
79  protected ‪$pageRepository;
80 
84  protected ‪$linkService;
85 
89  protected ‪$correlationIdRedirectCreation = '';
90 
95 
99  protected ‪$autoUpdateSlugs;
100 
105 
109  protected ‪$redirectTTL;
110 
114  protected ‪$httpStatusCode;
115 
117  {
118  $this->context = ‪$context;
119  $this->languageService = ‪$languageService;
120  $this->siteFinder = ‪$siteFinder;
121  $this->pageRepository = ‪$pageRepository;
122  $this->linkService = ‪$linkService;
123  }
124 
125  public function ‪rebuildSlugsForSlugChange(int $pageId, string $currentSlug, string $newSlug, ‪CorrelationId $correlationId): void
126  {
127  $currentPageRecord = ‪BackendUtility::getRecord('pages', $pageId);
128  if ($currentPageRecord === null) {
129  return;
130  }
131  $defaultPageId = (int)$currentPageRecord['sys_language_uid'] > 0 ? (int)$currentPageRecord['l10n_parent'] : $pageId;
132  $this->‪initializeSettings($defaultPageId);
133  if ($this->autoUpdateSlugs || $this->autoCreateRedirects) {
134  $this->‪createCorrelationIds($pageId, $correlationId);
135  if ($this->autoCreateRedirects) {
136  $this->‪createRedirect($currentSlug, $defaultPageId, (int)$currentPageRecord['sys_language_uid'], (int)$pageId);
137  }
138  if ($this->autoUpdateSlugs) {
139  $this->‪checkSubPages($currentPageRecord, $currentSlug, $newSlug);
140  }
141  $this->‪sendNotification();
142  GeneralUtility::makeInstance(RedirectCacheService::class)->rebuild();
143  }
144  }
145 
146  protected function ‪initializeSettings(int $pageId): void
147  {
148  $this->site = $this->siteFinder->getSiteByPageId($pageId);
149  $settings = $this->site->getConfiguration()['settings']['redirects'] ?? [];
150  $this->autoUpdateSlugs = $settings['autoUpdateSlugs'] ?? true;
151  $this->autoCreateRedirects = $settings['autoCreateRedirects'] ?? true;
152  if (!$this->context->getPropertyFromAspect('workspace', 'isLive')) {
153  $this->autoCreateRedirects = false;
154  }
155  $this->redirectTTL = (int)($settings['redirectTTL'] ?? 0);
156  $this->httpStatusCode = (int)($settings['httpStatusCode'] ?? 307);
157  }
158 
159  protected function ‪createCorrelationIds(int $pageId, ‪CorrelationId $correlationId): void
160  {
161  if ($correlationId->‪getSubject() === null) {
162  $subject = md5('pages:' . $pageId);
163  $correlationId = $correlationId->‪withSubject($subject);
164  }
165 
166  $this->correlationIdRedirectCreation = $correlationId->‪withAspects(self::CORRELATION_ID_IDENTIFIER, 'redirect');
167  $this->correlationIdSlugUpdate = $correlationId->‪withAspects(self::CORRELATION_ID_IDENTIFIER, 'slug');
168  }
169 
170  protected function ‪createRedirect(string $originalSlug, int $pageId, int $languageId, int $pid): void
171  {
172  $siteLanguage = $this->site->getLanguageById($languageId);
173  $basePath = rtrim($siteLanguage->getBase()->getPath(), '/');
174 
176  $date = $this->context->getAspect('date');
177  $endtime = $date->getDateTime()->modify('+' . $this->redirectTTL . ' days');
178  $targetLink = $this->linkService->asString([
179  'type' => 'page',
180  'pageuid' => $pageId,
181  'parameters' => '_language=' . $languageId
182  ]);
183  $record = [
184  'pid' => 0,
185  'updatedon' => $date->get('timestamp'),
186  'createdon' => $date->get('timestamp'),
187  'createdby' => $this->context->getPropertyFromAspect('backend.user', 'id'),
188  'deleted' => 0,
189  'disabled' => 0,
190  'starttime' => 0,
191  'endtime' => $this->redirectTTL > 0 ? $endtime->getTimestamp() : 0,
192  'source_host' => $siteLanguage->getBase()->getHost() ?: '*',
193  'source_path' => $basePath . $originalSlug,
194  'is_regexp' => 0,
195  'force_https' => 0,
196  'respect_query_parameters' => 0,
197  'target' => $targetLink,
198  'target_statuscode' => ‪$this->httpStatusCode,
199  'hitcount' => 0,
200  'lasthiton' => 0,
201  'disable_hitcount' => 0,
202  ];
203  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
204  ->getConnectionForTable('sys_redirect');
205  $connection->insert('sys_redirect', $record);
206  $id = (int)$connection->lastInsertId('sys_redirect');
207  $record['uid'] = $id;
208  $this->‪getRecordHistoryStore()->addRecord('sys_redirect', $id, $record, $this->correlationIdRedirectCreation);
209  }
210 
211  protected function ‪checkSubPages(array $currentPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage): void
212  {
213  $languageUid = (int)$currentPageRecord['sys_language_uid'];
214  // resolveSubPages needs the page id of the default language
215  $pageId = $languageUid === 0 ? (int)$currentPageRecord['uid'] : (int)$currentPageRecord['l10n_parent'];
216  $subPageRecords = $this->‪resolveSubPages($pageId, $languageUid);
217  foreach ($subPageRecords as $subPageRecord) {
218  $newSlug = $this->‪updateSlug($subPageRecord, $oldSlugOfParentPage, $newSlugOfParentPage);
219  if ($newSlug !== null && $this->autoCreateRedirects) {
220  $subPageId = (int)$subPageRecord['sys_language_uid'] === 0 ? (int)$subPageRecord['uid'] : (int)$subPageRecord['l10n_parent'];
221  $this->‪createRedirect($subPageRecord['slug'], $subPageId, $languageUid, $pageId);
222  }
223  }
224  }
225 
226  protected function ‪resolveSubPages(int $id, int $languageUid): array
227  {
228  // First resolve all sub-pages in default language
229  $queryBuilder = $this->‪getQueryBuilderForPages();
230  $subPages = $queryBuilder
231  ->select('*')
232  ->from('pages')
233  ->where(
234  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)),
235  $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
236  )
237  ->orderBy('uid', 'ASC')
238  ->execute()
239  ->fetchAll();
240 
241  // if the language is not the default language, resolve the language related records.
242  if ($languageUid > 0) {
243  $queryBuilder = $this->‪getQueryBuilderForPages();
244  $subPages = $queryBuilder
245  ->select('*')
246  ->from('pages')
247  ->where(
248  $queryBuilder->expr()->in('l10n_parent', $queryBuilder->createNamedParameter(array_column($subPages, 'uid'), Connection::PARAM_INT_ARRAY)),
249  $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter($languageUid, \PDO::PARAM_INT))
250  )
251  ->orderBy('uid', 'ASC')
252  ->execute()
253  ->fetchAll();
254  }
255  $results = [];
256  if (!empty($subPages)) {
257  $subPages = $this->pageRepository->getPagesOverlay($subPages, $languageUid);
258  foreach ($subPages as $subPage) {
259  $results[] = $subPage;
260  // resolveSubPages needs the page id of the default language
261  $pageId = $languageUid === 0 ? (int)$subPage['uid'] : (int)$subPage['l10n_parent'];
262  foreach ($this->‪resolveSubPages($pageId, $languageUid) as $page) {
263  $results[] = $page;
264  }
265  }
266  }
267  return $results;
268  }
269 
279  protected function ‪updateSlug(array $subPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage): ?string
280  {
281  if (strpos($subPageRecord['slug'], $oldSlugOfParentPage) !== 0) {
282  return null;
283  }
284 
285  $newSlug = rtrim($newSlugOfParentPage, '/') . '/'
286  . substr($subPageRecord['slug'], strlen(rtrim($oldSlugOfParentPage, '/') . '/'));
287  $state = ‪RecordStateFactory::forName('pages')
288  ->fromArray($subPageRecord, $subPageRecord['pid'], $subPageRecord['uid']);
289  $fieldConfig = ‪$GLOBALS['TCA']['pages']['columns']['slug']['config'] ?? [];
290  $slugHelper = GeneralUtility::makeInstance(SlugHelper::class, 'pages', 'slug', $fieldConfig);
291 
292  if (!$slugHelper->isUniqueInSite($newSlug, $state)) {
293  $newSlug = $slugHelper->buildSlugForUniqueInSite($newSlug, $state);
294  }
295 
296  $this->‪persistNewSlug((int)$subPageRecord['uid'], $newSlug);
297  return $newSlug;
298  }
299 
304  protected function ‪persistNewSlug(int $uid, string $newSlug): void
305  {
306  $this->‪disableHook();
307  $data = [];
308  $data['pages'][$uid]['slug'] = $newSlug;
309  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
310  $dataHandler->start($data, []);
311  $dataHandler->setCorrelationId($this->correlationIdSlugUpdate);
312  $dataHandler->process_datamap();
313  $this->‪enabledHook();
314  }
315 
316  protected function ‪sendNotification(): void
317  {
318  $data = [
319  'componentName' => 'redirects',
320  'eventName' => 'slugChanged',
321  'correlations' => [
322  'correlationIdSlugUpdate' => ‪$this->correlationIdSlugUpdate,
323  'correlationIdRedirectCreation' => ‪$this->correlationIdRedirectCreation,
324  ],
325  'autoUpdateSlugs' => (bool)$this->autoUpdateSlugs,
326  'autoCreateRedirects' => (bool)‪$this->autoCreateRedirects,
327  ];
328  GeneralUtility::makeInstance(PageRenderer::class)->loadRequireJsModule(
329  'TYPO3/CMS/Backend/BroadcastService',
330  sprintf('function(service) { service.post(%s); }', json_encode($data))
331  );
332  }
333 
334  protected function ‪getRecordHistoryStore(): ‪RecordHistoryStore
335  {
336  $backendUser = ‪$GLOBALS['BE_USER'];
337  return GeneralUtility::makeInstance(
338  RecordHistoryStore::class,
340  $backendUser->user['uid'],
341  $backendUser->user['ses_backuserid'] ?? null,
342  $this->context->getPropertyFromAspect('date', 'timestamp'),
343  $backendUser->workspace ?? 0
344  );
345  }
346 
347  protected function ‪getQueryBuilderForPages(): ‪QueryBuilder
348  {
349  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
350  ->getQueryBuilderForTable('pages');
352  $queryBuilder
355  ->‪add(GeneralUtility::makeInstance(DeletedRestriction::class))
356  ->‪add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->context->getPropertyFromAspect('workspace', 'id')));
357  return $queryBuilder;
358  }
359 
360  protected function ‪enabledHook(): void
361  {
362  ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects'] =
363  DataHandlerSlugUpdateHook::class;
364  }
365 
366  protected function ‪disableHook(): void
367  {
368  unset(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects']);
369  }
370 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:84
‪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:54
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\USER_BACKEND
‪const USER_BACKEND
Definition: RecordHistoryStore.php:38
‪TYPO3\CMS\Core\DataHandling\Model\CorrelationId\getSubject
‪string null getSubject()
Definition: CorrelationId.php:144
‪TYPO3\CMS\Redirects\Service\SlugService\getRecordHistoryStore
‪getRecordHistoryStore()
Definition: SlugService.php:322
‪TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook
Definition: DataHandlerSlugUpdateHook.php:29
‪TYPO3\CMS\Redirects\Service\SlugService\$autoUpdateSlugs
‪bool $autoUpdateSlugs
Definition: SlugService.php:90
‪TYPO3\CMS\Redirects\Service\SlugService\$redirectTTL
‪int $redirectTTL
Definition: SlugService.php:98
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface\removeAll
‪QueryRestrictionContainerInterface removeAll()
‪TYPO3\CMS\Redirects\Service\SlugService\createRedirect
‪createRedirect(string $originalSlug, int $pageId, int $languageId, int $pid)
Definition: SlugService.php:158
‪TYPO3\CMS\Redirects\Service\SlugService\$context
‪Context $context
Definition: SlugService.php:58
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Redirects\Service\SlugService\sendNotification
‪sendNotification()
Definition: SlugService.php:304
‪TYPO3\CMS\Redirects\Service\SlugService\updateSlug
‪string null updateSlug(array $subPageRecord, string $oldSlugOfParentPage, string $newSlugOfParentPage)
Definition: SlugService.php:267
‪TYPO3\CMS\Core\Database\Query\QueryBuilder\getRestrictions
‪QueryRestrictionContainerInterface getRestrictions()
Definition: QueryBuilder.php:104
‪TYPO3\CMS\Redirects\Service\SlugService\resolveSubPages
‪resolveSubPages(int $id, int $languageUid)
Definition: SlugService.php:214
‪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:113
‪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:199
‪TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory
Definition: RecordStateFactory.php:26
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:52
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Redirects\Service\SlugService\$languageService
‪LanguageService $languageService
Definition: SlugService.php:62
‪TYPO3\CMS\Core\Page\PageRenderer
Definition: PageRenderer.php:42
‪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:42
‪TYPO3\CMS\Redirects\Service\SlugService\$correlationIdSlugUpdate
‪CorrelationId string $correlationIdSlugUpdate
Definition: SlugService.php:86
‪TYPO3\CMS\Redirects\Service\SlugService\$linkService
‪LinkService $linkService
Definition: SlugService.php:78
‪TYPO3\CMS\Redirects\Service\SlugService\initializeSettings
‪initializeSettings(int $pageId)
Definition: SlugService.php:134
‪TYPO3\CMS\Redirects\Service\SlugService\$httpStatusCode
‪int $httpStatusCode
Definition: SlugService.php:102
‪TYPO3\CMS\Redirects\Service\SlugService\$siteFinder
‪SiteFinder $siteFinder
Definition: SlugService.php:70
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:75
‪TYPO3\CMS\Redirects\Service\SlugService\$site
‪SiteInterface $site
Definition: SlugService.php:66
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecord
‪static array null getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
Definition: BackendUtility.php:95
‪TYPO3\CMS\Redirects\Service\SlugService\$pageRepository
‪PageRepository $pageRepository
Definition: SlugService.php:74
‪TYPO3\CMS\Redirects\Service\SlugService\__construct
‪__construct(Context $context, LanguageService $languageService, SiteFinder $siteFinder, PageRepository $pageRepository, LinkService $linkService)
Definition: SlugService.php:104
‪TYPO3\CMS\Redirects\Service\SlugService\getQueryBuilderForPages
‪getQueryBuilderForPages()
Definition: SlugService.php:335
‪TYPO3\CMS\Redirects\Service
Definition: IntegrityService.php:18
‪TYPO3\CMS\Redirects\Service\SlugService\$autoCreateRedirects
‪bool $autoCreateRedirects
Definition: SlugService.php:94
‪TYPO3\CMS\Redirects\Service\SlugService\persistNewSlug
‪persistNewSlug(int $uid, string $newSlug)
Definition: SlugService.php:292
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Redirects\Service\SlugService\$correlationIdRedirectCreation
‪CorrelationId string $correlationIdRedirectCreation
Definition: SlugService.php:82
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:42
‪TYPO3\CMS\Redirects\Service\SlugService\createCorrelationIds
‪createCorrelationIds(int $pageId, CorrelationId $correlationId)
Definition: SlugService.php:147
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:52
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface\add
‪QueryRestrictionContainerInterface add(QueryRestrictionInterface $restriction)
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Redirects\Service\SlugService
Definition: SlugService.php:48
‪TYPO3\CMS\Core\Context\DateTimeAspect
Definition: DateTimeAspect.php:35
‪TYPO3\CMS\Redirects\Service\SlugService\disableHook
‪disableHook()
Definition: SlugService.php:354
‪TYPO3\CMS\Redirects\Service\SlugService\enabledHook
‪enabledHook()
Definition: SlugService.php:348
‪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:39