Started work on user slugs

Related to #2525
This commit is contained in:
Dan Brown 2021-03-08 22:34:22 +00:00
parent 34e6098687
commit 3a9caea846
6 changed files with 98 additions and 34 deletions

View File

@ -2,6 +2,7 @@
use BookStack\Api\ApiToken; use BookStack\Api\ApiToken;
use BookStack\Interfaces\Loggable; use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable;
use BookStack\Model; use BookStack\Model;
use BookStack\Notifications\ResetPassword; use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image; use BookStack\Uploads\Image;
@ -22,6 +23,7 @@ use Illuminate\Support\Collection;
* Class User * Class User
* @property string $id * @property string $id
* @property string $name * @property string $name
* @property string $slug
* @property string $email * @property string $email
* @property string $password * @property string $password
* @property Carbon $created_at * @property Carbon $created_at
@ -32,7 +34,7 @@ use Illuminate\Support\Collection;
* @property string $system_name * @property string $system_name
* @property Collection $roles * @property Collection $roles
*/ */
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
{ {
use Authenticatable, CanResetPassword, Notifiable; use Authenticatable, CanResetPassword, Notifiable;
@ -73,23 +75,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Returns the default public user. * Returns the default public user.
* @return User
*/ */
public static function getDefault() public static function getDefault(): User
{ {
if (!is_null(static::$defaultUser)) { if (!is_null(static::$defaultUser)) {
return 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; return static::$defaultUser;
} }
/** /**
* Check if the user is the default public user. * Check if the user is the default public user.
* @return bool
*/ */
public function isDefault() public function isDefault(): bool
{ {
return $this->system_name === 'public'; return $this->system_name === 'public';
} }
@ -116,12 +116,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Check if the user has a role. * 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. * Get the social account associated with this user.
* @return HasMany
*/ */
public function socialAccounts() public function socialAccounts(): HasMany
{ {
return $this->hasMany(SocialAccount::class); return $this->hasMany(SocialAccount::class);
} }
@ -208,11 +205,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
} }
/** /**
* Returns the user's avatar, * Returns a URL to the user's avatar
* @param int $size
* @return string
*/ */
public function getAvatar($size = 50) public function getAvatar(int $size = 50): string
{ {
$default = url('/user_avatar.png'); $default = url('/user_avatar.png');
$imageId = $this->image_id; $imageId = $this->image_id;
@ -230,9 +225,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Get the avatar for the user. * Get the avatar for the user.
* @return BelongsTo
*/ */
public function avatar() public function avatar(): BelongsTo
{ {
return $this->belongsTo(Image::class, 'image_id'); 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. * 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) { if (mb_strlen($this->name) <= $chars) {
return $this->name; return $this->name;

View File

@ -9,6 +9,7 @@ use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator; use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions; use BookStack\Facades\Permissions;
use BookStack\Interfaces\Sluggable;
use BookStack\Model; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater; use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner; use BookStack\Traits\HasOwner;
@ -37,7 +38,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView() * @method static Builder withLastView()
* @method static Builder withViewCount() * @method static Builder withViewCount()
*/ */
abstract class Entity extends Model abstract class Entity extends Model implements Sluggable
{ {
use SoftDeletes; use SoftDeletes;
use HasCreatorAndUpdater; use HasCreatorAndUpdater;

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Entities\Tools; <?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\BookChild;
use BookStack\Interfaces\Sluggable;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class SlugGenerator class SlugGenerator
@ -10,11 +11,11 @@ class SlugGenerator
* Generate a fresh slug for the given entity. * Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item. * 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); $slug = $this->formatNameAsSlug($model->name);
while ($this->slugInUse($slug, $entity)) { while ($this->slugInUse($slug, $model)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3); $slug .= '-' . Str::random(3);
} }
return $slug; return $slug;
} }
@ -35,16 +36,16 @@ class SlugGenerator
* Check if a slug is already in-use for this * Check if a slug is already in-use for this
* type of model within the same parent. * 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) { if ($model instanceof BookChild) {
$query->where('book_id', '=', $entity->book_id); $query->where('book_id', '=', $model->book_id);
} }
if ($entity->id) { if ($model->id) {
$query->where('id', '!=', $entity->id); $query->where('id', '!=', $model->id);
} }
return $query->count() > 0; return $query->count() > 0;

View File

@ -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
{
}

View File

@ -12,9 +12,11 @@
*/ */
$factory->define(\BookStack\Auth\User::class, function ($faker) { $factory->define(\BookStack\Auth\User::class, function ($faker) {
$name = $faker->name;
return [ return [
'name' => $faker->name, 'name' => $name,
'email' => $faker->email, 'email' => $faker->email,
'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
'password' => Str::random(10), 'password' => Str::random(10),
'remember_token' => Str::random(10), 'remember_token' => Str::random(10),
'email_confirmed' => 1 'email_confirmed' => 1

View File

@ -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');
});
}
}