Cleaned up some user/image areas of the app

Further cleanup of docblocks and standardisation of repos.
This commit is contained in:
Dan Brown 2020-12-08 23:46:38 +00:00
parent 6c09334ba0
commit 00308ad4ab
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 146 additions and 157 deletions

View File

@ -5,6 +5,7 @@ use BookStack\Auth\Permissions\RolePermission;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
@ -23,7 +24,7 @@ class Role extends Model implements Loggable
/**
* The roles that belong to the role.
*/
public function users()
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
@ -39,7 +40,7 @@ class Role extends Model implements Loggable
/**
* The RolePermissions that belong to the role.
*/
public function permissions()
public function permissions(): BelongsToMany
{
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}

View File

@ -8,25 +8,24 @@ use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
{
protected $user;
protected $role;
protected $userAvatar;
/**
* UserRepo constructor.
*/
public function __construct(User $user, Role $role)
public function __construct(UserAvatars $userAvatar)
{
$this->user = $user;
$this->role = $role;
$this->userAvatar = $userAvatar;
}
/**
@ -34,25 +33,23 @@ class UserRepo
*/
public function getByEmail(string $email): ?User
{
return $this->user->where('email', '=', $email)->first();
return User::query()->where('email', '=', $email)->first();
}
/**
* @param int $id
* @return User
* Get a user by their ID.
*/
public function getById($id)
public function getById(int $id): User
{
return $this->user->newQuery()->findOrFail($id);
return User::query()->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return Builder|static
*/
public function getAllUsers()
public function getAllUsers(): Collection
{
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
@ -68,7 +65,7 @@ class UserRepo
->take(1);
}
$query = $this->user->with(['roles', 'avatar', 'latestActivity'])
$query = User::query()->with(['roles', 'avatar', 'latestActivity'])
->orderBy($sort, $sortData['order']);
if ($sortData['search']) {
@ -96,14 +93,12 @@ class UserRepo
/**
* Assign a user to a system-level role.
* @param User $user
* @param $systemRoleName
* @throws NotFoundException
*/
public function attachSystemRole(User $user, $systemRoleName)
public function attachSystemRole(User $user, string $systemRoleName)
{
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
if ($role === null) {
$role = Role::getSystemRole($systemRoleName);
if (is_null($role)) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
@ -111,26 +106,23 @@ class UserRepo
/**
* Checks if the give user is the only admin.
* @param User $user
* @return bool
*/
public function isOnlyAdmin(User $user)
public function isOnlyAdmin(User $user): bool
{
if (!$user->hasSystemRole('admin')) {
return false;
}
$adminRole = $this->role->getSystemRole('admin');
if ($adminRole->users->count() > 1) {
$adminRole = Role::getSystemRole('admin');
if ($adminRole->users()->count() > 1) {
return false;
}
return true;
}
/**
* Set the assigned user roles via an array of role IDs.
* @param User $user
* @param array $roles
* @throws UserUpdateException
*/
public function setUserRoles(User $user, array $roles)
@ -145,14 +137,11 @@ class UserRepo
/**
* Check if the given user is the last admin and their new roles no longer
* contains the admin role.
* @param User $user
* @param array $newRoles
* @return bool
*/
protected function demotingLastAdmin(User $user, array $newRoles) : bool
{
if ($this->isOnlyAdmin($user)) {
$adminRole = $this->role->getSystemRole('admin');
$adminRole = Role::getSystemRole('admin');
if (!in_array(strval($adminRole->id), $newRoles)) {
return true;
}
@ -166,18 +155,18 @@ class UserRepo
*/
public function create(array $data, bool $emailConfirmed = false): User
{
return $this->user->forceCreate([
$details = [
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => $emailConfirmed,
'external_auth_id' => $data['external_auth_id'] ?? '',
]);
];
return User::query()->forceCreate($details);
}
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
* @throws Exception
*/
public function destroy(User $user)
@ -187,7 +176,10 @@ class UserRepo
$user->delete();
// Delete user profile images
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
@ -195,12 +187,8 @@ class UserRepo
/**
* Get the latest activity for a user.
* @param User $user
* @param int $count
* @param int $page
* @return array
*/
public function getActivity(User $user, $count = 20, $page = 0)
public function getActivity(User $user, int $count = 20, int $page = 0): array
{
return Activity::userActivity($user, $count, $page);
}
@ -241,33 +229,22 @@ class UserRepo
/**
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
return Role::query()->orderBy('display_name', 'asc')->get();
}
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
* @param User $user
* @return bool
*/
public function downloadAndAssignUserAvatar(User $user)
public function downloadAndAssignUserAvatar(User $user): void
{
if (!Images::avatarFetchEnabled()) {
return false;
}
try {
$avatar = Images::saveUserAvatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
$this->userAvatar->fetchAndAssignToUser($user);
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
return false;
}
}
}

View File

@ -28,8 +28,6 @@ class CreateAdmin extends Command
/**
* Create a new command instance.
*
* @param UserRepo $userRepo
*/
public function __construct(UserRepo $userRepo)
{

View File

@ -205,12 +205,12 @@ abstract class Entity extends Ownable
}
/**
* Get entity type.
* @return mixed
* Get the entity type as a simple lowercase word.
*/
public static function getType()
public static function getType(): string
{
return strtolower(static::getClassName());
$className = array_slice(explode('\\', static::class), -1, 1)[0];
return strtolower($className);
}
/**

View File

@ -188,7 +188,7 @@ class UserController extends Controller
$user->image_id = $image->id;
}
// Delete the profile image if set to
// Delete the profile image if reset option is in request
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
}

View File

@ -26,12 +26,4 @@ abstract class Ownable extends Model
return $this->belongsTo(User::class, 'updated_by');
}
/**
* Gets the class name.
* @return string
*/
public static function getClassName()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
}

View File

@ -1,7 +1,5 @@
<?php namespace BookStack\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
use BookStack\Exceptions\ImageUploadException;
use DB;
use ErrorException;
@ -17,24 +15,21 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService
{
protected $imageTool;
protected $cache;
protected $storageUrl;
protected $image;
protected $http;
protected $fileSystem;
/**
* ImageService constructor.
*/
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->image = $image;
$this->imageTool = $imageTool;
$this->fileSystem = $fileSystem;
$this->cache = $cache;
$this->http = $http;
}
/**
@ -77,14 +72,9 @@ class ImageService
/**
* Save a new image from a uri-encoded base64 string of data.
* @param string $base64Uri
* @param string $name
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, int $uploadedTo = 0): Image
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
@ -94,30 +84,11 @@ class ImageService
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/**
* Gets an image from url and saves it to the database.
* @param $url
* @param string $type
* @param bool|string $imageName
* @return mixed
* @throws Exception
*/
private function saveNewFromUrl($url, $type, $imageName = false)
{
$imageName = $imageName ? $imageName : basename($url);
try {
$imageData = $this->http->fetch($url);
} catch (HttpFetchException $exception) {
throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
return $this->saveNew($imageName, $imageData, $type);
}
/**
* Save a new image into storage.
* @throws ImageUploadException
*/
private function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
{
$storage = $this->getStorage($type);
$secureUploads = setting('app-secure-images');
@ -327,56 +298,6 @@ class ImageService
return (count($files) === 0 && count($folders) === 0);
}
/**
* Save an avatar image from an external service.
* @throws Exception
*/
public function saveUserAvatar(User $user, int $size = 500): Image
{
$avatarUrl = $this->getAvatarUrl();
$email = strtolower(trim($user->email));
$replacements = [
'${hash}' => md5($email),
'${size}' => $size,
'${email}' => urlencode($email),
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageName = str_replace(' ', '-', $user->name . '-avatar.png');
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->uploaded_to = $user->id;
$image->save();
return $image;
}
/**
* Check if fetching external avatars is enabled.
*/
public function avatarFetchEnabled(): bool
{
$fetchUrl = $this->getAvatarUrl();
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
}
/**
* Get the URL to fetch avatars from.
* @return string|mixed
*/
protected function getAvatarUrl()
{
$url = trim(config('services.avatar_url'));
if (empty($url) && !config('services.disable_services')) {
$url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
}
return $url;
}
/**
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
* Checks based off of only the image name.

100
app/Uploads/UserAvatars.php Normal file
View File

@ -0,0 +1,100 @@
<?php namespace BookStack\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
use Exception;
class UserAvatars
{
protected $imageService;
protected $http;
public function __construct(ImageService $imageService, HttpFetcher $http)
{
$this->imageService = $imageService;
$this->http = $http;
}
/**
* Fetch and assign an avatar image to the given user.
*/
public function fetchAndAssignToUser(User $user): void
{
if (!$this->avatarFetchEnabled()) {
return;
}
try {
$avatar = $this->saveAvatarImage($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
}
}
/**
* Save an avatar image from an external service.
* @throws Exception
*/
protected function saveAvatarImage(User $user, int $size = 500): Image
{
$avatarUrl = $this->getAvatarUrl();
$email = strtolower(trim($user->email));
$replacements = [
'${hash}' => md5($email),
'${size}' => $size,
'${email}' => urlencode($email),
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageName = str_replace(' ', '-', $user->id . '-avatar.png');
$imageData = $this->getAvatarImageData($userAvatarUrl);
$image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->save();
return $image;
}
/**
* Gets an image from url and returns it as a string of image data.
* @throws Exception
*/
protected function getAvatarImageData(string $url): string
{
try {
$imageData = $this->http->fetch($url);
} catch (HttpFetchException $exception) {
throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
return $imageData;
}
/**
* Check if fetching external avatars is enabled.
*/
protected function avatarFetchEnabled(): bool
{
$fetchUrl = $this->getAvatarUrl();
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
}
/**
* Get the URL to fetch avatars from.
*/
protected function getAvatarUrl(): string
{
$url = trim(config('services.avatar_url'));
if (empty($url) && !config('services.disable_services')) {
$url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
}
return $url;
}
}

View File

@ -13,8 +13,8 @@
<ul class="sortable-page-list sort-list">
@foreach($bookChildren as $bookChild)
<li class="text-{{ $bookChild->getClassName() }}"
data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getClassName() }}"
<li class="text-{{ $bookChild->getType() }}"
data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}"
data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}"
data-updated="{{ $bookChild->updated_at->timestamp }}">
<div class="entity-list-item">

View File

@ -12,7 +12,7 @@
@endif
@foreach($sidebarTree as $bookChild)
<li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
<li class="list-item-{{ $bookChild->getType() }} {{ $bookChild->getType() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
@include('partials.entity-list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)