[FEATURE] Add Upgrade Wizard to migrate to pagepath segment for pages 91/57991/9
authorBenni Mack <benni@typo3.org>
Wed, 22 Aug 2018 16:21:21 +0000 (18:21 +0200)
committerSusanne Moog <susanne.moog@typo3.org>
Mon, 27 Aug 2018 12:30:17 +0000 (14:30 +0200)
A new upgrade wizard is introduced, which sets page URLs based on the
page title and rootline, if they are empty.

All root pages will get "/", all others will have a "/path-to/my/page/"
pagepath.

On top, if RealURL generated pagepaths already (works with v1 and v2 by
checking for the database table), they will be used instead of
generated slugs.

Pages with an alias field take priority over "regular" pages and
values from RealURL, whereas alias fields will result in a slug like
"/my-alias-value".

Resolves: #85928
Releases: master
Change-Id: Ie0cfbfb9e8472e15717ea39cb1af65896743f293
Reviewed-on: https://review.typo3.org/57991
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Feature-85928-UpgradeWizardToMigratePagesToSpeakingURLs.rst [new file with mode: 0644]
typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php [new file with mode: 0644]
typo3/sysext/install/ext_localconf.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-85928-UpgradeWizardToMigratePagesToSpeakingURLs.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-85928-UpgradeWizardToMigratePagesToSpeakingURLs.rst
new file mode 100644 (file)
index 0000000..f13b5fd
--- /dev/null
@@ -0,0 +1,42 @@
+.. include:: ../../Includes.txt
+
+==================================================================
+Feature: #85928 - Upgrade wizard to migrate pages to speaking URLs
+==================================================================
+
+See :issue:`85928`
+
+Description
+===========
+
+TYPO3 now supports "Speaking URLs" for pages, and in order to fully make use of this feature, an
+upgrade wizard builds up the URL segment (pagepath) for all pages that do not have a value
+set already.
+
+In order to ease the pain when upgrading from previous versions that supported RealURL,
+the upgrade wizard checks for additional tables "tx_realurl_pathcache" (realurl v1) and
+"tx_realurl_pathdata" (realurl v2+) if they exist in the database, to fill the page paths based
+on these values - however they will get sanitized to match the slug layout with a prefixed "/".
+
+Pages that contain value in their "alias" database field, this takes priority over "regular" pages
+and values from RealURL, whereas alias fields will result in a slug like "/my-alias-value".
+
+
+Impact
+======
+
+After running the upgrade wizard, it is possible to use all of the speaking URL functionality for
+all pages that support a site configuration.
+
+The upgrade wizard also runs through all pages that do not have a site configuration yet, in
+order to ensure consistent state throughout the database. It is encouraged to create a site
+configuration for a pagepath before running this upgrade wizard.
+
+Please take note that running the upgrade wizard does not migrate a previously configured RealURL
+project fully to the new structure. It only eases the migration, but the full migration depends
+on many more previous URL generation configurations used.
+
+Also: if `simulate_static`, `realurl` or `cooluri` or any other extension for URL rewriting was
+used, it is highly possible that pages are now available under different URLs than before.
+
+.. index:: Database
\ No newline at end of file
diff --git a/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php b/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php
new file mode 100644 (file)
index 0000000..a1b2be5
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Updates;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\DataHandling\SlugHelper;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Fills pages.slug with a proper value for pages that do not have a slug updater.
+ * Does not take "deleted" pages into account, but respects workspace records.
+ *
+ * This is how it works:
+ * - Check if a page has pages.alias filled.
+ * - Check if realurl v1 (tx_realurl_pathcache) or v2 (tx_realurl_pathdata) has a page path, use that instead.
+ * - If not -> generate the slug.
+ */
+class PopulatePageSlugs extends AbstractUpdate
+{
+    /**
+     * The human-readable title of the upgrade wizard
+     *
+     * @var string
+     */
+    protected $title = 'Introduce URL parts ("slugs") to all existing pages';
+
+    protected $table = 'pages';
+
+    protected $fieldName = 'slug';
+
+    /**
+     * Checks whether updates are required.
+     *
+     * @param string &$description The description for the update
+     * @return bool Whether an update is required (TRUE) or not (FALSE)
+     */
+    public function checkForUpdate(&$description): bool
+    {
+        $description = 'TYPO3 includes native URL handling. Every page record has its own speaking URL path ' .
+            'called "slug" which can be edited in TYPO3 Backend. However, it is necessary that all pages have
+            a URL pre-filled. This is done by evaluating the page title / navigation title and all of its rootline.';
+
+        $updateNeeded = false;
+
+        // Check if the database table even exists
+        if ($this->checkIfWizardIsRequired() && !$this->isWizardDone()) {
+            $updateNeeded = true;
+        }
+
+        return $updateNeeded;
+    }
+
+    /**
+     * Performs the accordant updates.
+     *
+     * @param array &$dbQueries Queries done in this update
+     * @param string &$customMessage Custom message
+     * @return bool Whether everything went smoothly or not
+     * @throws \InvalidArgumentException
+     */
+    public function performUpdate(array &$dbQueries, &$customMessage): bool
+    {
+        $results = $this->populateSlugs();
+        $customMessage .= implode('<br>', $results);
+        $this->markWizardAsDone();
+        return true;
+    }
+
+    /**
+     * Fills the database table "pages" with slugs based on the page title and its configuration.
+     * But also checks "legacy" functionality.
+     */
+    protected function populateSlugs(): array
+    {
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
+        $queryBuilder = $connection->createQueryBuilder();
+        $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        $statement = $queryBuilder
+            ->select('*')
+            ->from($this->table)
+            ->where(
+                $queryBuilder->expr()->orX(
+                    $queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
+                    $queryBuilder->expr()->isNull($this->fieldName)
+                )
+            )
+            // Ensure that fields with alias are managed first
+            ->orderBy('alias', 'desc')
+            ->execute();
+
+        // Check for existing slugs from realurl
+        $suggestedSlugs = [];
+        if ($this->checkIfTableExists('tx_realurl_pathdata')) {
+            $suggestedSlugs = $this->getSuggestedSlugs('tx_realurl_pathdata');
+        } elseif ($this->checkIfTableExists('tx_realurl_pathcache')) {
+            $suggestedSlugs = $this->getSuggestedSlugs('tx_realurl_pathcache');
+        }
+
+        $fieldConfig = $GLOBALS['TCA'][$this->table]['columns'][$this->fieldName]['config'];
+        $evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
+        $hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
+        $hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
+        $slugHelper = GeneralUtility::makeInstance(SlugHelper::class, $this->table, $this->fieldName, $fieldConfig);
+        $messages = [];
+        while ($record = $statement->fetch()) {
+            $recordId = (int)$record['uid'];
+            $pid = (int)$record['pid'];
+            $languageId = (int)$record['sys_language_uid'];
+            $pageIdInDefaultLanguage = $languageId > 0 ? (int)$record['l10n_parent'] : $recordId;
+            $slug = $suggestedSlugs[$pageIdInDefaultLanguage][$languageId] ?? '';
+
+            // see if an alias field was used, then let's build a slug out of that.
+            if (!empty($record['alias'])) {
+                $slug = $slugHelper->sanitize('/' . $record['alias']);
+            }
+
+            if (empty($slug)) {
+                if ($pid === -1) {
+                    $queryBuilder = $connection->createQueryBuilder();
+                    $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+                    $liveVersion = $queryBuilder
+                        ->select('pid')
+                        ->from('pages')
+                        ->where(
+                            $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($record['t3ver_oid'], \PDO::PARAM_INT))
+                        )->execute()->fetch();
+                    $pid = (int)$liveVersion['pid'];
+                }
+                $slug = $slugHelper->generate($record, $pid);
+            }
+
+            if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $recordId, $pid, $languageId)) {
+                $slug = $slugHelper->buildSlugForUniqueInSite($slug, $recordId, $pid, $languageId);
+            }
+            if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $recordId, $pid, $languageId)) {
+                $slug = $slugHelper->buildSlugForUniqueInPid($slug, $recordId, $pid, $languageId);
+            }
+
+            $connection->update(
+                $this->table,
+                [$this->fieldName => $slug],
+                ['uid' => $recordId]
+            );
+            $messages[] = 'Update record ' . $this->table . ':' . $recordId . ' with slug "' . htmlspecialchars($slug) . '"';
+        }
+        return $messages;
+    }
+
+    /**
+     * Check if there are record within "pages" database table with an empty "slug" field.
+     *
+     * @return bool
+     * @throws \InvalidArgumentException
+     */
+    protected function checkIfWizardIsRequired(): bool
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
+        $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
+        $numberOfEntries = $queryBuilder
+            ->count('uid')
+            ->from($this->table)
+            ->where(
+                $queryBuilder->expr()->orX(
+                    $queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
+                    $queryBuilder->expr()->isNull($this->fieldName)
+                )
+            )
+            ->execute()
+            ->fetchColumn();
+        return $numberOfEntries > 0;
+    }
+
+    /**
+     * Resolve prepared realurl "pagepath" for pages
+     *
+     * @param string $tableName
+     * @return array with pageID (default language) and language ID as two-dimensional array containing the page path
+     */
+    protected function getSuggestedSlugs(string $tableName): array
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
+        $statement = $queryBuilder
+            ->select('*')
+            ->from($tableName)
+            ->where(
+                $queryBuilder->expr()->eq('mpvar', $queryBuilder->createNamedParameter(''))
+            )
+            ->execute();
+        $suggestedSlugs = [];
+        while ($row = $statement->fetch()) {
+            $suggestedSlugs[(int)$row['page_id']][(int)$row['language_id']] = '/' . trim($row['pagepath'], '/') . '/';
+        }
+        return $suggestedSlugs;
+    }
+}
index f99670e..f9c0638 100644 (file)
@@ -64,6 +64,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['redirects']
     = \TYPO3\CMS\Install\Updates\RedirectsExtensionUpdate::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['adminpanelExtension']
     = \TYPO3\CMS\Install\Updates\AdminPanelInstall::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['pagesSlugs']
+    = \TYPO3\CMS\Install\Updates\PopulatePageSlugs::class;
 
 $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
 $icons = [