diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index 7e710edaf..a5ab4ea9a 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -732,18 +732,21 @@ class PermissionService } /** - * Filters pages that are a direct relation to another item. + * Add conditions to a query to filter the selection to related entities + * where permissions are granted. + * @param $entityType * @param $query * @param $tableName * @param $entityIdColumn * @return mixed */ - public function filterRelatedPages($query, $tableName, $entityIdColumn) + public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn) { $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - $pageMorphClass = $this->entityProvider->page->getMorphClass(); + $pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass(); + $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { $query->where(function ($query) use (&$tableDetails, $pageMorphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) { @@ -761,7 +764,9 @@ class PermissionService }); })->orWhere($tableDetails['entityIdColumn'], '=', 0); }); + $this->clean(); + return $q; } 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/Auth/UserRepo.php b/app/Auth/UserRepo.php index 1a73d0072..94ebb27ab 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -198,7 +198,7 @@ class UserRepo $user->delete(); // Delete user profile images - $profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get(); + $profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get(); foreach ($profileImages as $image) { Images::destroy($image); } diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 88ca1bca6..1cff46da1 100644 --- a/app/Entities/Repos/EntityRepo.php +++ b/app/Entities/Repos/EntityRepo.php @@ -762,7 +762,7 @@ class EntityRepo */ public function searchForImage($imageString) { - $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(); + $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']); foreach ($pages as $page) { $page->url = $page->getUrl(); $page->html = ''; 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 b86bc2e38..dba2503ef 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo; use BookStack\Entities\Bookshelf; use BookStack\Entities\EntityContextManager; use BookStack\Entities\Repos\EntityRepo; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; use Views; @@ -15,18 +16,21 @@ class BookshelfController extends Controller protected $entityRepo; protected $userRepo; protected $entityContextManager; + protected $imageRepo; /** * BookController constructor. * @param EntityRepo $entityRepo * @param UserRepo $userRepo * @param EntityContextManager $entityContextManager + * @param ImageRepo $imageRepo */ - public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager) + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo) { $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; $this->entityContextManager = $entityContextManager; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -82,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) { @@ -91,13 +96,14 @@ class BookshelfController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255', 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); - $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); - $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); - Activity::add($bookshelf, 'bookshelf_create'); + $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); + $this->shelfUpdateActions($shelf, $request); - return redirect($bookshelf->getUrl()); + Activity::add($shelf, 'bookshelf_create'); + return redirect($shelf->getUrl()); } @@ -109,19 +115,19 @@ class BookshelfController extends Controller */ public function show(string $slug) { - /** @var Bookshelf $bookshelf */ - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('book-view', $bookshelf); + /** @var Bookshelf $shelf */ + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('book-view', $shelf); - $books = $this->entityRepo->getBookshelfChildren($bookshelf); - Views::add($bookshelf); - $this->entityContextManager->setShelfContext($bookshelf->id); + $books = $this->entityRepo->getBookshelfChildren($shelf); + Views::add($shelf); + $this->entityContextManager->setShelfContext($shelf->id); - $this->setPageTitle($bookshelf->getShortName()); + $this->setPageTitle($shelf->getShortName()); return view('shelves.show', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'books' => $books, - 'activity' => Activity::entityActivity($bookshelf, 20, 1) + 'activity' => Activity::entityActivity($shelf, 20, 1) ]); } @@ -133,19 +139,19 @@ class BookshelfController extends Controller */ public function edit(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-update', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $shelf); - $shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf); + $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf); $shelfBookIds = $shelfBooks->pluck('id'); $books = $this->entityRepo->getAll('book', false, 'update'); $books = $books->filter(function ($book) use ($shelfBookIds) { return !$shelfBookIds->contains($book->id); }); - $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()])); + $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()])); return view('shelves.edit', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'books' => $books, 'shelfBooks' => $shelfBooks, ]); @@ -154,10 +160,11 @@ class BookshelfController extends Controller /** * Update the specified bookshelf in storage. - * @param Request $request + * @param Request $request * @param string $slug * @return Response * @throws \BookStack\Exceptions\NotFoundException + * @throws \BookStack\Exceptions\ImageUploadException */ public function update(Request $request, string $slug) { @@ -166,10 +173,12 @@ class BookshelfController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255', 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all()); - $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + $this->shelfUpdateActions($shelf, $request); + Activity::add($shelf, 'bookshelf_update'); return redirect($shelf->getUrl()); @@ -184,11 +193,11 @@ class BookshelfController extends Controller */ public function showDelete(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $shelf); - $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()])); - return view('shelves.delete', ['shelf' => $bookshelf]); + $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()])); + return view('shelves.delete', ['shelf' => $shelf]); } /** @@ -200,10 +209,15 @@ class BookshelfController extends Controller */ public function destroy(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-delete', $bookshelf); - Activity::addMessage('bookshelf_delete', 0, $bookshelf->name); - $this->entityRepo->destroyBookshelf($bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $shelf); + Activity::addMessage('bookshelf_delete', 0, $shelf->name); + + if ($shelf->cover) { + $this->imageRepo->destroyImage($shelf->cover); + } + $this->entityRepo->destroyBookshelf($shelf); + return redirect('/shelves'); } @@ -215,12 +229,12 @@ class BookshelfController extends Controller */ public function showPermissions(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); $roles = $this->userRepo->getRestrictableRoles(); return view('shelves.permissions', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'roles' => $roles ]); } @@ -235,12 +249,12 @@ class BookshelfController extends Controller */ public function permissions(string $slug, Request $request) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); - $this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf); session()->flash('success', trans('entities.shelves_permissions_updated')); - return redirect($bookshelf->getUrl()); + return redirect($shelf->getUrl()); } /** @@ -251,11 +265,37 @@ class BookshelfController extends Controller */ public function copyPermissions(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); - $updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf); + $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf); session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount])); - return redirect($bookshelf->getUrl()); + return redirect($shelf->getUrl()); + } + + /** + * Common actions to run on bookshelf update. + * @param Bookshelf $shelf + * @param Request $request + * @throws \BookStack\Exceptions\ImageUploadException + */ + protected function shelfUpdateActions(Bookshelf $shelf, Request $request) + { + // Update the books that the shelf references + $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + + // Update the cover image if in request + 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 4d6f759b3..000000000 --- a/app/Http/Controllers/ImageController.php +++ /dev/null @@ -1,246 +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 string $type - * @param int $page - * @return \Illuminate\Http\JsonResponse - */ - public function getAllByType($type, $page = 0) - { - $imgData = $this->imageRepo->getPaginatedByType($type, $page); - 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); - } - - /** - * Get all images for a user. - * @param int $page - * @return \Illuminate\Http\JsonResponse - */ - public function getAllForUserType($page = 0) - { - $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id); - return response()->json($imgData); - } - - /** - * Get gallery images with a specific filter such as book or page - * @param $filter - * @param int $page - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function getGalleryFiltered(Request $request, $filter, $page = 0) - { - $this->validate($request, [ - 'page_id' => 'required|integer' - ]); - - $validFilters = collect(['page', 'book']); - if (!$validFilters->contains($filter)) { - return response('Invalid filter', 500); - } - - $pageId = $request->get('page_id'); - $imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24); - - return response()->json($imgData); - } - - /** - * 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); - $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - - /** - * Upload a drawing to the system. - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function uploadDrawing(Request $request) - { - $this->validate($request, [ - 'image' => 'required|string', - 'uploaded_to' => 'required|integer' - ]); - $this->checkPermission('image-create-all'); - $imageBase64Data = $request->get('image'); - - try { - $uploadedTo = $request->get('uploaded_to', 0); - $image = $this->imageRepo->saveDrawing($imageBase64Data, $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 $imageId - * @param Request $request - * @return \Illuminate\Http\JsonResponse - * @throws ImageUploadException - * @throws \Exception - */ - public function update($imageId, Request $request) - { - $this->validate($request, [ - 'name' => 'required|min:2|string' - ]); - $image = $this->imageRepo->getById($imageId); - $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/DrawioImageController.php b/app/Http/Controllers/Images/DrawioImageController.php new file mode 100644 index 000000000..2deb64d96 --- /dev/null +++ b/app/Http/Controllers/Images/DrawioImageController.php @@ -0,0 +1,89 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of gallery images, in a list. + * Can be paged and filtered by entity. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + $uploadedToFilter = $request->get('uploaded_to', null); + $parentTypeFilter = $request->get('filter_type', null); + + $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new gallery image in the system. + * @param Request $request + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request) + { + $this->validate($request, [ + 'image' => 'required|string', + 'uploaded_to' => 'required|integer' + ]); + + $this->checkPermission('image-create-all'); + $imageBase64Data = $request->get('image'); + + try { + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveDrawing($imageBase64Data, $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 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/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php new file mode 100644 index 000000000..35087463b --- /dev/null +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -0,0 +1,65 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of gallery images, in a list. + * Can be paged and filtered by entity. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + $uploadedToFilter = $request->get('uploaded_to', null); + $parentTypeFilter = $request->get('filter_type', null); + + $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new gallery 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, [ + 'file' => $this->imageRepo->getImageValidationRules() + ]); + + try { + $imageUpload = $request->file('file'); + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + +} diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php new file mode 100644 index 000000000..024003f87 --- /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/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 1bb5d46cd..1367f631b 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(); } @@ -107,9 +111,7 @@ class UserController extends Controller */ public function edit($id, SocialAuthService $socialAuthService) { - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->user->findOrFail($id); @@ -123,24 +125,24 @@ 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) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $this->validate($request, [ 'name' => 'min:2', '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); @@ -170,10 +172,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); } @@ -184,9 +199,7 @@ class UserController extends Controller */ public function delete($id) { - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->userRepo->getById($id); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); @@ -202,9 +215,7 @@ class UserController extends Controller public function destroy($id) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->userRepo->getById($id); 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 0ef8cad48..16d1bb3d1 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -2,6 +2,7 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Page; +use Illuminate\Database\Eloquent\Builder; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageRepo @@ -19,7 +20,12 @@ class ImageRepo * @param \BookStack\Auth\Permissions\PermissionService $permissionService * @param \BookStack\Entities\Page $page */ - public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page) + public function __construct( + Image $image, + ImageService $imageService, + PermissionService $permissionService, + Page $page + ) { $this->image = $image; $this->imageService = $imageService; @@ -31,7 +37,7 @@ class ImageRepo /** * Get an image with the given id. * @param $id - * @return mixed + * @return Image */ public function getById($id) { @@ -44,95 +50,115 @@ class ImageRepo * @param $query * @param int $page * @param int $pageSize + * @param bool $filterOnPage * @return array */ - private function returnPaginated($query, $page = 0, $pageSize = 24) + private function returnPaginated($query, $page = 1, $pageSize = 24) { - $images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to'); - $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); + $images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get(); $hasMore = count($images) > $pageSize; - $returnImages = $images->take(24); + $returnImages = $images->take($pageSize); $returnImages->each(function ($image) { $this->loadThumbs($image); }); return [ 'images' => $returnImages, - 'hasMore' => $hasMore + 'has_more' => $hasMore ]; } /** - * Gets a load images paginated, filtered by image type. + * Fetch a list of images in a paginated format, filtered by image type. + * Can be filtered by uploaded to and also by name. * @param string $type * @param int $page * @param int $pageSize - * @param bool|int $userFilter + * @param int $uploadedTo + * @param string|null $search + * @param callable|null $whereClause * @return array */ - public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false) + public function getPaginatedByType( + string $type, + int $page = 0, + int $pageSize = 24, + int $uploadedTo = null, + string $search = null, + callable $whereClause = null + ) { - $images = $this->image->where('type', '=', strtolower($type)); + $imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type)); - if ($userFilter !== false) { - $images = $images->where('created_by', '=', $userFilter); + if ($uploadedTo !== null) { + $imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo); } - return $this->returnPaginated($images, $page, $pageSize); + if ($search !== null) { + $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%'); + } + + // Filter by page access + $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); + + if ($whereClause !== null) { + $imageQuery = $imageQuery->where($whereClause); + } + + return $this->returnPaginated($imageQuery, $page, $pageSize); } /** - * Search for images by query, of a particular type. + * Get paginated gallery images within a specific page or book. * @param string $type + * @param string $filterType * @param int $page * @param int $pageSize - * @param string $searchTerm + * @param int|null $uploadedTo + * @param string|null $search * @return array */ - public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24) + public function getEntityFiltered( + string $type, + string $filterType = null, + int $page = 0, + int $pageSize = 24, + int $uploadedTo = null, + string $search = null + ) { - $images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%'); - return $this->returnPaginated($images, $page, $pageSize); - } + $contextPage = $this->page->findOrFail($uploadedTo); + $parentFilter = null; - /** - * Get gallery images with a particular filter criteria such as - * being within the current book or page. - * @param $filter - * @param $pageId - * @param int $pageNum - * @param int $pageSize - * @return array - */ - public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24) - { - $images = $this->image->where('type', '=', 'gallery'); - - $page = $this->page->findOrFail($pageId); - - if ($filter === 'page') { - $images = $images->where('uploaded_to', '=', $page->id); - } elseif ($filter === 'book') { - $validPageIds = $page->book->pages->pluck('id')->toArray(); - $images = $images->whereIn('uploaded_to', $validPageIds); + if ($filterType === 'book' || $filterType === 'page') { + $parentFilter = function(Builder $query) use ($filterType, $contextPage) { + if ($filterType === 'page') { + $query->where('uploaded_to', '=', $contextPage->id); + } elseif ($filterType === 'book') { + $validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray(); + $query->whereIn('uploaded_to', $validPageIds); + } + }; } - return $this->returnPaginated($images, $pageNum, $pageSize); + return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter); } /** * 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; } @@ -175,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. @@ -191,8 +232,8 @@ class ImageRepo protected function loadThumbs(Image $image) { $image->thumbs = [ - 'gallery' => $this->getThumbnail($image, 150, 150), - 'display' => $this->getThumbnail($image, 840, 0, true) + 'gallery' => $this->getThumbnail($image, 150, 150, false), + 'display' => $this->getThumbnail($image, 840, null, true) ]; } @@ -208,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); @@ -232,13 +273,11 @@ class ImageRepo } /** - * Check if the provided image type is valid. - * @param $type - * @return bool + * Get the validation rules for image files. + * @return string */ - public function isValidType($type) + public function getImageValidationRules() { - $validTypes = ['gallery', 'cover', 'system', 'user']; - return in_array($type, $validTypes); + return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'; } } diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 1dd8b713d..eaf787c9c 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, bool $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(); } /** @@ -306,6 +336,7 @@ class ImageService extends UploadService $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; 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 ce5cfbf4e..b894c3fa6 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -257,39 +257,38 @@ function drawIoPlugin() { DrawIO.show(drawingInit, updateContent); } - function updateContent(pngData) { - let id = "image-" + Math.random().toString(16).slice(2); - let loadingImage = window.baseUrl('/loading.gif'); - let data = { - image: pngData, - uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) - }; + async function updateContent(pngData) { + const id = "image-" + Math.random().toString(16).slice(2); + const loadingImage = window.baseUrl('/loading.gif'); + const pageId = Number(document.getElementById('page-editor').getAttribute('page-id')); // Handle updating an existing image if (currentNode) { DrawIO.close(); let imgElem = currentNode.querySelector('img'); - window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => { - pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url); - pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(imgElem, 'src', img.url); + pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id); + } catch (err) { window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } return; } - setTimeout(() => { + setTimeout(async () => { pageEditor.insertContent(`
`); DrawIO.close(); - window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { - pageEditor.dom.setAttrib(id, 'src', resp.data.url); - pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(id, 'src', img.url); + pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id); + } catch (err) { pageEditor.dom.remove(id); window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } }, 5); } @@ -300,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 b4fcfd59f..a570737d1 100644 --- a/resources/assets/js/services/drawio.js +++ b/resources/assets/js/services/drawio.js @@ -66,4 +66,23 @@ function drawPostMessage(data) { iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); } -export default {show, close}; \ No newline at end of file +async function upload(imageData, pageUploadedToId) { + let data = { + image: imageData, + uploaded_to: pageUploadedToId, + }; + const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data); + return resp.data; +} + +/** + * 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/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js index 6bfc2662d..dd1d9d17a 100644 --- a/resources/assets/js/vues/image-manager.js +++ b/resources/assets/js/vues/image-manager.js @@ -1,7 +1,7 @@ import * as Dates from "../services/dates"; import dropzone from "./components/dropzone"; -let page = 0; +let page = 1; let previousClickTime = 0; let previousClickImage = 0; let dataLoaded = false; @@ -20,7 +20,7 @@ const data = { selectedImage: false, dependantPages: false, showing: false, - view: 'all', + filter: null, hasMore: false, searching: false, searchTerm: '', @@ -56,32 +56,37 @@ const methods = { this.$el.children[0].components.overlay.hide(); }, - fetchData() { - let url = baseUrl + page; - let query = {}; - if (this.uploadedTo !== false) query.page_id = this.uploadedTo; - if (this.searching) query.term = this.searchTerm; + async fetchData() { + let query = { + page, + search: this.searching ? this.searchTerm : null, + uploaded_to: this.uploadedTo || null, + filter_type: this.filter, + }; - this.$http.get(url, {params: query}).then(response => { - this.images = this.images.concat(response.data.images); - this.hasMore = response.data.hasMore; - page++; - }); + const {data} = await this.$http.get(baseUrl, {params: query}); + this.images = this.images.concat(data.images); + this.hasMore = data.has_more; + page++; }, - setView(viewName) { - this.view = viewName; + setFilterType(filterType) { + this.filter = filterType; this.resetState(); this.fetchData(); }, resetState() { this.cancelSearch(); + this.resetListView(); + this.deleteConfirm = false; + baseUrl = window.baseUrl(`/images/${this.imageType}`); + }, + + resetListView() { this.images = []; this.hasMore = false; - this.deleteConfirm = false; - page = 0; - baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`); + page = 1; }, searchImages() { @@ -94,10 +99,7 @@ const methods = { } this.searching = true; - this.images = []; - this.hasMore = false; - page = 0; - baseUrl = window.baseUrl(`/images/${this.imageType}/search/`); + this.resetListView(); this.fetchData(); }, @@ -110,10 +112,10 @@ const methods = { }, imageSelect(image) { - let dblClickTime = 300; - let currentTime = Date.now(); - let timeDiff = currentTime - previousClickTime; - let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; + const dblClickTime = 300; + const currentTime = Date.now(); + const timeDiff = currentTime - previousClickTime; + const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; if (isDblClick) { this.callbackAndHide(image); @@ -132,11 +134,11 @@ const methods = { this.hide(); }, - saveImageDetails() { - let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); - this.$http.put(url, this.selectedImage).then(response => { - this.$events.emit('success', trans('components.image_update_success')); - }).catch(error => { + async saveImageDetails() { + let url = window.baseUrl(`/images/${this.selectedImage.id}`); + try { + await this.$http.put(url, this.selectedImage) + } catch (error) { if (error.response.status === 422) { let errors = error.response.data; let message = ''; @@ -145,27 +147,29 @@ const methods = { }); this.$events.emit('error', message); } - }); + } }, - deleteImage() { + async deleteImage() { if (!this.deleteConfirm) { - let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); - this.$http.get(url).then(resp => { - this.dependantPages = resp.data; - }).catch(console.error).then(() => { - this.deleteConfirm = true; - }); + const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); + try { + const {data} = await this.$http.get(url); + this.dependantPages = data; + } catch (error) { + console.error(error); + } + this.deleteConfirm = true; return; } - let url = window.baseUrl(`/images/${this.selectedImage.id}`); - this.$http.delete(url).then(resp => { - this.images.splice(this.images.indexOf(this.selectedImage), 1); - this.selectedImage = false; - this.$events.emit('success', trans('components.image_delete_success')); - this.deleteConfirm = false; - }); + + const url = window.baseUrl(`/images/${this.selectedImage.id}`); + await this.$http.delete(url); + this.images.splice(this.images.indexOf(this.selectedImage), 1); + this.selectedImage = false; + this.$events.emit('success', trans('components.image_delete_success')); + this.deleteConfirm = false; }, getDate(stringDate) { @@ -180,7 +184,7 @@ const methods = { const computed = { uploadUrl() { - return window.baseUrl(`/images/${this.imageType}/upload`); + return window.baseUrl(`/images/${this.imageType}`); } }; @@ -188,7 +192,7 @@ function mounted() { window.ImageManager = this; this.imageType = this.$el.getAttribute('image-type'); this.uploadedTo = this.$el.getAttribute('uploaded-to'); - baseUrl = window.baseUrl('/images/' + this.imageType + '/all/') + baseUrl = window.baseUrl('/images/' + this.imageType) } export default { diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index f40c92a19..48f4a902e 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -251,7 +251,7 @@ input[type=date] { } .form-group { - .text-pos, .text-neg { + div.text-pos, div.text-neg, p.text-post, p.text-neg { padding: $-xs 0; } } 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 f048b543b..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']) @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-manager.blade.php b/resources/views/components/image-manager.blade.php index df577b545..7c9084ad1 100644 --- a/resources/views/components/image-manager.blade.php +++ b/resources/views/components/image-manager.blade.php @@ -10,12 +10,12 @@
- diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index b9ad052c7..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']) @stop diff --git a/routes/web.php b/routes/web.php index 695f61654..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() { @@ -103,22 +104,21 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/user/{userId}', 'UserController@showProfilePage'); // Image routes - Route::group(['prefix' => 'images'], function() { - // Get for user images - Route::get('/user/all', 'ImageController@getAllForUserType'); - Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); - // Standard get, update and deletion for all types - Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); - Route::get('/base64/{id}', 'ImageController@getBase64Image'); - Route::put('/update/{imageId}', 'ImageController@update'); - Route::post('/drawing/upload', 'ImageController@uploadDrawing'); - Route::get('/usage/{id}', 'ImageController@usage'); - Route::post('/{type}/upload', 'ImageController@uploadByType'); - Route::get('/{type}/all', 'ImageController@getAllByType'); - Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); - Route::get('/{type}/search/{page}', 'ImageController@searchByType'); - Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); - Route::delete('/{id}', 'ImageController@destroy'); + Route::group(['prefix' => 'images'], function () { + + // 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'); + + // 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 diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 8373a809c..01bf23d5b 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -17,14 +17,10 @@ class ImageTest extends TestCase $admin = $this->getAdmin(); $this->actingAs($admin); - $imageName = 'first-image.png'; - $relPath = $this->getTestImagePath('gallery', $imageName); - $this->deleteImage($relPath); + $imgDetails = $this->uploadGalleryImage($page); + $relPath = $imgDetails['path']; - $upload = $this->uploadImage($imageName, $page->id); - $upload->assertStatus(200); - - $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath)); + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath)); $this->deleteImage($relPath); @@ -35,10 +31,93 @@ class ImageTest extends TestCase 'path' => $relPath, 'created_by' => $admin->id, 'updated_by' => $admin->id, - 'name' => $imageName + 'name' => $imgDetails['name'], ]); } + public function test_image_edit() + { + $editor = $this->getEditor(); + $this->actingAs($editor); + + $imgDetails = $this->uploadGalleryImage(); + $image = Image::query()->first(); + + $newName = str_random(); + $update = $this->put('/images/' . $image->id, ['name' => $newName]); + $update->assertSuccessful(); + $update->assertJson([ + 'id' => $image->id, + 'name' => $newName, + 'type' => 'gallery', + ]); + + $this->deleteImage($imgDetails['path']); + + $this->assertDatabaseHas('images', [ + 'type' => 'gallery', + 'name' => $newName + ]); + } + + public function test_gallery_get_list_format() + { + $this->asEditor(); + + $imgDetails = $this->uploadGalleryImage(); + $image = Image::query()->first(); + + $emptyJson = ['images' => [], 'has_more' => false]; + $resultJson = [ + 'images' => [ + [ + 'id' => $image->id, + 'name' => $imgDetails['name'], + ] + ], + 'has_more' => false, + ]; + + $pageId = $imgDetails['page']->id; + $firstPageRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}"); + $firstPageRequest->assertSuccessful()->assertJson($resultJson); + + $secondPageRequest = $this->get("/images/gallery?page=2&uploaded_to={$pageId}"); + $secondPageRequest->assertSuccessful()->assertExactJson($emptyJson); + + $namePartial = substr($imgDetails['name'], 0, 3); + $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}"); + $searchHitRequest->assertSuccessful()->assertJson($resultJson); + + $namePartial = str_random(16); + $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}"); + $searchHitRequest->assertSuccessful()->assertExactJson($emptyJson); + } + + public function test_image_usage() + { + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $imgDetails = $this->uploadGalleryImage($page); + + $image = Image::query()->first(); + $page->html = ''; + $page->save(); + + $usage = $this->get('/images/usage/' . $image->id); + $usage->assertSuccessful(); + $usage->assertJson([ + [ + 'id' => $page->id, + 'name' => $page->name + ] + ]); + + $this->deleteImage($imgDetails['path']); + } + public function test_php_files_cannot_be_uploaded() { $page = Page::first(); @@ -50,7 +129,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); @@ -72,7 +151,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); @@ -89,7 +168,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded double extension file was uploaded but should have been stopped'); @@ -101,9 +180,9 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); - $upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); + $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload->assertStatus(200); $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); @@ -119,9 +198,9 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); - $upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); + $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $imageUrl = json_decode($upload->getContent(), true)['url']; $page->html .= ""; $page->save(); @@ -139,13 +218,12 @@ class ImageTest extends TestCase public function test_system_images_remain_public() { config()->set('filesystems.default', 'local_secure'); - $this->asEditor(); + $this->asAdmin(); $galleryFile = $this->getTestImage('my-system-test-upload.png'); - $page = Page::first(); - $expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png'); + $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png'); - $upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); - $upload->assertStatus(200); + $upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []); + $upload->assertRedirect('/settings'); $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); @@ -183,8 +261,10 @@ class ImageTest extends TestCase $this->uploadImage($imageName, $page->id); $image = Image::first(); + $image->type = 'drawio'; + $image->save(); - $imageGet = $this->getJson("/images/base64/{$image->id}"); + $imageGet = $this->getJson("/images/drawio/base64/{$image->id}"); $imageGet->assertJson([ 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' ]); @@ -196,7 +276,7 @@ class ImageTest extends TestCase $editor = $this->getEditor(); $this->actingAs($editor); - $upload = $this->postJson('images/drawing/upload', [ + $upload = $this->postJson('images/drawio', [ 'uploaded_to' => $page->id, 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' ]); @@ -217,28 +297,58 @@ class ImageTest extends TestCase $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected"); } + protected function getTestProfileImage() + { + $imageName = 'profile.png'; + $relPath = $this->getTestImagePath('user', $imageName); + $this->deleteImage($relPath); + + return $this->getTestImage($imageName); + } + + public function test_user_image_upload() + { + $editor = $this->getEditor(); + $admin = $this->getAdmin(); + $this->actingAs($admin); + + $file = $this->getTestProfileImage(); + $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); + + $this->assertDatabaseHas('images', [ + 'type' => 'user', + 'uploaded_to' => $editor->id, + 'created_by' => $admin->id, + ]); + } + public function test_user_images_deleted_on_user_deletion() { $editor = $this->getEditor(); $this->actingAs($editor); - $imageName = 'profile.png'; - $relPath = $this->getTestImagePath('gallery', $imageName); - $this->deleteImage($relPath); - - $file = $this->getTestImage($imageName); - $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []); - $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []); + $file = $this->getTestProfileImage(); + $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); - $this->assertTrue($profileImages->count() === 2, "Found profile images does not match upload count"); + $this->assertTrue($profileImages->count() === 1, "Found profile images does not match upload count"); + + $imagePath = public_path($profileImages->first()->path); + $this->assertTrue(file_exists($imagePath)); $userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}"); $userDelete->assertStatus(302); + $this->assertDatabaseMissing('images', [ 'type' => 'user', 'created_by' => $editor->id ]); + $this->assertDatabaseMissing('images', [ + 'type' => 'user', + 'uploaded_to' => $editor->id + ]); + + $this->assertFalse(file_exists($imagePath)); } public function test_deleted_unused_images() diff --git a/tests/Uploads/UsesImages.php b/tests/Uploads/UsesImages.php index 93bf278e2..aa5ffe4c7 100644 --- a/tests/Uploads/UsesImages.php +++ b/tests/Uploads/UsesImages.php @@ -1,6 +1,7 @@ getTestImage($name); return $this->withHeader('Content-Type', $contentType) - ->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + ->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + } + + /** + * Upload a new gallery image. + * Returns the image name. + * Can provide a page to relate the image to. + * @param Page|null $page + * @return array + */ + protected function uploadGalleryImage(Page $page = null) + { + if ($page === null) { + $page = Page::query()->first(); + } + + $imageName = 'first-image.png'; + $relPath = $this->getTestImagePath('gallery', $imageName); + $this->deleteImage($relPath); + + $upload = $this->uploadImage($imageName, $page->id); + $upload->assertStatus(200); + return [ + 'name' => $imageName, + 'path' => $relPath, + 'page' => $page + ]; } /**