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