[BUGFIX] Ensure that slugs are always calculated in DataHandler 98/58098/14
authorOliver Hader <oliver@typo3.org>
Sun, 2 Sep 2018 21:19:52 +0000 (23:19 +0200)
committerBenni Mack <benni@typo3.org>
Mon, 3 Sep 2018 18:28:26 +0000 (20:28 +0200)
When NOT using FormEngine to create a new page (e.g. in the pagetree
or via the "Create multiple pages" wizard), so just using DataHandler
magic is uttermost critical that a slug is always created, to ensure
the "uniqueInSite" functionality, and to access a page, even though
the page was created after the upgrade wizard has run.

Resolves: #86050
Resolves: #85937
Releases: master
Change-Id: I9eff3385c369a04a6f5a33d0b840b6a2b698891c
Reviewed-on: https://review.typo3.org/58098
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/DataHandling/SlugEnricher.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/SlugHelper.php
typo3/sysext/core/Classes/Site/PseudoSiteFinder.php
typo3/sysext/frontend/Classes/Compatibility/LegacyDomainResolver.php

index b8810b3..c63e1f5 100644 (file)
@@ -966,6 +966,7 @@ class DataHandler implements LoggerAwareInterface
             $hookObjectsArr[] = $hookObject;
         }
         // Pre-process data-map and synchronize localization states
+        $this->datamap = GeneralUtility::makeInstance(SlugEnricher::class)->enrichDataMap($this->datamap);
         $this->datamap = DataMapProcessor::instance($this->datamap, $this->BE_USER)->process();
         // Organize tables so that the pages-table is always processed first. This is required if you want to make sure that content pointing to a new page will be created.
         $orderOfTables = [];
@@ -1931,6 +1932,7 @@ class DataHandler implements LoggerAwareInterface
      * @param string $field Field name
      * @param array $incomingFieldArray the fields being explicitly set by the outside (unlike $fieldArray) for the record
      * @return array $res The result array. The processed value (if any!) is set in the "value" key.
+     * @see SlugEnricher, SlugHelper
      */
     protected function checkValueForSlug(string $value, array $tcaFieldConf, string $table, $id, int $realPid, string $field, array $incomingFieldArray = []): array
     {
@@ -1938,7 +1940,7 @@ class DataHandler implements LoggerAwareInterface
         $helper = GeneralUtility::makeInstance(SlugHelper::class, $table, $field, $tcaFieldConf, $workspaceId);
         $fullRecord = array_replace_recursive($this->checkValue_currentRecord, $incomingFieldArray ?? []);
         // Generate a value if there is none, otherwise ensure that all characters are cleaned up
-        if (empty($value)) {
+        if ($value === '') {
             $value = $helper->generate($fullRecord, $realPid);
         } else {
             $value = $helper->sanitize($value);
diff --git a/typo3/sysext/core/Classes/DataHandling/SlugEnricher.php b/typo3/sysext/core/Classes/DataHandling/SlugEnricher.php
new file mode 100644 (file)
index 0000000..e713ebf
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+namespace TYPO3\CMS\Core\DataHandling;
+
+/*
+ * 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\Utility\MathUtility;
+
+/**
+ * New records that are capable of handling slugs (TCA type 'slug'), always
+ * require the field value to be set in order to run through the validation
+ * process to create a new slug. Fields having `null` as value are ignored
+ * and can be used to by-pass implicit slug initialization.
+ *
+ * @see DataHandler::fillInFieldArray(), DataHandler::checkValueForSlug()
+ */
+class SlugEnricher
+{
+    /**
+     * @var array
+     */
+    protected $slugFieldNamesPerTable = [];
+
+    /**
+     * @param array $dataMap
+     * @return array
+     */
+    public function enrichDataMap(array $dataMap): array
+    {
+        foreach ($dataMap as $tableName => &$tableDataMap) {
+            $slugFieldNames = $this->resolveSlugFieldNames($tableName);
+            if (empty($slugFieldNames)) {
+                continue;
+            }
+            foreach ($tableDataMap as $identifier => &$fieldValues) {
+                if (MathUtility::canBeInterpretedAsInteger($identifier)) {
+                    continue;
+                }
+                $fieldValues = $this->enrichUndefinedSlugFieldNames(
+                    $slugFieldNames,
+                    $fieldValues
+                );
+            }
+        }
+        return $dataMap;
+    }
+
+    /**
+     * @param array $slugFieldNames
+     * @param array $fieldValues
+     * @return array
+     */
+    protected function enrichUndefinedSlugFieldNames(array $slugFieldNames, array $fieldValues): array
+    {
+        if (empty($slugFieldNames)) {
+            return [];
+        }
+        $undefinedSlugFieldNames = array_diff(
+            $slugFieldNames,
+            array_keys($fieldValues)
+        );
+        if (empty($undefinedSlugFieldNames)) {
+            return $fieldValues;
+        }
+        return array_merge(
+            $fieldValues,
+            array_fill_keys(
+                $undefinedSlugFieldNames,
+                ''
+            )
+        );
+    }
+
+    /**
+     * @param string $tableName
+     * @return string[]
+     */
+    protected function resolveSlugFieldNames(string $tableName): array
+    {
+        if (isset($this->slugFieldNamesPerTable[$tableName])) {
+            return $this->slugFieldNamesPerTable[$tableName];
+        }
+
+        return $this->slugFieldNamesPerTable[$tableName] = array_keys(
+            array_filter(
+                $GLOBALS['TCA'][$tableName]['columns'] ?? [],
+                function (array $settings) {
+                    return ($settings['config']['type'] ?? null) === 'slug';
+                }
+            )
+        );
+    }
+}
index 81b8591..dfe6cc3 100644 (file)
@@ -340,6 +340,10 @@ class SlugHelper
             $queryBuilder->expr()->in(
                 't3ver_wsid',
                 $queryBuilder->createNamedParameter($workspaceIds, Connection::PARAM_INT_ARRAY)
+            ),
+            $queryBuilder->expr()->in(
+                't3ver_state',
+                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
             )
         );
     }
index fc2f11e..a1f1f27 100644 (file)
@@ -92,7 +92,7 @@ class PseudoSiteFinder
                 'domains' => $domainRecords,
                 'languages' => $allLanguages
             ]);
-            $this->pseudoSites[$rootPageId] = $site;
+            $this->pseudoSites[(int)$rootPageId] = $site;
         }
 
         // Now lets an empty Pseudo-Site for visiting things on pid=0
@@ -135,7 +135,7 @@ class PseudoSiteFinder
             ->orderBy('sorting')
             ->execute();
         while ($row = $statement->fetch()) {
-            $uid = $row['uid'];
+            $uid = (int)$row['uid'];
             $languageRecords[$uid] = [
                 'languageId' => $uid,
                 'title' => $row['title'],
index fdcad8f..40f25f2 100644 (file)
@@ -153,7 +153,7 @@ class LegacyDomainResolver implements SingletonInterface
         // walk the rootline downwards from the target page
         // to the root page, until a domain record is found
         foreach ($rootLine as $pageInRootline) {
-            $pidInRootline = $pageInRootline['uid'];
+            $pidInRootline = (int)$pageInRootline['uid'];
             if (empty($this->groupedDomainsPerPage[$pidInRootline])) {
                 continue;
             }