‪TYPO3CMS  ‪main
SlugHelperTest.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 PHPUnit\Framework\Attributes\DataProvider;
21 use PHPUnit\Framework\Attributes\Test;
23 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
24 
25 final class ‪SlugHelperTest extends UnitTestCase
26 {
27  protected bool ‪$resetSingletonInstances = true;
28 
29  public static function ‪sanitizeDataProvider(): array
30  {
31  return [
32  'empty string' => [
33  [],
34  '',
35  '',
36  ],
37  'existing base' => [
38  [],
39  '/',
40  '',
41  ],
42  'invalid base' => [
43  [],
44  '//',
45  '',
46  ],
47  'invalid slug' => [
48  [],
49  '/slug//',
50  'slug/',
51  ],
52  'lowercase characters' => [
53  [],
54  '1AZÄ',
55  '1azae',
56  ],
57  'strig tags' => [
58  [],
59  '<foo>bar</foo>',
60  'bar',
61  ],
62  'replace special chars to -' => [
63  [],
64  '1 2-3+4_5',
65  '1-2-3-4-5',
66  ],
67  'empty fallback character' => [
68  [
69  'fallbackCharacter' => '',
70  ],
71  '1_2',
72  '12',
73  ],
74  'different fallback character' => [
75  [
76  'fallbackCharacter' => '_',
77  ],
78  '1-2',
79  '1_2',
80  ],
81  'convert umlauts' => [
82  [],
83  'ä ß Ö',
84  'ae-ss-oe',
85  ],
86  'keep slashes' => [
87  [],
88  '1/2',
89  '1/2',
90  ],
91  'keep pending slash' => [
92  [],
93  '/1/2',
94  '1/2',
95  ],
96  'do not remove trailing slash' => [
97  [],
98  '1/2/',
99  '1/2/',
100  ],
101  'keep pending slash and remove fallback' => [
102  [],
103  '/-1/2',
104  '1/2',
105  ],
106  'do not remove trailing slash, but remove fallback' => [
107  [],
108  '1/2-/',
109  '1/2/',
110  ],
111  'reduce multiple fallback chars to one' => [
112  [],
113  '1---2',
114  '1-2',
115  ],
116  'various special chars' => [
117  [],
118  'special-chars-«-∑-€-®-†-Ω-¨-ø-π-å-‚-∂-ƒ-©-ª-º-∆-@-¥-≈-ç-√-∫-~-µ-∞-…-–',
119  'special-chars-eur-r-o-oe-p-aa-f-c-a-o-yen-c-u',
120  ],
121  'ensure colon and other http related parts are disallowed' => [
122  [],
123  'https://example.com:80/my/page/slug/',
124  'https//examplecom80/my/page/slug/',
125  ],
126  'non-ASCII characters are kept' => [
127  [],
128  'bla-arg应---用-ascii',
129  'bla-arg应-用-ascii',
130  ],
131  'non-normalized characters' => [
132  [],
133  hex2bin('667275cc88686e65757a6569746c696368656e'),
134  'fruehneuzeitlichen',
135  ],
136  ];
137  }
138 
139  #[DataProvider('sanitizeDataProvider')]
140  #[Test]
141  public function ‪sanitizeConvertsString(array $configuration, string $input, string $expected): void
142  {
143  $subject = new ‪SlugHelper(
144  'dummyTable',
145  'dummyField',
146  $configuration
147  );
148  self::assertEquals(
149  $expected,
150  $subject->sanitize($input)
151  );
152  }
153 
154  public static function ‪generateNeverDeliversEmptySlugDataProvider(): array
155  {
156  return [
157  'simple title' => [
158  'Products',
159  'products',
160  ],
161  'title with spaces' => [
162  'Product Cow',
163  'product-cow',
164  ],
165  'title with invalid characters' => [
166  'Products - Cows',
167  'products-cows',
168  ],
169  'title with only invalid characters' => [
170  '!!!',
171  'default-51cf35392ca400f2fce656a936831917',
172  ],
173  ];
174  }
175 
176  #[DataProvider('generateNeverDeliversEmptySlugDataProvider')]
177  #[Test]
178  public function ‪generateNeverDeliversEmptySlug(string $input, string $expected): void
179  {
180  ‪$GLOBALS['dummyTable']['ctrl'] = [];
181  $subject = new ‪SlugHelper(
182  'dummyTable',
183  'dummyField',
184  ['generatorOptions' => ['fields' => ['title']]]
185  );
186  self::assertEquals(
187  $expected,
188  $subject->generate(['title' => $input, 'uid' => 13], 13)
189  );
190  }
191 
192  public static function ‪sanitizeForPagesDataProvider(): array
193  {
194  return [
195  'empty string' => [
196  [],
197  '',
198  '/',
199  ],
200  'existing base' => [
201  [],
202  '/',
203  '/',
204  ],
205  'invalid base' => [
206  [],
207  '//',
208  '/',
209  ],
210  'invalid slug' => [
211  [],
212  '/slug//',
213  '/slug/',
214  ],
215  'lowercase characters' => [
216  [],
217  '1AZÄ',
218  '/1azae',
219  ],
220  'strig tags' => [
221  [],
222  '<foo>bar</foo>',
223  '/bar',
224  ],
225  'replace special chars to -' => [
226  [],
227  '1 2-3+4_5',
228  '/1-2-3-4-5',
229  ],
230  'empty fallback character' => [
231  [
232  'fallbackCharacter' => '',
233  ],
234  '1_2',
235  '/12',
236  ],
237  'different fallback character' => [
238  [
239  'fallbackCharacter' => '_',
240  ],
241  '1-2',
242  '/1_2',
243  ],
244  'convert umlauts' => [
245  [],
246  'ä ß Ö',
247  '/ae-ss-oe',
248  ],
249  'keep slashes' => [
250  [],
251  '1/2',
252  '/1/2',
253  ],
254  'keep pending slash' => [
255  [],
256  '/1/2',
257  '/1/2',
258  ],
259  'do not remove trailing slash' => [
260  [],
261  '1/2/',
262  '/1/2/',
263  ],
264  'keep pending slash and remove fallback' => [
265  [],
266  '/-1/2',
267  '/1/2',
268  ],
269  'do not remove trailing slash, but remove fallback' => [
270  [],
271  '1/2-/',
272  '/1/2/',
273  ],
274  'reduce multiple fallback chars to one' => [
275  [],
276  '1---2',
277  '/1-2',
278  ],
279  'various special chars' => [
280  [],
281  'special-chars-«-∑-€-®-†-Ω-¨-ø-π-å-‚-∂-ƒ-©-ª-º-∆-@-¥-≈-ç-√-∫-~-µ-∞-…-–',
282  '/special-chars-eur-r-o-oe-p-aa-f-c-a-o-yen-c-u',
283  ],
284  'ensure colon and other http related parts are disallowed' => [
285  [],
286  'https://example.com:80/my/page/slug/',
287  '/https//examplecom80/my/page/slug/',
288  ],
289  'chinese' => [
290  [],
291  '应用',
292  '/应用',
293  ],
294  'hindi' => [
295  [],
296  'कंपनी',
297  '/कंपनी',
298  ],
299  'hindi with plain accent character' => [
300  [],
301  'कंपनी^',
302  '/कंपनी',
303  ],
304  'hindi with combined accent character' => [
305  [],
306  'कंपनीâ',
307  '/कंपनीa',
308  ],
309  'japanese numbers (sino-japanese)' => [
310  [],
311  'さん',
312  '/さん',
313  ],
314  'japanese numbers (kanji)' => [
315  [],
316  '三つ',
317  '/三つ',
318  ],
319  'persian numbers' => [
320  [],
321  '۴',
322  '/4',
323  ],
324  ];
325  }
326 
327  #[DataProvider('sanitizeForPagesDataProvider')]
328  #[Test]
329  public function ‪sanitizeConvertsStringForPages(array $configuration, string $input, string $expected): void
330  {
331  $subject = new ‪SlugHelper(
332  'pages',
333  'slug',
334  $configuration
335  );
336  self::assertEquals(
337  $expected,
338  $subject->sanitize($input)
339  );
340  }
341 
343  {
344  return [
345  'simple title' => [
346  'Products',
347  '/products',
348  ],
349  'title with spaces' => [
350  'Product Cow',
351  '/product-cow',
352  ],
353  'title with invalid characters' => [
354  'Products - Cows',
355  '/products-cows',
356  ],
357  'title with only invalid characters' => [
358  '!!!',
359  '/default-51cf35392ca400f2fce656a936831917',
360  ],
361  ];
362  }
363 
364  #[DataProvider('generateNeverDeliversEmptySlugForPagesDataProvider')]
365  #[Test]
366  public function ‪generateNeverDeliversEmptySlugForPages(string $input, string $expected): void
367  {
368  ‪$GLOBALS['dummyTable']['ctrl'] = [];
369  $subject = new ‪SlugHelper(
370  'pages',
371  'slug',
372  ['generatorOptions' => ['fields' => ['title']]]
373  );
374  self::assertEquals(
375  $expected,
376  $subject->generate(['title' => $input, 'uid' => 13], 13)
377  );
378  }
379 
380  public static function ‪generatePrependsSlugsForPagesDataProvider(): array
381  {
382  return [
383  'simple title' => [
384  'Products',
385  '/parent-page/products',
386  [
387  'generatorOptions' => [
388  'fields' => ['title'],
389  'prefixParentPageSlug' => true,
390  ],
391  ],
392  ],
393  'title with spaces' => [
394  'Product Cow',
395  '/parent-page/product-cow',
396  [
397  'generatorOptions' => [
398  'fields' => ['title'],
399  'prefixParentPageSlug' => true,
400  ],
401  ],
402  ],
403  'title with slash' => [
404  'Product/Cow',
405  '/parent-page/product/cow',
406  [
407  'generatorOptions' => [
408  'fields' => ['title'],
409  'prefixParentPageSlug' => true,
410  ],
411  ],
412  ],
413  'title with slash and replace' => [
414  'Product/Cow',
415  '/parent-page/productcow',
416  [
417  'generatorOptions' => [
418  'fields' => ['title'],
419  'prefixParentPageSlug' => true,
420  'replacements' => [
421  '/' => '',
422  ],
423  ],
424  ],
425  ],
426  'title with slash and replace #2' => [
427  'Some Job in city1/city2 (m/w)',
428  '/parent-page/some-job-in-city1-city2',
429  [
430  'generatorOptions' => [
431  'fields' => ['title'],
432  'prefixParentPageSlug' => true,
433  'replacements' => [
434  '(m/w)' => '',
435  '/' => '-',
436  ],
437  ],
438  ],
439  ],
440  'title with invalid characters' => [
441  'Products - Cows',
442  '/parent-page/products-cows',
443  [
444  'generatorOptions' => [
445  'fields' => ['title'],
446  'prefixParentPageSlug' => true,
447  ],
448  ],
449  ],
450  'title with only invalid characters' => [
451  '!!!',
452  '/parent-page/default-51cf35392ca400f2fce656a936831917',
453  [
454  'generatorOptions' => [
455  'fields' => ['title'],
456  'prefixParentPageSlug' => true,
457  ],
458  ],
459  ],
460  ];
461  }
462 
463  #[DataProvider('generatePrependsSlugsForPagesDataProvider')]
464  #[Test]
465  public function ‪generatePrependsSlugsForPages(string $input, string $expected, array $options): void
466  {
467  ‪$GLOBALS['dummyTable']['ctrl'] = [];
468  $parentPage = [
469  'uid' => '13',
470  'pid' => '10',
471  'title' => 'Parent Page',
472  ];
473  $subject = $this->getAccessibleMock(
474  SlugHelper::class,
475  ['resolveParentPageRecord'],
476  [
477  'pages',
478  'slug',
479  $options,
480  ]
481  );
482  $series = [
483  [13, $parentPage],
484  [10, null],
485  ];
486  $subject->expects(self::exactly(2))
487  ->method('resolveParentPageRecord')
488  ->willReturnCallback(function (int $pid) use (&$series): ?array {
489  $arguments = array_shift($series);
490  self::assertSame($arguments[0], $pid);
491  return $arguments[1];
492  });
493  self::assertEquals(
494  $expected,
495  $subject->generate(['title' => $input, 'uid' => 13], 13)
496  );
497  }
498 
500  {
501  return [
502  'title and empty nav_title' => [
503  ['title' => 'Products', 'nav_title' => '', 'subtitle' => ''],
504  '/products',
505  [
506  'generatorOptions' => [
507  'fields' => [
508  ['nav_title', 'title'],
509  ],
510  ],
511  ],
512  ],
513  'title and nav_title' => [
514  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => ''],
515  '/best-products',
516  [
517  'generatorOptions' => [
518  'fields' => [
519  ['nav_title', 'title'],
520  ],
521  ],
522  ],
523  ],
524  'title and nav_title and subtitle' => [
525  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => 'Product subtitle'],
526  '/product-subtitle',
527  [
528  'generatorOptions' => [
529  'fields' => [
530  ['subtitle', 'nav_title', 'title'],
531  ],
532  ],
533  ],
534  ],
535  'definition with a non existing field (misconfiguration)' => [
536  ['title' => 'Products', 'nav_title' => '', 'subtitle' => ''],
537  '/products',
538  [
539  'generatorOptions' => [
540  'fields' => [
541  ['custom_field', 'title'],
542  ],
543  ],
544  ],
545  ],
546  'empty fields deliver default slug' => [
547  ['title' => '', 'nav_title' => '', 'subtitle' => ''],
548  '/default-b4dac929c2d313b7ff79fc5edeedd207',
549  [
550  'generatorOptions' => [
551  'fields' => [
552  ['nav_title', 'title'],
553  ],
554  ],
555  ],
556  ],
557  'fallback combined with a second field' => [
558  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => 'Product subtitle'],
559  '/best-products/product-subtitle',
560  [
561  'generatorOptions' => [
562  'fields' => [
563  ['nav_title', 'title'], 'subtitle',
564  ],
565  ],
566  ],
567  ],
568  'empty config array deliver default slug' => [
569  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => 'Product subtitle'],
570  '/default-e13d142b36dcca110f2c3b57ee7a2dd3',
571  [
572  'generatorOptions' => [
573  'fields' => [
574  [],
575  ],
576  ],
577  ],
578  ],
579  'empty config deliver default slug' => [
580  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => 'Product subtitle'],
581  '/default-e13d142b36dcca110f2c3b57ee7a2dd3',
582  [
583  'generatorOptions' => [
584  'fields' => [],
585  ],
586  ],
587  ],
588  'combine two fallbacks' => [
589  ['title' => 'Products', 'nav_title' => 'Best products', 'subtitle' => 'Product subtitle', 'seo_title' => 'SEO product title'],
590  '/seo-product-title/best-products',
591  [
592  'generatorOptions' => [
593  'fields' => [
594  ['seo_title', 'title'], ['nav_title', 'subtitle'],
595  ],
596  ],
597  ],
598  ],
599  ];
600  }
601 
602  #[DataProvider('generateSlugWithNavTitleAndFallbackForPagesDataProvider')]
603  #[Test]
604  public function ‪generateSlugWithNavTitleAndFallbackForPages(array $input, string $expected, array $options): void
605  {
606  ‪$GLOBALS['dummyTable']['ctrl'] = [];
607  $subject = new ‪SlugHelper(
608  'pages',
609  'slug',
610  ['generatorOptions' => $options['generatorOptions']]
611  );
612  self::assertEquals(
613  $expected,
614  $subject->generate([
615  'title' => $input['title'],
616  'nav_title' => $input['nav_title'],
617  'subtitle' => $input['subtitle'],
618  'seo_title' => $input['seo_title'] ?? '',
619  'uid' => 13,
620  ], 13)
621  );
622  }
623 
624  #[Test]
625  public function ‪generateSlugWithHookModifiers(): void
626  {
627  $options = [];
628  $options['fallbackCharacter'] = '-';
629  $options['generatorOptions'] = [
630  'fields' => ['title'],
631  'postModifiers' => [
632  0 => static function ($parameters, $subject) {
633  $slug = $parameters['slug'];
634  if ($parameters['pid'] == 13) {
635  $slug = 'prepend' . $slug;
636  }
637  return $slug;
638  },
639  ],
640  ];
641  $subject = new ‪SlugHelper(
642  'pages',
643  'slug',
644  $options
645  );
646  $expected = '/prepend/products';
647  self::assertEquals(
648  $expected,
649  $subject->generate([
650  'title' => 'Products',
651  'nav_title' => 'Best products',
652  'subtitle' => 'Product subtitle',
653  'seo_title' => 'SEO product title',
654  'uid' => 23,
655  ], 13)
656  );
657  }
658 
659  public static function ‪generateSlugWithPid0DataProvider(): array
660  {
661  return [
662  'pages' => [
663  ['table' => 'pages', 'title' => 'Products'],
664  '/',
665  ],
666  'dummyTable' => [
667  ['table' => 'dummyTable', 'title' => 'Products'],
668  'products',
669  ],
670  ];
671  }
672 
673  #[DataProvider('generateSlugWithPid0DataProvider')]
674  #[Test]
675  public function ‪generateSlugWithPid0(array $input, string $expected)
676  {
677  if (empty(‪$GLOBALS[$input['table']]['ctrl'])) {
678  ‪$GLOBALS[$input['table']]['ctrl'] = [];
679  }
680  $subject = new ‪SlugHelper(
681  $input['table'],
682  'title',
683  ['generatorOptions' => ['fields' => ['title']]]
684  );
685  self::assertEquals(
686  $expected,
687  $subject->generate(['title' => $input['title'], 'uid' => 13], 0)
688  );
689  }
690 
691  public static function ‪generatePrependsSlugsForNonPagesDataProvider(): array
692  {
693  return [
694  'simple title' => [
695  'Product Name',
696  'product-name',
697  [
698  'generatorOptions' => [
699  'fields' => ['title'],
700  'prefixParentPageSlug' => true,
701  ],
702  ],
703  ],
704  ];
705  }
706 
707  #[DataProvider('generatePrependsSlugsForNonPagesDataProvider')]
708  #[Test]
709  public function ‪generatePrependsSlugsForNonPages(string $input, string $expected, array $options): void
710  {
711  ‪$GLOBALS['dummyTable']['ctrl'] = [];
712  $parentPage = [
713  'uid' => '0',
714  'pid' => null,
715  ];
716  $subject = $this->getAccessibleMock(
717  SlugHelper::class,
718  ['resolveParentPageRecord'],
719  [
720  'another_table',
721  'slug',
722  $options,
723  ]
724  );
725  $subject->expects(self::any())
726  ->method('resolveParentPageRecord')
727  ->withAnyParameters()
728  ->willReturn($parentPage);
729  self::assertEquals(
730  $expected,
731  $subject->generate(['title' => $input, 'uid' => 13], 13)
732  );
733  }
734 }
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateNeverDeliversEmptySlug
‪generateNeverDeliversEmptySlug(string $input, string $expected)
Definition: SlugHelperTest.php:178
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateSlugWithNavTitleAndFallbackForPagesDataProvider
‪static generateSlugWithNavTitleAndFallbackForPagesDataProvider()
Definition: SlugHelperTest.php:499
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateSlugWithPid0DataProvider
‪static generateSlugWithPid0DataProvider()
Definition: SlugHelperTest.php:659
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\sanitizeForPagesDataProvider
‪static sanitizeForPagesDataProvider()
Definition: SlugHelperTest.php:192
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateNeverDeliversEmptySlugDataProvider
‪static generateNeverDeliversEmptySlugDataProvider()
Definition: SlugHelperTest.php:154
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generatePrependsSlugsForNonPages
‪generatePrependsSlugsForNonPages(string $input, string $expected, array $options)
Definition: SlugHelperTest.php:709
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generatePrependsSlugsForPagesDataProvider
‪static generatePrependsSlugsForPagesDataProvider()
Definition: SlugHelperTest.php:380
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateSlugWithHookModifiers
‪generateSlugWithHookModifiers()
Definition: SlugHelperTest.php:625
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateNeverDeliversEmptySlugForPages
‪generateNeverDeliversEmptySlugForPages(string $input, string $expected)
Definition: SlugHelperTest.php:366
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateNeverDeliversEmptySlugForPagesDataProvider
‪static generateNeverDeliversEmptySlugForPagesDataProvider()
Definition: SlugHelperTest.php:342
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\sanitizeConvertsStringForPages
‪sanitizeConvertsStringForPages(array $configuration, string $input, string $expected)
Definition: SlugHelperTest.php:329
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\sanitizeDataProvider
‪static sanitizeDataProvider()
Definition: SlugHelperTest.php:29
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\sanitizeConvertsString
‪sanitizeConvertsString(array $configuration, string $input, string $expected)
Definition: SlugHelperTest.php:141
‪TYPO3\CMS\Core\DataHandling\SlugHelper
Definition: SlugHelper.php:43
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateSlugWithNavTitleAndFallbackForPages
‪generateSlugWithNavTitleAndFallbackForPages(array $input, string $expected, array $options)
Definition: SlugHelperTest.php:604
‪TYPO3\CMS\Core\Tests\Unit\DataHandling
Definition: DataHandlerTest.php:18
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generateSlugWithPid0
‪generateSlugWithPid0(array $input, string $expected)
Definition: SlugHelperTest.php:675
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generatePrependsSlugsForNonPagesDataProvider
‪static generatePrependsSlugsForNonPagesDataProvider()
Definition: SlugHelperTest.php:691
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest
Definition: SlugHelperTest.php:26
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\generatePrependsSlugsForPages
‪generatePrependsSlugsForPages(string $input, string $expected, array $options)
Definition: SlugHelperTest.php:465
‪TYPO3\CMS\Core\Tests\Unit\DataHandling\SlugHelperTest\$resetSingletonInstances
‪bool $resetSingletonInstances
Definition: SlugHelperTest.php:27