[FEATURE] Make prepending slash in TCA slug configurable 74/58474/3
authorBenni Mack <benni@typo3.org>
Sat, 29 Sep 2018 19:05:45 +0000 (21:05 +0200)
committerBenni Mack <benni@typo3.org>
Sun, 30 Sep 2018 08:12:37 +0000 (10:12 +0200)
A new TCA option for TCA type "slug" is added, called "prependSlash",
which adds a "/" in front of the field. For pages (pages.slug),
this is mandatory and cannot be configured, as the slug field
has to be filled and set to "/" as a base for the root page.

For other database fields, this is optional (and disabled by default),
and can be enabled via "prependSlash" in TCA config.

This option is mostly useful for recursive records, like categories,
but for most "flat" structures like "news" or "events", this is not
suitable. For pages, it is hard-coded and cannot be (un-)set.

Resolves: #86457
Releases: master
Change-Id: I997908ed74af7ca21873b0793674e9185cc581ce
Reviewed-on: https://review.typo3.org/58474
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Jörg Bösche <typo3@joergboesche.de>
Reviewed-by: Jigal van Hemert <jigal.van.hemert@typo3.org>
Tested-by: Jigal van Hemert <jigal.van.hemert@typo3.org>
typo3/sysext/core/Classes/DataHandling/SlugHelper.php
typo3/sysext/core/Documentation/Changelog/master/Feature-86457-TCATypeSlugAddsAPrependingSlash.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/DataHandling/SlugHelperTest.php

index 0c401b2..6b313a7 100644 (file)
@@ -57,6 +57,15 @@ class SlugHelper
     protected $workspaceEnabled;
 
     /**
+     * Defines whether the slug field should start with "/".
+     * For pages (due to rootline functionality), this is a must have, otherwise the root level page
+     * would have an empty value.
+     *
+     * @var bool
+     */
+    protected $prependSlashInSlug;
+
+    /**
      * Slug constructor.
      *
      * @param string $tableName TCA table
@@ -71,6 +80,12 @@ class SlugHelper
         $this->configuration = $configuration;
         $this->workspaceId = $workspaceId;
 
+        if ($this->tableName === 'pages' && $this->fieldName === 'slug') {
+            $this->prependSlashInSlug = true;
+        } else {
+            $this->prependSlashInSlug = $this->configuration['prependSlash'] ?? false;
+        }
+
         $this->workspaceEnabled = BackendUtility::isTableWorkspaceEnabled($tableName);
     }
 
@@ -91,6 +106,7 @@ class SlugHelper
         $slug = preg_replace('/[ \t\x{00A0}\-+_]+/u', $fallbackCharacter, $slug);
 
         // Convert extended letters to ascii equivalents
+        // The specCharsToASCII() converts "€" to "EUR"
         $slug = GeneralUtility::makeInstance(CharsetConverter::class)->specCharsToASCII('utf-8', $slug);
 
         // Get rid of all invalid characters, but allow slashes
@@ -101,8 +117,7 @@ class SlugHelper
             $slug = preg_replace('/' . preg_quote($fallbackCharacter) . '{2,}/', $fallbackCharacter, $slug);
         }
 
-        // Ensure slug is lower cased after all replacement was done:
-        // The specCharsToASCII() above for example converts "€" to "EUR"
+        // Ensure slug is lower cased after all replacement was done
         $slug = mb_strtolower($slug, 'utf-8');
         // keep slashes: re-convert them after rawurlencode did everything
         $slug = rawurlencode($slug);
@@ -112,7 +127,10 @@ class SlugHelper
         $extractedSlug = $this->extract($slug);
         // Remove trailing and beginning slashes, except if the trailing slash was added, then we'll re-add it
         $appendTrailingSlash = $extractedSlug !== '' && substr($slug, -1) === '/';
-        $slug = '/' . $extractedSlug . ($appendTrailingSlash ? '/' : '');
+        $slug = $extractedSlug . ($appendTrailingSlash ? '/' : '');
+        if ($this->prependSlashInSlug) {
+            $slug = '/' . $slug;
+        }
         return $slug;
     }
 
@@ -177,8 +195,11 @@ class SlugHelper
         $slug = implode($fieldSeparator, $slugParts);
         $slug = $this->sanitize($slug);
         // No valid data found
-        if ($slug === '/') {
-            $slug .= 'default-' . GeneralUtility::shortMD5(json_encode($recordData));
+        if ($slug === '' || $slug === '/') {
+            $slug = 'default-' . GeneralUtility::shortMD5(json_encode($recordData));
+        }
+        if ($this->prependSlashInSlug) {
+            $slug = '/' . $slug;
         }
         if (!empty($prefix)) {
             $slug = $prefix . $slug;
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-86457-TCATypeSlugAddsAPrependingSlash.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-86457-TCATypeSlugAddsAPrependingSlash.rst
new file mode 100644 (file)
index 0000000..219d6ee
--- /dev/null
@@ -0,0 +1,43 @@
+.. include:: ../../Includes.txt
+
+=======================================================
+Feature: #86457 - TCA Type Slug adds a prepending slash
+=======================================================
+
+See :issue:`86457`
+
+Description
+===========
+
+The new TCA type slug field now hard-codes a slash as a prefix for all pages, as this is
+mandatory for URL resolving and ensuring a uniqueness within a site.
+
+However, for slug types within regular records, it is not necessary to do so, therefore, the slash
+is never prepended on a "regular" slug field.
+
+If - in some special cases - the "slug" field should contain a slash (due to e.g. nested categories
+with speaking segments), a new option `prependSlash` is added to TCA type slug.
+
+
+Impact
+======
+
+Third-party extensions using the slug field now receive a slug value without a slash, and
+can use this as a regular - sanitized - slug field. It is however, recommended to use the
+`uniqueInPid` eval option to ensure uniqueness.
+
+If a nested record structure is given, it is recommended to use the new option `prependSlash`
+by setting it to :php:`true`.
+
+:php:
+       'type' => 'slug',
+       'config' => [
+               'generatorOptions' => [
+                       'fields' => ['title'],
+               ]
+               'fallbackCharacter' => '-',
+               'prefixSlash' => '/',
+               'eval' => 'uniqueInPid'
+       ]
+
+.. index:: TCA
\ No newline at end of file
index cba006a..b2c6635 100644 (file)
@@ -34,6 +34,166 @@ class SlugHelperTest extends UnitTestCase
             'empty string' => [
                 [],
                 '',
+                '',
+            ],
+            'existing base' => [
+                [],
+                '/',
+                '',
+            ],
+            'invalid base' => [
+                [],
+                '//',
+                '',
+            ],
+            'invalid slug' => [
+                [],
+                '/slug//',
+                'slug/',
+            ],
+            'lowercase characters' => [
+                [],
+                '1AZÄ',
+                '1azae',
+            ],
+            'strig tags' => [
+                [],
+                '<foo>bar</foo>',
+                'bar'
+            ],
+            'replace special chars to -' => [
+                [],
+                '1 2-3+4_5',
+                '1-2-3-4-5',
+            ],
+            'empty fallback character' => [
+                [
+                    'fallbackCharacter' => '',
+                ],
+                '1_2',
+                '12',
+            ],
+            'different fallback character' => [
+                [
+                    'fallbackCharacter' => '_',
+                ],
+                '1-2',
+                '1_2',
+            ],
+            'convert umlauts' => [
+                [],
+                'ä ß Ö',
+                'ae-ss-oe'
+            ],
+            'keep slashes' => [
+                [],
+                '1/2',
+                '1/2',
+            ],
+            'keep pending slash' => [
+                [],
+                '/1/2',
+                '1/2',
+            ],
+            'do not remove trailing slash' => [
+                [],
+                '1/2/',
+                '1/2/',
+            ],
+            'keep pending slash and remove fallback' => [
+                [],
+                '/-1/2',
+                '1/2',
+            ],
+            'do not remove trailing slash, but remove fallback' => [
+                [],
+                '1/2-/',
+                '1/2/',
+            ],
+            'reduce multiple fallback chars to one' => [
+                [],
+                '1---2',
+                '1-2',
+            ],
+            'various special chars' => [
+                [],
+                'special-chars-«-∑-€-®-†-Ω-¨-ø-π-å-‚-∂-ƒ-©-ª-º-∆-@-¥-≈-ç-√-∫-~-µ-∞-…-–',
+                'special-chars-eur-r-o-oe-p-aa-f-c-a-o-yen-c-u'
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider sanitizeDataProvider
+     * @param array $configuration
+     * @param string $input
+     * @param string $expected
+     */
+    public function sanitizeConvertsString(array $configuration, string $input, string $expected)
+    {
+        $subject = new SlugHelper(
+            'dummyTable',
+            'dummyField',
+            $configuration
+        );
+        static::assertEquals(
+            $expected,
+            $subject->sanitize($input)
+        );
+    }
+
+    public function generateNeverDeliversEmptySlugDataProvider()
+    {
+        return [
+            'simple title' => [
+                'Products',
+                'products'
+            ],
+            'title with spaces' => [
+                'Product Cow',
+                'product-cow'
+            ],
+            'title with invalid characters' => [
+                'Products - Cows',
+                'products-cows'
+            ],
+            'title with only invalid characters' => [
+                '!!!',
+                'default-51cf35392c'
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider generateNeverDeliversEmptySlugDataProvider
+     * @param string $input
+     * @param string $expected
+     * @test
+     */
+    public function generateNeverDeliversEmptySlug(string $input, string $expected)
+    {
+        $GLOBALS['dummyTable']['ctrl'] = [];
+        $subject = new SlugHelper(
+            'dummyTable',
+            'dummyField',
+            ['generatorOptions' => ['fields' => ['title']]]
+        );
+        static::assertEquals(
+            $expected,
+            $subject->generate(['title' => $input, 'uid' => 13], 13)
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function sanitizeForPagesDataProvider(): array
+    {
+        return [
+            'empty string' => [
+                [],
+                '',
                 '/',
             ],
             'existing base' => [
@@ -120,28 +280,21 @@ class SlugHelperTest extends UnitTestCase
                 'special-chars-«-∑-€-®-†-Ω-¨-ø-π-å-‚-∂-ƒ-©-ª-º-∆-@-¥-≈-ç-√-∫-~-µ-∞-…-–',
                 '/special-chars-eur-r-o-oe-p-aa-f-c-a-o-yen-c-u'
             ],
-            'various special chars, allow unicode' => [
-                [
-                    'allowUnicodeCharacters' => true,
-                ],
-                'special-chars-«-∑-€-®-†-Ω-¨-ø-π-å-‚-∂-ƒ-©-ª-º-∆-@-¥-≈-ç-√-∫-~-µ-∞-…-–',
-                '/special-chars-eur-r-o-oe-p-aa-f-c-a-o-yen-c-u'
-            ]
         ];
     }
 
     /**
      * @test
-     * @dataProvider sanitizeDataProvider
+     * @dataProvider sanitizeForPagesDataProvider
      * @param array $configuration
      * @param string $input
      * @param string $expected
      */
-    public function sanitizeConvertsString(array $configuration, string $input, string $expected)
+    public function sanitizeConvertsStringForPages(array $configuration, string $input, string $expected)
     {
         $subject = new SlugHelper(
-            'dummyTable',
-            'dummyField',
+            'pages',
+            'slug',
             $configuration
         );
         static::assertEquals(
@@ -150,7 +303,7 @@ class SlugHelperTest extends UnitTestCase
         );
     }
 
-    public function generateNeverDeliversEmptySlugDataProvider()
+    public function generateNeverDeliversEmptySlugForPagesDataProvider()
     {
         return [
             'simple title' => [
@@ -173,17 +326,17 @@ class SlugHelperTest extends UnitTestCase
     }
 
     /**
-     * @dataProvider generateNeverDeliversEmptySlugDataProvider
+     * @dataProvider generateNeverDeliversEmptySlugForPagesDataProvider
      * @param string $input
      * @param string $expected
      * @test
      */
-    public function generateNeverDeliversEmptySlug(string $input, string $expected)
+    public function generateNeverDeliversEmptySlugForPages(string $input, string $expected)
     {
         $GLOBALS['dummyTable']['ctrl'] = [];
         $subject = new SlugHelper(
-            'dummyTable',
-            'dummyField',
+            'pages',
+            'slug',
             ['generatorOptions' => ['fields' => ['title']]]
         );
         static::assertEquals(