From 79f6dc00a366fbb318ac70db7ec1128615482655 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 May 2019 15:48:15 +0100 Subject: [PATCH] Change image-selector to not use manager - Now changes the images directly for user, system & cover. - Extra permission checks added to edit & delete actions. --- app/Auth/User.php | 2 +- app/Http/Controllers/BookController.php | 54 +++- app/Http/Controllers/BookshelfController.php | 14 +- app/Http/Controllers/ImageController.php | 242 ------------------ .../Images/CoverImageController.php | 99 ------- .../Images/DrawioImageController.php | 22 ++ .../Controllers/Images/ImageController.php | 115 +++++++++ .../Images/SystemImageController.php | 67 ----- .../Images/UserImageController.php | 73 ------ app/Http/Controllers/SettingController.php | 33 ++- app/Http/Controllers/UserController.php | 32 ++- app/Uploads/Image.php | 11 + app/Uploads/ImageRepo.php | 58 ++--- app/Uploads/ImageService.php | 54 +++- .../assets/js/components/image-picker.js | 56 ++-- .../assets/js/components/markdown-editor.js | 4 +- .../assets/js/components/wysiwyg-editor.js | 4 +- resources/assets/js/services/drawio.js | 12 +- resources/assets/sass/_layout.scss | 4 + resources/lang/en/validation.php | 1 + resources/views/books/create.blade.php | 1 - resources/views/books/edit.blade.php | 4 +- resources/views/books/form.blade.php | 8 +- .../views/components/image-picker.blade.php | 22 +- resources/views/settings/index.blade.php | 10 +- resources/views/shelves/create.blade.php | 2 - resources/views/shelves/edit.blade.php | 2 +- resources/views/shelves/form.blade.php | 8 +- resources/views/users/edit.blade.php | 5 +- routes/web.php | 21 +- 30 files changed, 415 insertions(+), 625 deletions(-) delete mode 100644 app/Http/Controllers/ImageController.php delete mode 100644 app/Http/Controllers/Images/CoverImageController.php create mode 100644 app/Http/Controllers/Images/ImageController.php delete mode 100644 app/Http/Controllers/Images/SystemImageController.php delete mode 100644 app/Http/Controllers/Images/UserImageController.php diff --git a/app/Auth/User.php b/app/Auth/User.php index 05e77e13d..12f022b06 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon * The attributes that are mass assignable. * @var array */ - protected $fillable = ['name', 'email', 'image_id']; + protected $fillable = ['name', 'email']; /** * The attributes excluded from the model's JSON form. diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 24e0d784d..7c8ad5c2f 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -6,6 +6,7 @@ use BookStack\Entities\Book; use BookStack\Entities\EntityContextManager; use BookStack\Entities\Repos\EntityRepo; use BookStack\Entities\ExportService; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; use Views; @@ -17,6 +18,7 @@ class BookController extends Controller protected $userRepo; protected $exportService; protected $entityContextManager; + protected $imageRepo; /** * BookController constructor. @@ -24,17 +26,20 @@ class BookController extends Controller * @param UserRepo $userRepo * @param ExportService $exportService * @param EntityContextManager $entityContextManager + * @param ImageRepo $imageRepo */ public function __construct( EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService, - EntityContextManager $entityContextManager + EntityContextManager $entityContextManager, + ImageRepo $imageRepo ) { $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; $this->exportService = $exportService; $this->entityContextManager = $entityContextManager; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -101,13 +106,15 @@ class BookController extends Controller * @param string $shelfSlug * @return Response * @throws \BookStack\Exceptions\NotFoundException + * @throws \BookStack\Exceptions\ImageUploadException */ public function store(Request $request, string $shelfSlug = null) { $this->checkPermission('book-create-all'); $this->validate($request, [ 'name' => 'required|string|max:255', - 'description' => 'string|max:1000' + 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); $bookshelf = null; @@ -117,6 +124,7 @@ class BookController extends Controller } $book = $this->entityRepo->createFromInput('book', $request->all()); + $this->bookUpdateActions($book, $request); Activity::add($book, 'book_create', $book->id); if ($bookshelf) { @@ -170,20 +178,27 @@ class BookController extends Controller /** * Update the specified book in storage. - * @param Request $request + * @param Request $request * @param $slug * @return Response + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \BookStack\Exceptions\NotFoundException */ - public function update(Request $request, $slug) + public function update(Request $request, string $slug) { $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-update', $book); $this->validate($request, [ 'name' => 'required|string|max:255', - 'description' => 'string|max:1000' + 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); + $book = $this->entityRepo->updateFromInput('book', $book, $request->all()); + $this->bookUpdateActions($book, $request); + Activity::add($book, 'book_update', $book->id); + return redirect($book->getUrl()); } @@ -311,7 +326,12 @@ class BookController extends Controller $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-delete', $book); Activity::addMessage('book_delete', 0, $book->name); + + if ($book->cover) { + $this->imageRepo->destroyImage($book->cover); + } $this->entityRepo->destroyBook($book); + return redirect('/books'); } @@ -383,4 +403,28 @@ class BookController extends Controller $textContent = $this->exportService->bookToPlainText($book); return $this->downloadResponse($textContent, $bookSlug . '.txt'); } + + /** + * Common actions to run on book update. + * Handles updating the cover image. + * @param Book $book + * @param Request $request + * @throws \BookStack\Exceptions\ImageUploadException + */ + protected function bookUpdateActions(Book $book, Request $request) + { + // Update the cover image if in request + if ($request->has('image')) { + $newImage = $request->file('image'); + $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true); + $book->image_id = $image->id; + $book->save(); + } + + if ($request->has('image_reset')) { + $this->imageRepo->destroyImage($book->cover); + $book->image_id = 0; + $book->save(); + } + } } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index e63cfd1d5..dba2503ef 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -86,8 +86,9 @@ class BookshelfController extends Controller /** * Store a newly created bookshelf in storage. - * @param Request $request + * @param Request $request * @return Response + * @throws \BookStack\Exceptions\ImageUploadException */ public function store(Request $request) { @@ -284,10 +285,17 @@ class BookshelfController extends Controller $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); // Update the cover image if in request - if ($request->has('image') && userCan('image-create-all')) { - $image = $this->imageRepo->saveNew($request->file('image'), 'cover', $shelf->id); + if ($request->has('image')) { + $newImage = $request->file('image'); + $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true); $shelf->image_id = $image->id; $shelf->save(); } + + if ($request->has('image_reset')) { + $this->imageRepo->destroyImage($shelf->cover); + $shelf->image_id = 0; + $shelf->save(); + } } } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php deleted file mode 100644 index df7758176..000000000 --- a/app/Http/Controllers/ImageController.php +++ /dev/null @@ -1,242 +0,0 @@ -image = $image; - $this->file = $file; - $this->imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Provide an image file from storage. - * @param string $path - * @return mixed - */ - public function showImage(string $path) - { - $path = storage_path('uploads/images/' . $path); - if (!file_exists($path)) { - abort(404); - } - - return response()->file($path); - } - - /** - * Get all images for a specific type, Paginated - * @param Request $request - * @param string $type - * @param int $page - * @return \Illuminate\Http\JsonResponse - */ - public function getAllByType(Request $request, $type, $page = 0) - { - $uploadedToFilter = $request->get('uploaded_to', null); - - // For user profile request, check access to user images - if ($type === 'user') { - $this->checkPermissionOrCurrentUser('users-manage', $uploadedToFilter ?? 0); - } - - $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, $uploadedToFilter); - return response()->json($imgData); - } - - /** - * Search through images within a particular type. - * @param $type - * @param int $page - * @param Request $request - * @return mixed - */ - public function searchByType(Request $request, $type, $page = 0) - { - $this->validate($request, [ - 'term' => 'required|string' - ]); - - $searchTerm = $request->get('term'); - $imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24); - return response()->json($imgData); - } - - public function uploadUserImage(Request $request) - { - // TODO - } - - public function uploadSystemImage(Request $request) - { - // TODO - } - - public function uploadCoverImage(Request $request) - { - // TODO - } - - /** - * Upload a draw.io image into the system. - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function uploadDrawioImage(Request $request) - { - $this->validate($request, [ - 'image' => 'required|string', - 'uploaded_to' => 'required|integer' - ]); - $uploadedTo = $request->get('uploaded_to', 0); - $page = $this-> - $this->checkPermission('image-create-all'); - $imageBase64Data = $request->get('image'); - - try { - $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - - /** - * Handles image uploads for use on pages. - * @param string $type - * @param Request $request - * @return \Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function uploadByType($type, Request $request) - { - $this->checkPermission('image-create-all'); - $this->validate($request, [ - 'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff' - ]); - - if (!$this->imageRepo->isValidType($type)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $imageUpload = $request->file('file'); - - try { - $uploadedTo = $request->get('uploaded_to', 0); - - // For user profile request, check access to user images - if ($type === 'user') { - $this->checkPermissionOrCurrentUser('users-manage', $uploadedTo ?? 0); - } - - $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - /** - * Get the content of an image based64 encoded. - * @param $id - * @return \Illuminate\Http\JsonResponse|mixed - */ - public function getBase64Image($id) - { - $image = $this->imageRepo->getById($id); - $imageData = $this->imageRepo->getImageData($image); - if ($imageData === null) { - return $this->jsonError("Image data could not be found"); - } - return response()->json([ - 'content' => base64_encode($imageData) - ]); - } - - /** - * Generate a sized thumbnail for an image. - * @param $id - * @param $width - * @param $height - * @param $crop - * @return \Illuminate\Http\JsonResponse - * @throws ImageUploadException - * @throws \Exception - */ - public function getThumbnail($id, $width, $height, $crop) - { - $this->checkPermission('image-create-all'); - $image = $this->imageRepo->getById($id); - $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); - return response()->json(['url' => $thumbnailUrl]); - } - - /** - * Update image details - * @param integer $id - * @param Request $request - * @return \Illuminate\Http\JsonResponse - * @throws ImageUploadException - * @throws \Exception - */ - public function update($id, Request $request) - { - $this->validate($request, [ - 'name' => 'required|min:2|string' - ]); - - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('image-update', $image); - - $image = $this->imageRepo->updateImageDetails($image, $request->all()); - return response()->json($image); - } - - /** - * Show the usage of an image on pages. - * @param \BookStack\Entities\Repos\EntityRepo $entityRepo - * @param $id - * @return \Illuminate\Http\JsonResponse - */ - public function usage(EntityRepo $entityRepo, $id) - { - $image = $this->imageRepo->getById($id); - $pageSearch = $entityRepo->searchForImage($image->url); - return response()->json($pageSearch); - } - - /** - * Deletes an image and all thumbnail/image files - * @param int $id - * @return \Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function destroy($id) - { - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('image-delete', $image); - - $this->imageRepo->destroyImage($image); - return response()->json(trans('components.images_deleted')); - } -} diff --git a/app/Http/Controllers/Images/CoverImageController.php b/app/Http/Controllers/Images/CoverImageController.php deleted file mode 100644 index fdec56be9..000000000 --- a/app/Http/Controllers/Images/CoverImageController.php +++ /dev/null @@ -1,99 +0,0 @@ -imageRepo = $imageRepo; - $this->entityRepo = $entityRepo; - - parent::__construct(); - } - - /** - * Get a list of cover images, in a list. - * Can be paged and filtered by entity. - * @param Request $request - * @param string $entity - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request, $entity) - { - if (!$this->isValidEntityTypeForCover($entity)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - - $type = 'cover_' . $entity; - $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, null, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new cover image in the system. - * @param Request $request - * @param string $entity - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request, $entity) - { - $this->checkPermission('image-create-all'); - $this->validate($request, [ - 'file' => $this->imageRepo->getImageValidationRules(), - 'uploaded_to' => 'required|integer' - ]); - - if (!$this->isValidEntityTypeForCover($entity)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $uploadedTo = $request->get('uploaded_to', 0); - $entityInstance = $this->entityRepo->getById($entity, $uploadedTo); - $this->checkOwnablePermission($entity . '-update', $entityInstance); - - try { - $type = 'cover_' . $entity; - $imageUpload = $request->file('file'); - $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - - /** - * Check if the given entity type is valid entity to have cover images. - * @param string $entityType - * @return bool - */ - protected function isValidEntityTypeForCover(string $entityType) - { - return ($entityType === 'book' || $entityType === 'bookshelf'); - } - -} diff --git a/app/Http/Controllers/Images/DrawioImageController.php b/app/Http/Controllers/Images/DrawioImageController.php index eb0e32827..2deb64d96 100644 --- a/app/Http/Controllers/Images/DrawioImageController.php +++ b/app/Http/Controllers/Images/DrawioImageController.php @@ -64,4 +64,26 @@ class DrawioImageController extends Controller return response()->json($image); } + /** + * Get the content of an image based64 encoded. + * @param $id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getAsBase64($id) + { + $image = $this->imageRepo->getById($id); + $page = $image->getPage(); + if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) { + return $this->jsonError("Image data could not be found"); + } + + $imageData = $this->imageRepo->getImageData($image); + if ($imageData === null) { + return $this->jsonError("Image data could not be found"); + } + return response()->json([ + 'content' => base64_encode($imageData) + ]); + } + } diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php new file mode 100644 index 000000000..a76fb5e53 --- /dev/null +++ b/app/Http/Controllers/Images/ImageController.php @@ -0,0 +1,115 @@ +image = $image; + $this->file = $file; + $this->imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Provide an image file from storage. + * @param string $path + * @return mixed + */ + public function showImage(string $path) + { + $path = storage_path('uploads/images/' . $path); + if (!file_exists($path)) { + abort(404); + } + + return response()->file($path); + } + + + /** + * Update image details + * @param integer $id + * @param Request $request + * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception + */ + public function update($id, Request $request) + { + $this->validate($request, [ + 'name' => 'required|min:2|string' + ]); + + $image = $this->imageRepo->getById($id); + $this->checkImagePermission($image); + $this->checkOwnablePermission('image-update', $image); + + $image = $this->imageRepo->updateImageDetails($image, $request->all()); + return response()->json($image); + } + + /** + * Show the usage of an image on pages. + * @param \BookStack\Entities\Repos\EntityRepo $entityRepo + * @param $id + * @return \Illuminate\Http\JsonResponse + */ + public function usage(EntityRepo $entityRepo, $id) + { + $image = $this->imageRepo->getById($id); + $this->checkImagePermission($image); + $pageSearch = $entityRepo->searchForImage($image->url); + return response()->json($pageSearch); + } + + /** + * Deletes an image and all thumbnail/image files + * @param int $id + * @return \Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function destroy($id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('image-delete', $image); + $this->checkImagePermission($image); + + $this->imageRepo->destroyImage($image); + return response()->json(trans('components.images_deleted')); + } + + /** + * Check related page permission and ensure type is drawio or gallery. + * @param Image $image + */ + protected function checkImagePermission(Image $image) + { + if ($image->type !== 'drawio' || $image->type !== 'gallery') { + $this->showPermissionError(); + } + + $relatedPage = $image->getPage(); + if ($relatedPage) { + $this->checkOwnablePermission('page-view', $relatedPage); + } + } +} diff --git a/app/Http/Controllers/Images/SystemImageController.php b/app/Http/Controllers/Images/SystemImageController.php deleted file mode 100644 index ad3de9625..000000000 --- a/app/Http/Controllers/Images/SystemImageController.php +++ /dev/null @@ -1,67 +0,0 @@ -imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Get a list of system images, in a list. - * @param Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request) - { - $this->checkPermission('settings-manage'); - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - - $imgData = $this->imageRepo->getPaginatedByType('system', $page, 24, null, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new system image. - * @param Request $request - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request) - { - $this->checkPermission('image-create-all'); - $this->checkPermission('settings-manage'); - - $this->validate($request, [ - 'file' => $this->imageRepo->getImageValidationRules() - ]); - - try { - $imageUpload = $request->file('file'); - $image = $this->imageRepo->saveNew($imageUpload, 'system', 0); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - -} diff --git a/app/Http/Controllers/Images/UserImageController.php b/app/Http/Controllers/Images/UserImageController.php deleted file mode 100644 index 312ccf47e..000000000 --- a/app/Http/Controllers/Images/UserImageController.php +++ /dev/null @@ -1,73 +0,0 @@ -imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Get a list of user profile images, in a list. - * @param Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request) - { - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - $userId = $request->get('uploaded_to', null); - - $this->checkPermissionOrCurrentUser('users-manage', $userId); - - $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $userId, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new user profile image in the system. - * @param Request $request - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request) - { - $this->checkPermission('image-create-all'); - - $this->validate($request, [ - 'uploaded_to' => 'required|integer', - 'file' => $this->imageRepo->getImageValidationRules() - ]); - - $userId = $request->get('uploaded_to', null); - $this->checkPermissionOrCurrentUser('users-manage', $userId); - - try { - $imageUpload = $request->file('file'); - $uploadedTo = $request->get('uploaded_to', 0); - $image = $this->imageRepo->saveNew($imageUpload, 'user', $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - -} diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 159e19a2b..650833c7f 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,6 +1,7 @@ imageRepo = $imageRepo; + parent::__construct(); + } + + /** * Display a listing of the settings. * @return Response @@ -35,6 +49,9 @@ class SettingController extends Controller { $this->preventAccessForDemoUsers(); $this->checkPermission('settings-manage'); + $this->validate($request, [ + 'app_logo' => $this->imageRepo->getImageValidationRules(), + ]); // Cycles through posted settings and update them foreach ($request->all() as $name => $value) { @@ -42,7 +59,21 @@ class SettingController extends Controller continue; } $key = str_replace('setting-', '', trim($name)); - Setting::put($key, $value); + setting()->put($key, $value); + } + + // Update logo image if set + if ($request->has('app_logo')) { + $logoFile = $request->file('app_logo'); + $this->imageRepo->destroyByType('system'); + $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86); + setting()->put('app-logo', $image->url); + } + + // Clear logo image if requested + if ($request->get('app_logo_reset', null)) { + $this->imageRepo->destroyByType('system'); + setting()->remove('app-logo'); } session()->flash('success', trans('settings.settings_save_success')); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a93e8d9c9..c7a7c5646 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService; use BookStack\Auth\User; use BookStack\Auth\UserRepo; use BookStack\Exceptions\UserUpdateException; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -12,16 +13,19 @@ class UserController extends Controller protected $user; protected $userRepo; + protected $imageRepo; /** * UserController constructor. - * @param User $user + * @param User $user * @param UserRepo $userRepo + * @param ImageRepo $imageRepo */ - public function __construct(User $user, UserRepo $userRepo) + public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo) { $this->user = $user; $this->userRepo = $userRepo; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -94,6 +98,7 @@ class UserController extends Controller $this->userRepo->setUserRoles($user, $roles); } + // TODO - Check this uses new profile assignment $this->userRepo->downloadAndAssignUserAvatar($user); return redirect('/settings/users'); @@ -121,10 +126,11 @@ class UserController extends Controller /** * Update the specified user in storage. - * @param Request $request - * @param int $id + * @param Request $request + * @param int $id * @return Response * @throws UserUpdateException + * @throws \BookStack\Exceptions\ImageUploadException */ public function update(Request $request, $id) { @@ -136,7 +142,8 @@ class UserController extends Controller 'email' => 'min:2|email|unique:users,email,' . $id, 'password' => 'min:5|required_with:password_confirm', 'password-confirm' => 'same:password|required_with:password', - 'setting' => 'array' + 'setting' => 'array', + 'profile_image' => $this->imageRepo->getImageValidationRules(), ]); $user = $this->userRepo->getById($id); @@ -166,10 +173,23 @@ class UserController extends Controller } } + // Save profile image if in request + if ($request->has('profile_image')) { + $imageUpload = $request->file('profile_image'); + $this->imageRepo->destroyImage($user->avatar); + $image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id); + $user->image_id = $image->id; + } + + // Delete the profile image if set to + if ($request->has('profile_image_reset')) { + $this->imageRepo->destroyImage($user->avatar); + } + $user->save(); session()->flash('success', trans('settings.users_edit_success')); - $redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id; + $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id); return redirect($redirectUrl); } diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index df6d9fb0d..6fa5db2a5 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -1,5 +1,6 @@ belongsTo(Page::class, 'uploaded_to')->first(); + } } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 235889eee..dbf652ddf 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -2,7 +2,6 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Page; -use BookStack\Http\Requests\Request; use Illuminate\Database\Eloquent\Builder; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -38,7 +37,7 @@ class ImageRepo /** * Get an image with the given id. * @param $id - * @return mixed + * @return Image */ public function getById($id) { @@ -100,16 +99,8 @@ class ImageRepo $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%'); } - // Filter by page access if gallery - if ($type === 'gallery') { - $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); - } - - // Filter by entity if cover - if (strpos($type, 'cover_') === 0) { - $entityType = explode('_', $type)[1]; - $imageQuery = $this->restrictionService->filterRelatedEntity($entityType, $imageQuery, 'images', 'uploaded_to'); - } + // Filter by page access + $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); if ($whereClause !== null) { $imageQuery = $imageQuery->where($whereClause); @@ -157,15 +148,17 @@ class ImageRepo /** * Save a new image into storage and return the new image. * @param UploadedFile $uploadFile - * @param string $type + * @param string $type * @param int $uploadedTo + * @param int|null $resizeWidth + * @param int|null $resizeHeight + * @param bool $keepRatio * @return Image * @throws \BookStack\Exceptions\ImageUploadException - * @throws \Exception */ - public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0) + public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true) { - $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo); + $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio); $this->loadThumbs($image); return $image; } @@ -208,12 +201,27 @@ class ImageRepo * @return bool * @throws \Exception */ - public function destroyImage(Image $image) + public function destroyImage(Image $image = null) { - $this->imageService->destroy($image); + if ($image) { + $this->imageService->destroy($image); + } return true; } + /** + * Destroy all images of a certain type. + * @param string $imageType + * @throws \Exception + */ + public function destroyByType(string $imageType) + { + $images = $this->image->where('type', '=', $imageType)->get(); + foreach ($images as $image) { + $this->destroyImage($image); + } + } + /** * Load thumbnails onto an image object. @@ -241,7 +249,7 @@ class ImageRepo * @throws \BookStack\Exceptions\ImageUploadException * @throws \Exception */ - public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) + protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { try { return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); @@ -264,18 +272,6 @@ class ImageRepo } } - /** - * Check if the provided image type is valid. - * @param $type - * @return bool - */ - public function isValidType($type) - { - // TODO - To delete? - $validTypes = ['gallery', 'cover', 'system', 'user']; - return in_array($type, $validTypes); - } - /** * Get the validation rules for image files. * @return string diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 1dd8b713d..63c3b3172 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\ImageManager; +use phpDocumentor\Reflection\Types\Integer; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageService extends UploadService @@ -57,15 +58,30 @@ class ImageService extends UploadService /** * Saves a new image from an upload. * @param UploadedFile $uploadedFile - * @param string $type + * @param string $type * @param int $uploadedTo + * @param int|null $resizeWidth + * @param int|null $resizeHeight + * @param bool $keepRatio * @return mixed * @throws ImageUploadException */ - public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0) + public function saveNewFromUpload( + UploadedFile $uploadedFile, + string $type, + int $uploadedTo = 0, + int $resizeWidth = null, + int $resizeHeight = null, + bool $keepRatio = true + ) { $imageName = $uploadedFile->getClientOriginalName(); $imageData = file_get_contents($uploadedFile->getRealPath()); + + if ($resizeWidth !== null || $resizeHeight !== null) { + $imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio); + } + return $this->saveNew($imageName, $imageData, $type, $uploadedTo); } @@ -122,7 +138,7 @@ class ImageService extends UploadService $secureUploads = setting('app-secure-images'); $imageName = str_replace(' ', '-', $imageName); - $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; + $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/'; while ($storage->exists($imagePath . $imageName)) { $imageName = str_random(3) . $imageName; @@ -201,8 +217,28 @@ class ImageService extends UploadService return $this->getPublicUrl($thumbFilePath); } + $thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio); + + $storage->put($thumbFilePath, $thumbData); + $storage->setVisibility($thumbFilePath, 'public'); + $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); + + return $this->getPublicUrl($thumbFilePath); + } + + /** + * Resize image data. + * @param string $imageData + * @param int $width + * @param int $height + * @param bool $keepRatio + * @return string + * @throws ImageUploadException + */ + protected function resizeImage(string $imageData, $width = 220, $height = null, $keepRatio = true) + { try { - $thumb = $this->imageTool->make($storage->get($imagePath)); + $thumb = $this->imageTool->make($imageData); } catch (Exception $e) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { throw new ImageUploadException(trans('errors.cannot_create_thumbs')); @@ -211,20 +247,14 @@ class ImageService extends UploadService } if ($keepRatio) { - $thumb->resize($width, null, function ($constraint) { + $thumb->resize($width, $height, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); } else { $thumb->fit($width, $height); } - - $thumbData = (string)$thumb->encode(); - $storage->put($thumbFilePath, $thumbData); - $storage->setVisibility($thumbFilePath, 'public'); - $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); - - return $this->getPublicUrl($thumbFilePath); + return (string)$thumb->encode(); } /** diff --git a/resources/assets/js/components/image-picker.js b/resources/assets/js/components/image-picker.js index 7cbed4509..7455fa622 100644 --- a/resources/assets/js/components/image-picker.js +++ b/resources/assets/js/components/image-picker.js @@ -4,54 +4,50 @@ class ImagePicker { constructor(elem) { this.elem = elem; this.imageElem = elem.querySelector('img'); - this.input = elem.querySelector('input'); + this.imageInput = elem.querySelector('input[type=file]'); + this.resetInput = elem.querySelector('input[data-reset-input]'); + this.removeInput = elem.querySelector('input[data-remove-input]'); - this.isUsingIds = elem.getAttribute('data-current-id') !== ''; - this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width'); - this.isResizeCropping = elem.getAttribute('data-resize-crop') !== ''; + this.defaultImage = elem.getAttribute('data-default-image'); - let selectButton = elem.querySelector('button[data-action="show-image-manager"]'); - selectButton.addEventListener('click', this.selectImage.bind(this)); - - let resetButton = elem.querySelector('button[data-action="reset-image"]'); + const resetButton = elem.querySelector('button[data-action="reset-image"]'); resetButton.addEventListener('click', this.reset.bind(this)); - let removeButton = elem.querySelector('button[data-action="remove-image"]'); + const removeButton = elem.querySelector('button[data-action="remove-image"]'); if (removeButton) { removeButton.addEventListener('click', this.removeImage.bind(this)); } + + this.imageInput.addEventListener('change', this.fileInputChange.bind(this)); } - selectImage() { - window.ImageManager.show(image => { - if (!this.isResizing) { - this.setImage(image); - return; - } + fileInputChange() { + this.resetInput.setAttribute('disabled', 'disabled'); + if (this.removeInput) { + this.removeInput.setAttribute('disabled', 'disabled'); + } - let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false'); - - window.$http.get(window.baseUrl(requestString)).then(resp => { - image.url = resp.data.url; - this.setImage(image); - }); - }); + for (let file of this.imageInput.files) { + this.imageElem.src = window.URL.createObjectURL(file); + } + this.imageElem.classList.remove('none'); } reset() { - this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')}); - } - - setImage(image) { - this.imageElem.src = image.url; - this.input.value = this.isUsingIds ? image.id : image.url; + this.imageInput.value = ''; + this.imageElem.src = this.defaultImage; + this.resetInput.removeAttribute('disabled'); + if (this.removeInput) { + this.removeInput.setAttribute('disabled', 'disabled'); + } this.imageElem.classList.remove('none'); } removeImage() { - this.imageElem.src = this.elem.getAttribute('data-default-image'); + this.imageInput.value = ''; this.imageElem.classList.add('none'); - this.input.value = 'none'; + this.removeInput.removeAttribute('disabled'); + this.resetInput.setAttribute('disabled', 'disabled'); } } diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index 55cf67813..b099a7ca9 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -394,9 +394,7 @@ class MarkdownEditor { const drawingId = imgContainer.getAttribute('drawio-diagram'); DrawIO.show(() => { - return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - return `data:image/png;base64,${resp.data.content}`; - }); + return DrawIO.load(drawingId); }, (pngData) => { let data = { diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index 3f60ff031..b894c3fa6 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -299,9 +299,7 @@ function drawIoPlugin() { } let drawingId = currentNode.getAttribute('drawio-diagram'); - return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - return `data:image/png;base64,${resp.data.content}`; - }); + return DrawIO.load(drawingId); } window.tinymce.PluginManager.add('drawio', function(editor, url) { diff --git a/resources/assets/js/services/drawio.js b/resources/assets/js/services/drawio.js index 6acf091f0..a570737d1 100644 --- a/resources/assets/js/services/drawio.js +++ b/resources/assets/js/services/drawio.js @@ -75,4 +75,14 @@ async function upload(imageData, pageUploadedToId) { return resp.data; } -export default {show, close, upload}; \ No newline at end of file +/** + * Load an existing image, by fetching it as Base64 from the system. + * @param drawingId + * @returns {Promise} + */ +async function load(drawingId) { + const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`)); + return `data:image/png;base64,${resp.data.content}`; +} + +export default {show, close, upload, load}; \ No newline at end of file diff --git a/resources/assets/sass/_layout.scss b/resources/assets/sass/_layout.scss index 137048935..9bb4e1c70 100644 --- a/resources/assets/sass/_layout.scss +++ b/resources/assets/sass/_layout.scss @@ -140,6 +140,10 @@ body.flexbox { display: inline-block; } +.hidden { + display: none; +} + .float { float: left; &.right { diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 9baeb9f30..210980ac2 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -71,6 +71,7 @@ return [ 'timezone' => 'The :attribute must be a valid zone.', 'unique' => 'The :attribute has already been taken.', 'url' => 'The :attribute format is invalid.', + 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', // Custom validation lines 'custom' => [ diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php index 882ce556a..40b781441 100644 --- a/resources/views/books/create.blade.php +++ b/resources/views/books/create.blade.php @@ -33,5 +33,4 @@ - @include('components.image-manager', ['imageType' => 'cover']) @stop \ No newline at end of file diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index a02029a10..2e51ed6e9 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -16,12 +16,10 @@

{{ trans('entities.books_edit') }}

-
+ @include('books.form', ['model' => $book])
- - @include('components.image-manager', ['imageType' => 'cover', 'uploaded_to']) @stop \ No newline at end of file diff --git a/resources/views/books/form.blade.php b/resources/views/books/form.blade.php index ebbc279fd..4edec240a 100644 --- a/resources/views/books/form.blade.php +++ b/resources/views/books/form.blade.php @@ -18,13 +18,9 @@

{{ trans('common.cover_image_description') }}

@include('components.image-picker', [ - 'resizeHeight' => '512', - 'resizeWidth' => '512', - 'showRemove' => false, 'defaultImage' => baseUrl('/book_default_cover.png'), - 'currentImage' => isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') , - 'currentId' => isset($model) && $model->image_id ? $model->image_id : 0, - 'name' => 'image_id', + 'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : baseUrl('/book_default_cover.png') , + 'name' => 'image', 'imageClass' => 'cover' ]) diff --git a/resources/views/components/image-picker.blade.php b/resources/views/components/image-picker.blade.php index 10df50293..73885aeb4 100644 --- a/resources/views/components/image-picker.blade.php +++ b/resources/views/components/image-picker.blade.php @@ -1,22 +1,32 @@ -
+
{{ trans('components.image_preview') }}
- + + + + + @if(isset($removeName)) + + @endif +
- @if ($showRemove) + @if(isset($removeName)) | @endif
- -{{-- TODO - Revamp to be custom file upload button, instead of being linked to image manager--}} -{{-- TODO - Remove image manager use where this is used and clean image manager for drawing/gallery use.--}} + @if($errors->has($name)) +
{{ $errors->first($name) }}
+ @endif +
\ No newline at end of file diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index 582bb078e..2dabe9dec 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -79,7 +79,7 @@

{{ trans('settings.app_customization') }}

-
+ {!! csrf_field() !!}
@@ -119,14 +119,12 @@
@include('components.image-picker', [ - 'resizeHeight' => '43', - 'resizeWidth' => '200', - 'showRemove' => true, + 'removeName' => 'setting-app-logo', + 'removeValue' => 'none', 'defaultImage' => baseUrl('/logo.png'), 'currentImage' => setting('app-logo'), - 'name' => 'setting-app-logo', + 'name' => 'app_logo', 'imageClass' => 'logo-image', - 'currentId' => false ])
diff --git a/resources/views/shelves/create.blade.php b/resources/views/shelves/create.blade.php index 72bf904fe..706e15d07 100644 --- a/resources/views/shelves/create.blade.php +++ b/resources/views/shelves/create.blade.php @@ -26,6 +26,4 @@
- @include('components.image-manager', ['imageType' => 'cover']) - @stop \ No newline at end of file diff --git a/resources/views/shelves/edit.blade.php b/resources/views/shelves/edit.blade.php index fa8a3d63b..8c2cd4f45 100644 --- a/resources/views/shelves/edit.blade.php +++ b/resources/views/shelves/edit.blade.php @@ -16,7 +16,7 @@

{{ trans('entities.shelves_edit') }}

- + @include('shelves.form', ['model' => $shelf]) diff --git a/resources/views/shelves/form.blade.php b/resources/views/shelves/form.blade.php index 8a270a894..4191f421d 100644 --- a/resources/views/shelves/form.blade.php +++ b/resources/views/shelves/form.blade.php @@ -53,13 +53,9 @@

{{ trans('common.cover_image_description') }}

@include('components.image-picker', [ - 'resizeHeight' => '512', - 'resizeWidth' => '512', - 'showRemove' => false, 'defaultImage' => baseUrl('/book_default_cover.png'), - 'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , - 'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0, - 'name' => 'image_id', + 'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , + 'name' => 'image', 'imageClass' => 'cover' ])
diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 7c8175d9a..377500193 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -9,7 +9,7 @@

{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}

-
id}") }}" method="post"> + id}") }}" method="post" enctype="multipart/form-data"> {!! csrf_field() !!} @@ -29,7 +29,7 @@ 'defaultImage' => baseUrl('/user_avatar.png'), 'currentImage' => $user->getAvatar(80), 'currentId' => $user->image_id, - 'name' => 'image_id', + 'name' => 'profile_image', 'imageClass' => 'avatar large' ])
@@ -87,5 +87,4 @@ @endif - @include('components.image-manager', ['imageType' => 'user', 'uploaded_to' => $user->id]) @stop diff --git a/routes/web.php b/routes/web.php index 975ab5b17..25d7ab692 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,7 +6,8 @@ Route::get('/robots.txt', 'HomeController@getRobots'); // Authenticated routes... Route::group(['middleware' => 'auth'], function () { - Route::get('/uploads/images/{path}', 'ImageController@showImage') + // Secure images routing + Route::get('/uploads/images/{path}', 'Images\ImageController@showImage') ->where('path', '.*$'); Route::group(['prefix' => 'pages'], function() { @@ -105,27 +106,19 @@ Route::group(['middleware' => 'auth'], function () { // Image routes Route::group(['prefix' => 'images'], function () { - // TODO - Check auth on these - // TODO - Maybe check types for only gallery or drawing - // Standard get, update and deletion for all types - Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); - Route::get('/base64/{id}', 'ImageController@getBase64Image'); - Route::get('/usage/{id}', 'ImageController@usage'); - // Gallery Route::get('/gallery', 'Images\GalleryImageController@list'); Route::post('/gallery', 'Images\GalleryImageController@create'); // Drawio Route::get('/drawio', 'Images\DrawioImageController@list'); + Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64'); Route::post('/drawio', 'Images\DrawioImageController@create'); - - // TODO - Check auth on these - // TODO - Maybe check types for only gallery or drawing - // Or add to gallery/drawio controllers - Route::put('/{id}', 'ImageController@update'); - Route::delete('/{id}', 'ImageController@destroy'); + // Shared gallery & draw.io endpoint + Route::get('/usage/{id}', 'Images\ImageController@usage'); + Route::put('/{id}', 'Images\ImageController@update'); + Route::delete('/{id}', 'Images\ImageController@destroy'); }); // Attachments routes