From f7dd609b264d4266151efa3b8a4c5775526346fc Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 21 Aug 2022 15:27:41 +0100 Subject: [PATCH] feat: discussion UTF-8 slug driver (#3606) * feat: add utf-8 slug driver * test: add tests for slugging expectations * fix: non-word characters aren't removed Signed-off-by: Sami Mazouz Co-authored-by: Alexander Skvortsov --- framework/core/src/Discussion/Discussion.php | 1 + .../core/src/Discussion/Utf8SlugDriver.php | 46 +++++++++ .../core/src/Http/HttpServiceProvider.php | 4 +- .../integration/slugger/SlugDriverTest.php | 93 +++++++++++++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 framework/core/src/Discussion/Utf8SlugDriver.php create mode 100644 framework/core/tests/integration/slugger/SlugDriverTest.php diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index e81342296..10b6dfd13 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -441,6 +441,7 @@ class Discussion extends AbstractModel * * This automatically creates a matching slug for the discussion. * + * @todo slug should be set by the slugger, drop slug column entirely? * @param string $title */ protected function setTitleAttribute($title) diff --git a/framework/core/src/Discussion/Utf8SlugDriver.php b/framework/core/src/Discussion/Utf8SlugDriver.php new file mode 100644 index 000000000..b84facbbe --- /dev/null +++ b/framework/core/src/Discussion/Utf8SlugDriver.php @@ -0,0 +1,46 @@ +discussions = $discussions; + } + + public function toSlug(AbstractModel $instance): string + { + $slug = preg_replace('/[-\s]+/u', '-', $instance->title); + $slug = preg_replace('/[^\p{L}\p{N}\p{M}_-]+/u', '', $slug); + $slug = strtolower($slug); + + return $instance->id.(trim($slug) ? '-'.$slug : ''); + } + + public function fromSlug(string $slug, User $actor): AbstractModel + { + if (strpos($slug, '-')) { + $slug_array = explode('-', $slug); + $slug = $slug_array[0]; + } + + return $this->discussions->findOrFail($slug, $actor); + } +} diff --git a/framework/core/src/Http/HttpServiceProvider.php b/framework/core/src/Http/HttpServiceProvider.php index fee561746..26f0f6593 100644 --- a/framework/core/src/Http/HttpServiceProvider.php +++ b/framework/core/src/Http/HttpServiceProvider.php @@ -11,6 +11,7 @@ namespace Flarum\Http; use Flarum\Discussion\Discussion; use Flarum\Discussion\IdWithTransliteratedSlugDriver; +use Flarum\Discussion\Utf8SlugDriver; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\IdSlugDriver; @@ -37,7 +38,8 @@ class HttpServiceProvider extends AbstractServiceProvider $this->container->singleton('flarum.http.slugDrivers', function () { return [ Discussion::class => [ - 'default' => IdWithTransliteratedSlugDriver::class + 'default' => IdWithTransliteratedSlugDriver::class, + 'utf8' => Utf8SlugDriver::class, ], User::class => [ 'default' => UsernameSlugDriver::class, diff --git a/framework/core/tests/integration/slugger/SlugDriverTest.php b/framework/core/tests/integration/slugger/SlugDriverTest.php new file mode 100644 index 000000000..b83d6a076 --- /dev/null +++ b/framework/core/tests/integration/slugger/SlugDriverTest.php @@ -0,0 +1,93 @@ +prepareDatabase([ + 'users' => [ + $this->normalUser(), + ], + 'discussions' => [ + ['id' => 20, 'title' => 'Empty discussion', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0], + ['id' => 21, 'title' => 'తెలుగు', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0], + ['id' => 22, 'title' => '支持中文吗', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0], + ['id' => 23, 'title' => 'తెలుగు%$', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0], + ['id' => 24, 'title' => '支持中文吗%*', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null, 'comment_count' => 0, 'is_private' => 0], + ], + ]); + } + + /** + * @dataProvider slugInstancePairDataProvider + * @test + */ + public function slugger_formats_the_correct_slug_from_instance(string $driver, string $modelClassName, int $id, string $slug) + { + $this->setting("slug_driver_$modelClassName", $driver); + + /** @var SlugManager $slugger */ + $slugger = $this->app()->getContainer()->make(SlugManager::class)->forResource($modelClassName); + + $instance = $modelClassName::query()->find($id); + + /** @see Discussion::setTitleAttribute() */ + if ($modelClassName === Discussion::class) { + $instance->title = $instance->title; + } + + $this->assertEquals($slug, $slugger->toSlug($instance)); + } + + /** + * @dataProvider slugInstancePairDataProvider + * @test + */ + public function slugger_returns_the_correct_instance_from_slug(string $driver, string $modelClassName, int $id, string $slug) + { + $this->setting("slug_driver_$modelClassName", $driver); + + /** @var SlugManager $slugger */ + $slugger = $this->app()->getContainer()->make(SlugManager::class)->forResource($modelClassName); + + $this->assertEquals($modelClassName::query()->find($id), $slugger->fromSlug($slug, User::query()->find(1))); + } + + public function slugInstancePairDataProvider(): array + { + return [ + ['default', Discussion::class, 20, '20-empty-discussion'], + ['default', Discussion::class, 21, '21'], + ['default', Discussion::class, 22, '22'], + + ['utf8', Discussion::class, 20, '20-empty-discussion'], + ['utf8', Discussion::class, 21, '21-తెలుగు'], + ['utf8', Discussion::class, 22, '22-支持中文吗'], + ['utf8', Discussion::class, 23, '23-తెలుగు'], + ['utf8', Discussion::class, 24, '24-支持中文吗'], + + ['default', User::class, 2, 'normal'], + ['id', User::class, 2, '2'], + ]; + } +}