]> BookStack Code Mirror - bookstack/commitdiff
Started work on user slugs
authorDan Brown <redacted>
Mon, 8 Mar 2021 22:34:22 +0000 (22:34 +0000)
committerDan Brown <redacted>
Mon, 8 Mar 2021 22:34:22 +0000 (22:34 +0000)
Related to #2525

app/Auth/User.php
app/Entities/Models/Entity.php
app/Entities/Tools/SlugGenerator.php
app/Interfaces/Sluggable.php [new file with mode: 0644]
database/factories/ModelFactory.php
database/migrations/2021_03_08_215138_add_user_slug.php [new file with mode: 0644]

index 9d2210101b53ac98a1d1b94e5b667d1a7c5355ff..07232e1cc902f823427e7aec19db9e26fa37dab0 100644 (file)
@@ -2,6 +2,7 @@
 
 use BookStack\Api\ApiToken;
 use BookStack\Interfaces\Loggable;
+use BookStack\Interfaces\Sluggable;
 use BookStack\Model;
 use BookStack\Notifications\ResetPassword;
 use BookStack\Uploads\Image;
@@ -22,6 +23,7 @@ use Illuminate\Support\Collection;
  * Class User
  * @property string $id
  * @property string $name
+ * @property string $slug
  * @property string $email
  * @property string $password
  * @property Carbon $created_at
@@ -32,7 +34,7 @@ use Illuminate\Support\Collection;
  * @property string $system_name
  * @property Collection $roles
  */
-class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
+class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
 {
     use Authenticatable, CanResetPassword, Notifiable;
 
@@ -73,23 +75,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Returns the default public user.
-     * @return User
      */
-    public static function getDefault()
+    public static function getDefault(): User
     {
         if (!is_null(static::$defaultUser)) {
             return static::$defaultUser;
         }
         
-        static::$defaultUser = static::where('system_name', '=', 'public')->first();
+        static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
         return static::$defaultUser;
     }
 
     /**
      * Check if the user is the default public user.
-     * @return bool
      */
-    public function isDefault()
+    public function isDefault(): bool
     {
         return $this->system_name === 'public';
     }
@@ -116,12 +116,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Check if the user has a role.
-     * @param $role
-     * @return mixed
      */
-    public function hasSystemRole($role)
+    public function hasSystemRole(string $roleSystemName): bool
     {
-        return $this->roles->pluck('system_name')->contains($role);
+        return $this->roles->pluck('system_name')->contains($roleSystemName);
     }
 
     /**
@@ -185,9 +183,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get the social account associated with this user.
-     * @return HasMany
      */
-    public function socialAccounts()
+    public function socialAccounts(): HasMany
     {
         return $this->hasMany(SocialAccount::class);
     }
@@ -208,11 +205,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     }
 
     /**
-     * Returns the user's avatar,
-     * @param int $size
-     * @return string
+     * Returns a URL to the user's avatar
      */
-    public function getAvatar($size = 50)
+    public function getAvatar(int $size = 50): string
     {
         $default = url('/user_avatar.png');
         $imageId = $this->image_id;
@@ -230,9 +225,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get the avatar for the user.
-     * @return BelongsTo
      */
-    public function avatar()
+    public function avatar(): BelongsTo
     {
         return $this->belongsTo(Image::class, 'image_id');
     }
@@ -277,10 +271,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get a shortened version of the user's name.
-     * @param int $chars
-     * @return string
      */
-    public function getShortName($chars = 8)
+    public function getShortName(int $chars = 8): string
     {
         if (mb_strlen($this->name) <= $chars) {
             return $this->name;
index c6b2468b0814afd124d1c64a4e0f8b116b9ec8d5..f5f43fe8cfe2f9646675acc77235aa8c9db43282 100644 (file)
@@ -9,6 +9,7 @@ use BookStack\Auth\Permissions\JointPermission;
 use BookStack\Entities\Tools\SearchIndex;
 use BookStack\Entities\Tools\SlugGenerator;
 use BookStack\Facades\Permissions;
+use BookStack\Interfaces\Sluggable;
 use BookStack\Model;
 use BookStack\Traits\HasCreatorAndUpdater;
 use BookStack\Traits\HasOwner;
@@ -37,7 +38,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static Builder withLastView()
  * @method static Builder withViewCount()
  */
-abstract class Entity extends Model
+abstract class Entity extends Model implements Sluggable
 {
     use SoftDeletes;
     use HasCreatorAndUpdater;
index 7075bc72c14ce50e78119592be480f70c6756266..4501279f2a31d3c367e6a2ce34be9f1afedaf639 100644 (file)
@@ -1,6 +1,7 @@
 <?php namespace BookStack\Entities\Tools;
 
-use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Interfaces\Sluggable;
 use Illuminate\Support\Str;
 
 class SlugGenerator
@@ -10,11 +11,11 @@ class SlugGenerator
      * Generate a fresh slug for the given entity.
      * The slug will generated so it does not conflict within the same parent item.
      */
-    public function generate(Entity $entity): string
+    public function generate(Sluggable $model): string
     {
-        $slug = $this->formatNameAsSlug($entity->name);
-        while ($this->slugInUse($slug, $entity)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+        $slug = $this->formatNameAsSlug($model->name);
+        while ($this->slugInUse($slug, $model)) {
+            $slug .= '-' . Str::random(3);
         }
         return $slug;
     }
@@ -35,16 +36,16 @@ class SlugGenerator
      * Check if a slug is already in-use for this
      * type of model within the same parent.
      */
-    protected function slugInUse(string $slug, Entity $entity): bool
+    protected function slugInUse(string $slug, Sluggable $model): bool
     {
-        $query = $entity->newQuery()->where('slug', '=', $slug);
+        $query = $model->newQuery()->where('slug', '=', $slug);
 
-        if ($entity instanceof BookChild) {
-            $query->where('book_id', '=', $entity->book_id);
+        if ($model instanceof BookChild) {
+            $query->where('book_id', '=', $model->book_id);
         }
 
-        if ($entity->id) {
-            $query->where('id', '!=', $entity->id);
+        if ($model->id) {
+            $query->where('id', '!=', $model->id);
         }
 
         return $query->count() > 0;
diff --git a/app/Interfaces/Sluggable.php b/app/Interfaces/Sluggable.php
new file mode 100644 (file)
index 0000000..84f0e5b
--- /dev/null
@@ -0,0 +1,18 @@
+<?php namespace BookStack\Interfaces;
+
+use Illuminate\Database\Eloquent\Builder;
+
+/**
+ * Interface Sluggable
+ *
+ * Assigned to models that can have slugs.
+ * Must have the below properties.
+ *
+ * @property int $id
+ * @property string $name
+ * @method Builder newQuery
+ */
+interface Sluggable
+{
+
+}
\ No newline at end of file
index 405e5fcf4490a062408dd79569fd41099847ead3..722f68a7c9fdbe758aaf0ebb8101ce4431f16922 100644 (file)
 */
 
 $factory->define(\BookStack\Auth\User::class, function ($faker) {
+    $name = $faker->name;
     return [
-        'name' => $faker->name,
+        'name' => $name,
         'email' => $faker->email,
+        'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
         'password' => Str::random(10),
         'remember_token' => Str::random(10),
         'email_confirmed' => 1
diff --git a/database/migrations/2021_03_08_215138_add_user_slug.php b/database/migrations/2021_03_08_215138_add_user_slug.php
new file mode 100644 (file)
index 0000000..906e06b
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Str;
+
+class AddUserSlug extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('slug', 250);
+        });
+
+        $slugMap = [];
+        DB::table('users')->cursor()->each(function ($user) use (&$slugMap) {
+            $userSlug = Str::slug($user->name);
+            while (isset($slugMap[$userSlug])) {
+                $userSlug = Str::slug($user->name . Str::random(4));
+            }
+            $slugMap[$userSlug] = true;
+
+            DB::table('users')
+                ->where('id', $user->id)
+                ->update(['slug' => $userSlug]);
+        });
+
+        Schema::table('users', function (Blueprint $table) {
+            $table->unique('slug');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('slug');
+        });
+    }
+}