Skip to content

Commit 615038a

Browse files
authored
Merge pull request #2626 from BookStackApp/2525_add_user_slugs
User slugs
2 parents 34e6098 + 3c57cbc commit 615038a

File tree

18 files changed

+210
-102
lines changed

18 files changed

+210
-102
lines changed

app/Auth/Access/SocialAuthService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ public function fillSocialAccount(string $socialDriver, SocialUser $socialUser):
221221
* Detach a social account from a user.
222222
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
223223
*/
224-
public function detachSocialAccount(string $socialDriver)
224+
public function detachSocialAccount(string $socialDriver): void
225225
{
226226
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
227227
}

app/Auth/User.php

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<?php namespace BookStack\Auth;
22

33
use BookStack\Api\ApiToken;
4+
use BookStack\Entities\Tools\SlugGenerator;
45
use BookStack\Interfaces\Loggable;
6+
use BookStack\Interfaces\Sluggable;
57
use BookStack\Model;
68
use BookStack\Notifications\ResetPassword;
79
use BookStack\Uploads\Image;
@@ -22,6 +24,7 @@
2224
* Class User
2325
* @property string $id
2426
* @property string $name
27+
* @property string $slug
2528
* @property string $email
2629
* @property string $password
2730
* @property Carbon $created_at
@@ -32,7 +35,7 @@
3235
* @property string $system_name
3336
* @property Collection $roles
3437
*/
35-
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
38+
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
3639
{
3740
use Authenticatable, CanResetPassword, Notifiable;
3841

@@ -73,23 +76,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
7376

7477
/**
7578
* Returns the default public user.
76-
* @return User
7779
*/
78-
public static function getDefault()
80+
public static function getDefault(): User
7981
{
8082
if (!is_null(static::$defaultUser)) {
8183
return static::$defaultUser;
8284
}
8385

84-
static::$defaultUser = static::where('system_name', '=', 'public')->first();
86+
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
8587
return static::$defaultUser;
8688
}
8789

8890
/**
8991
* Check if the user is the default public user.
90-
* @return bool
9192
*/
92-
public function isDefault()
93+
public function isDefault(): bool
9394
{
9495
return $this->system_name === 'public';
9596
}
@@ -116,12 +117,10 @@ public function hasRole($roleId): bool
116117

117118
/**
118119
* Check if the user has a role.
119-
* @param $role
120-
* @return mixed
121120
*/
122-
public function hasSystemRole($role)
121+
public function hasSystemRole(string $roleSystemName): bool
123122
{
124-
return $this->roles->pluck('system_name')->contains($role);
123+
return $this->roles->pluck('system_name')->contains($roleSystemName);
125124
}
126125

127126
/**
@@ -185,9 +184,8 @@ public function attachRole(Role $role)
185184

186185
/**
187186
* Get the social account associated with this user.
188-
* @return HasMany
189187
*/
190-
public function socialAccounts()
188+
public function socialAccounts(): HasMany
191189
{
192190
return $this->hasMany(SocialAccount::class);
193191
}
@@ -208,11 +206,9 @@ public function hasSocialAccount($socialDriver = false)
208206
}
209207

210208
/**
211-
* Returns the user's avatar,
212-
* @param int $size
213-
* @return string
209+
* Returns a URL to the user's avatar
214210
*/
215-
public function getAvatar($size = 50)
211+
public function getAvatar(int $size = 50): string
216212
{
217213
$default = url('/user_avatar.png');
218214
$imageId = $this->image_id;
@@ -230,9 +226,8 @@ public function getAvatar($size = 50)
230226

231227
/**
232228
* Get the avatar for the user.
233-
* @return BelongsTo
234229
*/
235-
public function avatar()
230+
public function avatar(): BelongsTo
236231
{
237232
return $this->belongsTo(Image::class, 'image_id');
238233
}
@@ -272,15 +267,13 @@ public function getEditUrl(string $path = ''): string
272267
*/
273268
public function getProfileUrl(): string
274269
{
275-
return url('/user/' . $this->id);
270+
return url('/user/' . $this->slug);
276271
}
277272

278273
/**
279274
* Get a shortened version of the user's name.
280-
* @param int $chars
281-
* @return string
282275
*/
283-
public function getShortName($chars = 8)
276+
public function getShortName(int $chars = 8): string
284277
{
285278
if (mb_strlen($this->name) <= $chars) {
286279
return $this->name;
@@ -311,4 +304,13 @@ public function logDescriptor(): string
311304
{
312305
return "({$this->id}) {$this->name}";
313306
}
307+
308+
/**
309+
* @inheritDoc
310+
*/
311+
public function refreshSlug(): string
312+
{
313+
$this->slug = app(SlugGenerator::class)->generate($this);
314+
return $this->slug;
315+
}
314316
}

app/Auth/UserRepo.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ public function getById(int $id): User
4545
return User::query()->findOrFail($id);
4646
}
4747

48+
/**
49+
* Get a user by their slug.
50+
*/
51+
public function getBySlug(string $slug): User
52+
{
53+
return User::query()->where('slug', '=', $slug)->firstOrFail();
54+
}
55+
4856
/**
4957
* Get all the users with their permissions.
5058
*/
@@ -159,7 +167,13 @@ public function create(array $data, bool $emailConfirmed = false): User
159167
'email_confirmed' => $emailConfirmed,
160168
'external_auth_id' => $data['external_auth_id'] ?? '',
161169
];
162-
return User::query()->forceCreate($details);
170+
171+
$user = new User();
172+
$user->forceFill($details);
173+
$user->refreshSlug();
174+
$user->save();
175+
176+
return $user;
163177
}
164178

165179
/**

app/Entities/Models/Entity.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use BookStack\Entities\Tools\SearchIndex;
1010
use BookStack\Entities\Tools\SlugGenerator;
1111
use BookStack\Facades\Permissions;
12+
use BookStack\Interfaces\Sluggable;
1213
use BookStack\Model;
1314
use BookStack\Traits\HasCreatorAndUpdater;
1415
use BookStack\Traits\HasOwner;
@@ -37,7 +38,7 @@
3738
* @method static Builder withLastView()
3839
* @method static Builder withViewCount()
3940
*/
40-
abstract class Entity extends Model
41+
abstract class Entity extends Model implements Sluggable
4142
{
4243
use SoftDeletes;
4344
use HasCreatorAndUpdater;
@@ -289,11 +290,11 @@ public function indexForSearch()
289290
}
290291

291292
/**
292-
* Generate and set a new URL slug for this model.
293+
* @inheritdoc
293294
*/
294295
public function refreshSlug(): string
295296
{
296-
$this->slug = (new SlugGenerator)->generate($this);
297+
$this->slug = app(SlugGenerator::class)->generate($this);
297298
return $this->slug;
298299
}
299300
}

app/Entities/Tools/SearchRunner.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace BookStack\Entities\Tools;
22

33
use BookStack\Auth\Permissions\PermissionService;
4+
use BookStack\Auth\User;
45
use BookStack\Entities\EntityProvider;
56
use BookStack\Entities\Models\Entity;
67
use Illuminate\Database\Connection;
@@ -270,24 +271,20 @@ protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $i
270271

271272
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
272273
{
273-
if (!is_numeric($input) && $input !== 'me') {
274-
return;
275-
}
276-
if ($input === 'me') {
277-
$input = user()->id;
274+
$userSlug = $input === 'me' ? user()->slug : trim($input);
275+
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
276+
if ($user) {
277+
$query->where('created_by', '=', $user->id);
278278
}
279-
$query->where('created_by', '=', $input);
280279
}
281280

282281
protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
283282
{
284-
if (!is_numeric($input) && $input !== 'me') {
285-
return;
286-
}
287-
if ($input === 'me') {
288-
$input = user()->id;
283+
$userSlug = $input === 'me' ? user()->slug : trim($input);
284+
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
285+
if ($user) {
286+
$query->where('updated_by', '=', $user->id);
289287
}
290-
$query->where('updated_by', '=', $input);
291288
}
292289

293290
protected function filterInName(EloquentBuilder $query, Entity $model, $input)

app/Entities/Tools/SlugGenerator.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace BookStack\Entities\Tools;
22

3-
use BookStack\Entities\Models\Entity;
3+
use BookStack\Entities\Models\BookChild;
4+
use BookStack\Interfaces\Sluggable;
45
use Illuminate\Support\Str;
56

67
class SlugGenerator
@@ -10,11 +11,11 @@ class SlugGenerator
1011
* Generate a fresh slug for the given entity.
1112
* The slug will generated so it does not conflict within the same parent item.
1213
*/
13-
public function generate(Entity $entity): string
14+
public function generate(Sluggable $model): string
1415
{
15-
$slug = $this->formatNameAsSlug($entity->name);
16-
while ($this->slugInUse($slug, $entity)) {
17-
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
16+
$slug = $this->formatNameAsSlug($model->name);
17+
while ($this->slugInUse($slug, $model)) {
18+
$slug .= '-' . Str::random(3);
1819
}
1920
return $slug;
2021
}
@@ -35,16 +36,16 @@ protected function formatNameAsSlug(string $name): string
3536
* Check if a slug is already in-use for this
3637
* type of model within the same parent.
3738
*/
38-
protected function slugInUse(string $slug, Entity $entity): bool
39+
protected function slugInUse(string $slug, Sluggable $model): bool
3940
{
40-
$query = $entity->newQuery()->where('slug', '=', $slug);
41+
$query = $model->newQuery()->where('slug', '=', $slug);
4142

42-
if ($entity instanceof BookChild) {
43-
$query->where('book_id', '=', $entity->book_id);
43+
if ($model instanceof BookChild) {
44+
$query->where('book_id', '=', $model->book_id);
4445
}
4546

46-
if ($entity->id) {
47-
$query->where('id', '!=', $entity->id);
47+
if ($model->id) {
48+
$query->where('id', '!=', $model->id);
4849
}
4950

5051
return $query->count() > 0;

app/Http/Controllers/Auth/SocialController.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
use BookStack\Exceptions\SocialSignInException;
1010
use BookStack\Exceptions\UserRegistrationException;
1111
use BookStack\Http\Controllers\Controller;
12-
use Illuminate\Http\RedirectResponse;
1312
use Illuminate\Http\Request;
14-
use Illuminate\Routing\Redirector;
1513
use Illuminate\Support\Str;
1614
use Laravel\Socialite\Contracts\User as SocialUser;
1715

@@ -31,12 +29,11 @@ public function __construct(SocialAuthService $socialAuthService, RegistrationSe
3129
$this->registrationService = $registrationService;
3230
}
3331

34-
3532
/**
3633
* Redirect to the relevant social site.
37-
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
34+
* @throws SocialDriverNotConfigured
3835
*/
39-
public function getSocialLogin(string $socialDriver)
36+
public function login(string $socialDriver)
4037
{
4138
session()->put('social-callback', 'login');
4239
return $this->socialAuthService->startLogIn($socialDriver);
@@ -47,7 +44,7 @@ public function getSocialLogin(string $socialDriver)
4744
* @throws SocialDriverNotConfigured
4845
* @throws UserRegistrationException
4946
*/
50-
public function socialRegister(string $socialDriver)
47+
public function register(string $socialDriver)
5148
{
5249
$this->registrationService->ensureRegistrationAllowed();
5350
session()->put('social-callback', 'register');
@@ -60,7 +57,7 @@ public function socialRegister(string $socialDriver)
6057
* @throws SocialDriverNotConfigured
6158
* @throws UserRegistrationException
6259
*/
63-
public function socialCallback(Request $request, string $socialDriver)
60+
public function callback(Request $request, string $socialDriver)
6461
{
6562
if (!session()->has('social-callback')) {
6663
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
@@ -99,7 +96,7 @@ public function socialCallback(Request $request, string $socialDriver)
9996
/**
10097
* Detach a social account from a user.
10198
*/
102-
public function detachSocialAccount(string $socialDriver)
99+
public function detach(string $socialDriver)
103100
{
104101
$this->socialAuthService->detachSocialAccount($socialDriver);
105102
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));

app/Http/Controllers/SearchController.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
<?php namespace BookStack\Http\Controllers;
22

33
use BookStack\Actions\ViewService;
4-
use BookStack\Entities\Models\Book;
5-
use BookStack\Entities\Models\Bookshelf;
6-
use BookStack\Entities\Models\Entity;
74
use BookStack\Entities\Tools\SearchRunner;
85
use BookStack\Entities\Tools\ShelfContext;
96
use BookStack\Entities\Tools\SearchOptions;

0 commit comments

Comments
 (0)