use BookStack\Api\ApiToken;
use BookStack\Interfaces\Loggable;
+use BookStack\Interfaces\Sluggable;
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
* Class User
* @property string $id
* @property string $name
+ * @property string $slug
* @property string $email
* @property string $password
* @property Carbon $created_at
* @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;
/**
* 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';
}
/**
* 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);
}
/**
/**
* Get the social account associated with this user.
- * @return HasMany
*/
- public function socialAccounts()
+ public function socialAccounts(): HasMany
{
return $this->hasMany(SocialAccount::class);
}
}
/**
- * 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;
/**
* Get the avatar for the user.
- * @return BelongsTo
*/
- public function avatar()
+ public function avatar(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
* 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;
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;
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
-abstract class Entity extends Model
+abstract class Entity extends Model implements Sluggable
{
use SoftDeletes;
use HasCreatorAndUpdater;
<?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
* 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;
}
* 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;
--- /dev/null
+<?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
*/
$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
--- /dev/null
+<?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');
+ });
+ }
+}