Merge pull request #1410 from BookStackApp/image_management_rewrite

Image management rewrite
This commit is contained in:
Dan Brown 2019-05-04 18:16:58 +01:00 committed by GitHub
commit 15786e2630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 962 additions and 576 deletions

View File

@ -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 $query
* @param $tableName * @param $tableName
* @param $entityIdColumn * @param $entityIdColumn
* @return mixed * @return mixed
*/ */
public function filterRelatedPages($query, $tableName, $entityIdColumn) public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
{ {
$this->currentAction = 'view'; $this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$pageMorphClass = $this->entityProvider->page->getMorphClass(); $pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) { $query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
@ -761,7 +764,9 @@ class PermissionService
}); });
})->orWhere($tableDetails['entityIdColumn'], '=', 0); })->orWhere($tableDetails['entityIdColumn'], '=', 0);
}); });
$this->clean(); $this->clean();
return $q; return $q;
} }

View File

@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
* The attributes that are mass assignable. * The attributes that are mass assignable.
* @var array * @var array
*/ */
protected $fillable = ['name', 'email', 'image_id']; protected $fillable = ['name', 'email'];
/** /**
* The attributes excluded from the model's JSON form. * The attributes excluded from the model's JSON form.

View File

@ -198,7 +198,7 @@ class UserRepo
$user->delete(); $user->delete();
// Delete user profile images // 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) { foreach ($profileImages as $image) {
Images::destroy($image); Images::destroy($image);
} }

View File

@ -762,7 +762,7 @@ class EntityRepo
*/ */
public function searchForImage($imageString) 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) { foreach ($pages as $page) {
$page->url = $page->getUrl(); $page->url = $page->getUrl();
$page->html = ''; $page->html = '';

View File

@ -6,6 +6,7 @@ use BookStack\Entities\Book;
use BookStack\Entities\EntityContextManager; use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService; use BookStack\Entities\ExportService;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -17,6 +18,7 @@ class BookController extends Controller
protected $userRepo; protected $userRepo;
protected $exportService; protected $exportService;
protected $entityContextManager; protected $entityContextManager;
protected $imageRepo;
/** /**
* BookController constructor. * BookController constructor.
@ -24,17 +26,20 @@ class BookController extends Controller
* @param UserRepo $userRepo * @param UserRepo $userRepo
* @param ExportService $exportService * @param ExportService $exportService
* @param EntityContextManager $entityContextManager * @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
*/ */
public function __construct( public function __construct(
EntityRepo $entityRepo, EntityRepo $entityRepo,
UserRepo $userRepo, UserRepo $userRepo,
ExportService $exportService, ExportService $exportService,
EntityContextManager $entityContextManager EntityContextManager $entityContextManager,
ImageRepo $imageRepo
) { ) {
$this->entityRepo = $entityRepo; $this->entityRepo = $entityRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
$this->exportService = $exportService; $this->exportService = $exportService;
$this->entityContextManager = $entityContextManager; $this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct(); parent::__construct();
} }
@ -101,13 +106,15 @@ class BookController extends Controller
* @param string $shelfSlug * @param string $shelfSlug
* @return Response * @return Response
* @throws \BookStack\Exceptions\NotFoundException * @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/ */
public function store(Request $request, string $shelfSlug = null) public function store(Request $request, string $shelfSlug = null)
{ {
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000' 'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]); ]);
$bookshelf = null; $bookshelf = null;
@ -117,6 +124,7 @@ class BookController extends Controller
} }
$book = $this->entityRepo->createFromInput('book', $request->all()); $book = $this->entityRepo->createFromInput('book', $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_create', $book->id); Activity::add($book, 'book_create', $book->id);
if ($bookshelf) { if ($bookshelf) {
@ -170,20 +178,27 @@ class BookController extends Controller
/** /**
* Update the specified book in storage. * Update the specified book in storage.
* @param Request $request * @param Request $request
* @param $slug * @param $slug
* @return Response * @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); $book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', '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()); $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_update', $book->id); Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -311,7 +326,12 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name); Activity::addMessage('book_delete', 0, $book->name);
if ($book->cover) {
$this->imageRepo->destroyImage($book->cover);
}
$this->entityRepo->destroyBook($book); $this->entityRepo->destroyBook($book);
return redirect('/books'); return redirect('/books');
} }
@ -383,4 +403,28 @@ class BookController extends Controller
$textContent = $this->exportService->bookToPlainText($book); $textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt'); 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();
}
}
} }

View File

@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf; use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager; use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo; use BookStack\Entities\Repos\EntityRepo;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Views; use Views;
@ -15,18 +16,21 @@ class BookshelfController extends Controller
protected $entityRepo; protected $entityRepo;
protected $userRepo; protected $userRepo;
protected $entityContextManager; protected $entityContextManager;
protected $imageRepo;
/** /**
* BookController constructor. * BookController constructor.
* @param EntityRepo $entityRepo * @param EntityRepo $entityRepo
* @param UserRepo $userRepo * @param UserRepo $userRepo
* @param EntityContextManager $entityContextManager * @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->entityRepo = $entityRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
$this->entityContextManager = $entityContextManager; $this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct(); parent::__construct();
} }
@ -82,8 +86,9 @@ class BookshelfController extends Controller
/** /**
* Store a newly created bookshelf in storage. * Store a newly created bookshelf in storage.
* @param Request $request * @param Request $request
* @return Response * @return Response
* @throws \BookStack\Exceptions\ImageUploadException
*/ */
public function store(Request $request) public function store(Request $request)
{ {
@ -91,13 +96,14 @@ class BookshelfController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000', 'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]); ]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); $this->shelfUpdateActions($shelf, $request);
Activity::add($bookshelf, 'bookshelf_create');
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) public function show(string $slug)
{ {
/** @var Bookshelf $bookshelf */ /** @var Bookshelf $shelf */
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $bookshelf); $this->checkOwnablePermission('book-view', $shelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf); $books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($bookshelf); Views::add($shelf);
$this->entityContextManager->setShelfContext($bookshelf->id); $this->entityContextManager->setShelfContext($shelf->id);
$this->setPageTitle($bookshelf->getShortName()); $this->setPageTitle($shelf->getShortName());
return view('shelves.show', [ return view('shelves.show', [
'shelf' => $bookshelf, 'shelf' => $shelf,
'books' => $books, '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) public function edit(string $slug)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf); $this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf); $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
$shelfBookIds = $shelfBooks->pluck('id'); $shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update'); $books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) { $books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id); 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', [ return view('shelves.edit', [
'shelf' => $bookshelf, 'shelf' => $shelf,
'books' => $books, 'books' => $books,
'shelfBooks' => $shelfBooks, 'shelfBooks' => $shelfBooks,
]); ]);
@ -154,10 +160,11 @@ class BookshelfController extends Controller
/** /**
* Update the specified bookshelf in storage. * Update the specified bookshelf in storage.
* @param Request $request * @param Request $request
* @param string $slug * @param string $slug
* @return Response * @return Response
* @throws \BookStack\Exceptions\NotFoundException * @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/ */
public function update(Request $request, string $slug) public function update(Request $request, string $slug)
{ {
@ -166,10 +173,12 @@ class BookshelfController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000', 'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]); ]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all()); $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); $this->shelfUpdateActions($shelf, $request);
Activity::add($shelf, 'bookshelf_update'); Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl()); return redirect($shelf->getUrl());
@ -184,11 +193,11 @@ class BookshelfController extends Controller
*/ */
public function showDelete(string $slug) public function showDelete(string $slug)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf); $this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()])); $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
return view('shelves.delete', ['shelf' => $bookshelf]); return view('shelves.delete', ['shelf' => $shelf]);
} }
/** /**
@ -200,10 +209,15 @@ class BookshelfController extends Controller
*/ */
public function destroy(string $slug) public function destroy(string $slug)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf); $this->checkOwnablePermission('bookshelf-delete', $shelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name); Activity::addMessage('bookshelf_delete', 0, $shelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
if ($shelf->cover) {
$this->imageRepo->destroyImage($shelf->cover);
}
$this->entityRepo->destroyBookshelf($shelf);
return redirect('/shelves'); return redirect('/shelves');
} }
@ -215,12 +229,12 @@ class BookshelfController extends Controller
*/ */
public function showPermissions(string $slug) public function showPermissions(string $slug)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf); $this->checkOwnablePermission('restrictions-manage', $shelf);
$roles = $this->userRepo->getRestrictableRoles(); $roles = $this->userRepo->getRestrictableRoles();
return view('shelves.permissions', [ return view('shelves.permissions', [
'shelf' => $bookshelf, 'shelf' => $shelf,
'roles' => $roles 'roles' => $roles
]); ]);
} }
@ -235,12 +249,12 @@ class BookshelfController extends Controller
*/ */
public function permissions(string $slug, Request $request) public function permissions(string $slug, Request $request)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf); $this->checkOwnablePermission('restrictions-manage', $shelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf); $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
session()->flash('success', trans('entities.shelves_permissions_updated')); 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) public function copyPermissions(string $slug)
{ {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf); $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])); 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();
}
} }
} }

View File

@ -1,246 +0,0 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->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'));
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class DrawioImageController extends Controller
{
protected $imageRepo;
/**
* DrawioImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->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)
]);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class GalleryImageController extends Controller
{
protected $imageRepo;
/**
* GalleryImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->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);
}
}

View File

@ -0,0 +1,115 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->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);
}
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService; use BookStack\Uploads\ImageService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -8,6 +9,19 @@ use Setting;
class SettingController extends Controller class SettingController extends Controller
{ {
protected $imageRepo;
/**
* SettingController constructor.
* @param $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/** /**
* Display a listing of the settings. * Display a listing of the settings.
* @return Response * @return Response
@ -35,6 +49,9 @@ class SettingController extends Controller
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermission('settings-manage'); $this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
]);
// Cycles through posted settings and update them // Cycles through posted settings and update them
foreach ($request->all() as $name => $value) { foreach ($request->all() as $name => $value) {
@ -42,7 +59,21 @@ class SettingController extends Controller
continue; continue;
} }
$key = str_replace('setting-', '', trim($name)); $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')); session()->flash('success', trans('settings.settings_save_success'));

View File

@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Auth\UserRepo; use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException; use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -12,16 +13,19 @@ class UserController extends Controller
protected $user; protected $user;
protected $userRepo; protected $userRepo;
protected $imageRepo;
/** /**
* UserController constructor. * UserController constructor.
* @param User $user * @param User $user
* @param UserRepo $userRepo * @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->user = $user;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
$this->imageRepo = $imageRepo;
parent::__construct(); parent::__construct();
} }
@ -107,9 +111,7 @@ class UserController extends Controller
*/ */
public function edit($id, SocialAuthService $socialAuthService) public function edit($id, SocialAuthService $socialAuthService)
{ {
$this->checkPermissionOr('users-manage', function () use ($id) { $this->checkPermissionOrCurrentUser('users-manage', $id);
return $this->currentUser->id == $id;
});
$user = $this->user->findOrFail($id); $user = $this->user->findOrFail($id);
@ -123,24 +125,24 @@ class UserController extends Controller
/** /**
* Update the specified user in storage. * Update the specified user in storage.
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return Response * @return Response
* @throws UserUpdateException * @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
*/ */
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) { $this->checkPermissionOrCurrentUser('users-manage', $id);
return $this->currentUser->id == $id;
});
$this->validate($request, [ $this->validate($request, [
'name' => 'min:2', 'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id, 'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm', 'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password', 'password-confirm' => 'same:password|required_with:password',
'setting' => 'array' 'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
]); ]);
$user = $this->userRepo->getById($id); $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(); $user->save();
session()->flash('success', trans('settings.users_edit_success')); 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); return redirect($redirectUrl);
} }
@ -184,9 +199,7 @@ class UserController extends Controller
*/ */
public function delete($id) public function delete($id)
{ {
$this->checkPermissionOr('users-manage', function () use ($id) { $this->checkPermissionOrCurrentUser('users-manage', $id);
return $this->currentUser->id == $id;
});
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
@ -202,9 +215,7 @@ class UserController extends Controller
public function destroy($id) public function destroy($id)
{ {
$this->preventAccessForDemoUsers(); $this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) { $this->checkPermissionOrCurrentUser('users-manage', $id);
return $this->currentUser->id == $id;
});
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById($id);

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Uploads; <?php namespace BookStack\Uploads;
use BookStack\Entities\Page;
use BookStack\Ownable; use BookStack\Ownable;
use Images; use Images;
@ -20,4 +21,14 @@ class Image extends Ownable
{ {
return Images::getThumbnail($this, $width, $height, $keepRatio); return Images::getThumbnail($this, $width, $height, $keepRatio);
} }
/**
* Get the page this image has been uploaded to.
* Only applicable to gallery or drawio image types.
* @return Page|null
*/
public function getPage()
{
return $this->belongsTo(Page::class, 'uploaded_to')->first();
}
} }

View File

@ -2,6 +2,7 @@
use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Page; use BookStack\Entities\Page;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo class ImageRepo
@ -19,7 +20,12 @@ class ImageRepo
* @param \BookStack\Auth\Permissions\PermissionService $permissionService * @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param \BookStack\Entities\Page $page * @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->image = $image;
$this->imageService = $imageService; $this->imageService = $imageService;
@ -31,7 +37,7 @@ class ImageRepo
/** /**
* Get an image with the given id. * Get an image with the given id.
* @param $id * @param $id
* @return mixed * @return Image
*/ */
public function getById($id) public function getById($id)
{ {
@ -44,95 +50,115 @@ class ImageRepo
* @param $query * @param $query
* @param int $page * @param int $page
* @param int $pageSize * @param int $pageSize
* @param bool $filterOnPage
* @return array * @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 = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize; $hasMore = count($images) > $pageSize;
$returnImages = $images->take(24); $returnImages = $images->take($pageSize);
$returnImages->each(function ($image) { $returnImages->each(function ($image) {
$this->loadThumbs($image); $this->loadThumbs($image);
}); });
return [ return [
'images' => $returnImages, '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 string $type
* @param int $page * @param int $page
* @param int $pageSize * @param int $pageSize
* @param bool|int $userFilter * @param int $uploadedTo
* @param string|null $search
* @param callable|null $whereClause
* @return array * @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) { if ($uploadedTo !== null) {
$images = $images->where('created_by', '=', $userFilter); $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 $type
* @param string $filterType
* @param int $page * @param int $page
* @param int $pageSize * @param int $pageSize
* @param string $searchTerm * @param int|null $uploadedTo
* @param string|null $search
* @return array * @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 . '%'); $contextPage = $this->page->findOrFail($uploadedTo);
return $this->returnPaginated($images, $page, $pageSize); $parentFilter = null;
}
/** if ($filterType === 'book' || $filterType === 'page') {
* Get gallery images with a particular filter criteria such as $parentFilter = function(Builder $query) use ($filterType, $contextPage) {
* being within the current book or page. if ($filterType === 'page') {
* @param $filter $query->where('uploaded_to', '=', $contextPage->id);
* @param $pageId } elseif ($filterType === 'book') {
* @param int $pageNum $validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
* @param int $pageSize $query->whereIn('uploaded_to', $validPageIds);
* @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);
} }
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. * Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile * @param UploadedFile $uploadFile
* @param string $type * @param string $type
* @param int $uploadedTo * @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return Image * @return Image
* @throws \BookStack\Exceptions\ImageUploadException * @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); $this->loadThumbs($image);
return $image; return $image;
} }
@ -175,12 +201,27 @@ class ImageRepo
* @return bool * @return bool
* @throws \Exception * @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; 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. * Load thumbnails onto an image object.
@ -191,8 +232,8 @@ class ImageRepo
protected function loadThumbs(Image $image) protected function loadThumbs(Image $image)
{ {
$image->thumbs = [ $image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150), 'gallery' => $this->getThumbnail($image, 150, 150, false),
'display' => $this->getThumbnail($image, 840, 0, true) 'display' => $this->getThumbnail($image, 840, null, true)
]; ];
} }
@ -208,7 +249,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException * @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception * @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 { try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
@ -232,13 +273,11 @@ class ImageRepo
} }
/** /**
* Check if the provided image type is valid. * Get the validation rules for image files.
* @param $type * @return string
* @return bool
*/ */
public function isValidType($type) public function getImageValidationRules()
{ {
$validTypes = ['gallery', 'cover', 'system', 'user']; return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
return in_array($type, $validTypes);
} }
} }

View File

@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService class ImageService extends UploadService
@ -57,15 +58,30 @@ class ImageService extends UploadService
/** /**
* Saves a new image from an upload. * Saves a new image from an upload.
* @param UploadedFile $uploadedFile * @param UploadedFile $uploadedFile
* @param string $type * @param string $type
* @param int $uploadedTo * @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return mixed * @return mixed
* @throws ImageUploadException * @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(); $imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath()); $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); return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
} }
@ -122,7 +138,7 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images'); $secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName); $imageName = str_replace(' ', '-', $imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) { while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName; $imageName = str_random(3) . $imageName;
@ -201,8 +217,28 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath); 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 { try {
$thumb = $this->imageTool->make($storage->get($imagePath)); $thumb = $this->imageTool->make($imageData);
} catch (Exception $e) { } catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs')); throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
@ -211,20 +247,14 @@ class ImageService extends UploadService
} }
if ($keepRatio) { if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) { $thumb->resize($width, $height, function ($constraint) {
$constraint->aspectRatio(); $constraint->aspectRatio();
$constraint->upsize(); $constraint->upsize();
}); });
} else { } else {
$thumb->fit($width, $height); $thumb->fit($width, $height);
} }
return (string)$thumb->encode();
$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);
} }
/** /**
@ -306,6 +336,7 @@ class ImageService extends UploadService
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName); $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
$image->created_by = $user->id; $image->created_by = $user->id;
$image->updated_by = $user->id; $image->updated_by = $user->id;
$image->uploaded_to = $user->id;
$image->save(); $image->save();
return $image; return $image;

View File

@ -4,54 +4,50 @@ class ImagePicker {
constructor(elem) { constructor(elem) {
this.elem = elem; this.elem = elem;
this.imageElem = elem.querySelector('img'); 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.defaultImage = elem.getAttribute('data-default-image');
this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width');
this.isResizeCropping = elem.getAttribute('data-resize-crop') !== '';
let selectButton = elem.querySelector('button[data-action="show-image-manager"]'); const resetButton = elem.querySelector('button[data-action="reset-image"]');
selectButton.addEventListener('click', this.selectImage.bind(this));
let resetButton = elem.querySelector('button[data-action="reset-image"]');
resetButton.addEventListener('click', this.reset.bind(this)); 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) { if (removeButton) {
removeButton.addEventListener('click', this.removeImage.bind(this)); removeButton.addEventListener('click', this.removeImage.bind(this));
} }
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
} }
selectImage() { fileInputChange() {
window.ImageManager.show(image => { this.resetInput.setAttribute('disabled', 'disabled');
if (!this.isResizing) { if (this.removeInput) {
this.setImage(image); this.removeInput.setAttribute('disabled', 'disabled');
return; }
}
let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false'); for (let file of this.imageInput.files) {
this.imageElem.src = window.URL.createObjectURL(file);
window.$http.get(window.baseUrl(requestString)).then(resp => { }
image.url = resp.data.url; this.imageElem.classList.remove('none');
this.setImage(image);
});
});
} }
reset() { reset() {
this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')}); this.imageInput.value = '';
} this.imageElem.src = this.defaultImage;
this.resetInput.removeAttribute('disabled');
setImage(image) { if (this.removeInput) {
this.imageElem.src = image.url; this.removeInput.setAttribute('disabled', 'disabled');
this.input.value = this.isUsingIds ? image.id : image.url; }
this.imageElem.classList.remove('none'); this.imageElem.classList.remove('none');
} }
removeImage() { removeImage() {
this.imageElem.src = this.elem.getAttribute('data-default-image'); this.imageInput.value = '';
this.imageElem.classList.add('none'); this.imageElem.classList.add('none');
this.input.value = 'none'; this.removeInput.removeAttribute('disabled');
this.resetInput.setAttribute('disabled', 'disabled');
} }
} }

View File

@ -394,9 +394,7 @@ class MarkdownEditor {
const drawingId = imgContainer.getAttribute('drawio-diagram'); const drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(() => { DrawIO.show(() => {
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { return DrawIO.load(drawingId);
return `data:image/png;base64,${resp.data.content}`;
});
}, (pngData) => { }, (pngData) => {
let data = { let data = {

View File

@ -257,39 +257,38 @@ function drawIoPlugin() {
DrawIO.show(drawingInit, updateContent); DrawIO.show(drawingInit, updateContent);
} }
function updateContent(pngData) { async function updateContent(pngData) {
let id = "image-" + Math.random().toString(16).slice(2); const id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif'); const loadingImage = window.baseUrl('/loading.gif');
let data = { const pageId = Number(document.getElementById('page-editor').getAttribute('page-id'));
image: pngData,
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
};
// Handle updating an existing image // Handle updating an existing image
if (currentNode) { if (currentNode) {
DrawIO.close(); DrawIO.close();
let imgElem = currentNode.querySelector('img'); let imgElem = currentNode.querySelector('img');
window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => { try {
pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url); const img = await DrawIO.upload(pngData, pageId);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id); pageEditor.dom.setAttrib(imgElem, 'src', img.url);
}).catch(err => { pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
} catch (err) {
window.$events.emit('error', trans('errors.image_upload_error')); window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err); console.log(err);
}); }
return; return;
} }
setTimeout(() => { setTimeout(async () => {
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`); pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
DrawIO.close(); DrawIO.close();
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { try {
pageEditor.dom.setAttrib(id, 'src', resp.data.url); const img = await DrawIO.upload(pngData, pageId);
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); pageEditor.dom.setAttrib(id, 'src', img.url);
}).catch(err => { pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
} catch (err) {
pageEditor.dom.remove(id); pageEditor.dom.remove(id);
window.$events.emit('error', trans('errors.image_upload_error')); window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err); console.log(err);
}); }
}, 5); }, 5);
} }
@ -300,9 +299,7 @@ function drawIoPlugin() {
} }
let drawingId = currentNode.getAttribute('drawio-diagram'); let drawingId = currentNode.getAttribute('drawio-diagram');
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { return DrawIO.load(drawingId);
return `data:image/png;base64,${resp.data.content}`;
});
} }
window.tinymce.PluginManager.add('drawio', function(editor, url) { window.tinymce.PluginManager.add('drawio', function(editor, url) {

View File

@ -66,4 +66,23 @@ function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
} }
export default {show, close}; 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<string>}
*/
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};

View File

@ -1,7 +1,7 @@
import * as Dates from "../services/dates"; import * as Dates from "../services/dates";
import dropzone from "./components/dropzone"; import dropzone from "./components/dropzone";
let page = 0; let page = 1;
let previousClickTime = 0; let previousClickTime = 0;
let previousClickImage = 0; let previousClickImage = 0;
let dataLoaded = false; let dataLoaded = false;
@ -20,7 +20,7 @@ const data = {
selectedImage: false, selectedImage: false,
dependantPages: false, dependantPages: false,
showing: false, showing: false,
view: 'all', filter: null,
hasMore: false, hasMore: false,
searching: false, searching: false,
searchTerm: '', searchTerm: '',
@ -56,32 +56,37 @@ const methods = {
this.$el.children[0].components.overlay.hide(); this.$el.children[0].components.overlay.hide();
}, },
fetchData() { async fetchData() {
let url = baseUrl + page; let query = {
let query = {}; page,
if (this.uploadedTo !== false) query.page_id = this.uploadedTo; search: this.searching ? this.searchTerm : null,
if (this.searching) query.term = this.searchTerm; uploaded_to: this.uploadedTo || null,
filter_type: this.filter,
};
this.$http.get(url, {params: query}).then(response => { const {data} = await this.$http.get(baseUrl, {params: query});
this.images = this.images.concat(response.data.images); this.images = this.images.concat(data.images);
this.hasMore = response.data.hasMore; this.hasMore = data.has_more;
page++; page++;
});
}, },
setView(viewName) { setFilterType(filterType) {
this.view = viewName; this.filter = filterType;
this.resetState(); this.resetState();
this.fetchData(); this.fetchData();
}, },
resetState() { resetState() {
this.cancelSearch(); this.cancelSearch();
this.resetListView();
this.deleteConfirm = false;
baseUrl = window.baseUrl(`/images/${this.imageType}`);
},
resetListView() {
this.images = []; this.images = [];
this.hasMore = false; this.hasMore = false;
this.deleteConfirm = false; page = 1;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`);
}, },
searchImages() { searchImages() {
@ -94,10 +99,7 @@ const methods = {
} }
this.searching = true; this.searching = true;
this.images = []; this.resetListView();
this.hasMore = false;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
this.fetchData(); this.fetchData();
}, },
@ -110,10 +112,10 @@ const methods = {
}, },
imageSelect(image) { imageSelect(image) {
let dblClickTime = 300; const dblClickTime = 300;
let currentTime = Date.now(); const currentTime = Date.now();
let timeDiff = currentTime - previousClickTime; const timeDiff = currentTime - previousClickTime;
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
if (isDblClick) { if (isDblClick) {
this.callbackAndHide(image); this.callbackAndHide(image);
@ -132,11 +134,11 @@ const methods = {
this.hide(); this.hide();
}, },
saveImageDetails() { async saveImageDetails() {
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); let url = window.baseUrl(`/images/${this.selectedImage.id}`);
this.$http.put(url, this.selectedImage).then(response => { try {
this.$events.emit('success', trans('components.image_update_success')); await this.$http.put(url, this.selectedImage)
}).catch(error => { } catch (error) {
if (error.response.status === 422) { if (error.response.status === 422) {
let errors = error.response.data; let errors = error.response.data;
let message = ''; let message = '';
@ -145,27 +147,29 @@ const methods = {
}); });
this.$events.emit('error', message); this.$events.emit('error', message);
} }
}); }
}, },
deleteImage() { async deleteImage() {
if (!this.deleteConfirm) { if (!this.deleteConfirm) {
let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
this.$http.get(url).then(resp => { try {
this.dependantPages = resp.data; const {data} = await this.$http.get(url);
}).catch(console.error).then(() => { this.dependantPages = data;
this.deleteConfirm = true; } catch (error) {
}); console.error(error);
}
this.deleteConfirm = true;
return; return;
} }
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
this.$http.delete(url).then(resp => { const url = window.baseUrl(`/images/${this.selectedImage.id}`);
this.images.splice(this.images.indexOf(this.selectedImage), 1); await this.$http.delete(url);
this.selectedImage = false; this.images.splice(this.images.indexOf(this.selectedImage), 1);
this.$events.emit('success', trans('components.image_delete_success')); this.selectedImage = false;
this.deleteConfirm = false; this.$events.emit('success', trans('components.image_delete_success'));
}); this.deleteConfirm = false;
}, },
getDate(stringDate) { getDate(stringDate) {
@ -180,7 +184,7 @@ const methods = {
const computed = { const computed = {
uploadUrl() { uploadUrl() {
return window.baseUrl(`/images/${this.imageType}/upload`); return window.baseUrl(`/images/${this.imageType}`);
} }
}; };
@ -188,7 +192,7 @@ function mounted() {
window.ImageManager = this; window.ImageManager = this;
this.imageType = this.$el.getAttribute('image-type'); this.imageType = this.$el.getAttribute('image-type');
this.uploadedTo = this.$el.getAttribute('uploaded-to'); this.uploadedTo = this.$el.getAttribute('uploaded-to');
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/') baseUrl = window.baseUrl('/images/' + this.imageType)
} }
export default { export default {

View File

@ -251,7 +251,7 @@ input[type=date] {
} }
.form-group { .form-group {
.text-pos, .text-neg { div.text-pos, div.text-neg, p.text-post, p.text-neg {
padding: $-xs 0; padding: $-xs 0;
} }
} }

View File

@ -140,6 +140,10 @@ body.flexbox {
display: inline-block; display: inline-block;
} }
.hidden {
display: none;
}
.float { .float {
float: left; float: left;
&.right { &.right {

View File

@ -71,6 +71,7 @@ return [
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.', 'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.', '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 validation lines
'custom' => [ 'custom' => [

View File

@ -33,5 +33,4 @@
</div> </div>
</div> </div>
@include('components.image-manager', ['imageType' => 'cover'])
@stop @stop

View File

@ -16,12 +16,10 @@
<div class="content-wrap card"> <div class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.books_edit') }}</h1> <h1 class="list-heading">{{ trans('entities.books_edit') }}</h1>
<form action="{{ $book->getUrl() }}" method="POST"> <form action="{{ $book->getUrl() }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@include('books.form', ['model' => $book]) @include('books.form', ['model' => $book])
</form> </form>
</div> </div>
</div> </div>
@include('components.image-manager', ['imageType' => 'cover'])
@stop @stop

View File

@ -18,13 +18,9 @@
<p class="small">{{ trans('common.cover_image_description') }}</p> <p class="small">{{ trans('common.cover_image_description') }}</p>
@include('components.image-picker', [ @include('components.image-picker', [
'resizeHeight' => '512',
'resizeWidth' => '512',
'showRemove' => false,
'defaultImage' => baseUrl('/book_default_cover.png'), 'defaultImage' => baseUrl('/book_default_cover.png'),
'currentImage' => isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') , 'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
'currentId' => isset($model) && $model->image_id ? $model->image_id : 0, 'name' => 'image',
'name' => 'image_id',
'imageClass' => 'cover' 'imageClass' => 'cover'
]) ])
</div> </div>

View File

@ -10,12 +10,12 @@
<div class="flex-fill image-manager-body"> <div class="flex-fill image-manager-body">
<div class="image-manager-content"> <div class="image-manager-content">
<div v-if="imageType === 'gallery'" class="image-manager-header primary-background-light nav-tabs grid third"> <div v-if="imageType === 'gallery' || imageType === 'drawio'" class="image-manager-header primary-background-light nav-tabs grid third">
<div class="tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: (view=='all')}" @click="setView('all')">@icon('images') {{ trans('components.image_all') }}</div> <div class="tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: !filter}" @click="setFilterType(null)">@icon('images') {{ trans('components.image_all') }}</div>
<div class="tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (view=='book')}" @click="setView('book')">@icon('book', ['class' => 'text-book svg-icon']) {{ trans('entities.book') }}</div> <div class="tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (filter=='book')}" @click="setFilterType('book')">@icon('book', ['class' => 'text-book svg-icon']) {{ trans('entities.book') }}</div>
<div class="tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (view=='page')}" @click="setView('page')">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</div> <div class="tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (filter=='page')}" @click="setFilterType('page')">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</div>
</div> </div>
<div v-show="view === 'all'" > <div>
<form @submit.prevent="searchImages" class="contained-search-box"> <form @submit.prevent="searchImages" class="contained-search-box">
<input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm"> <input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm">
<button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel">@icon('close')</button> <button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel">@icon('close')</button>
@ -63,7 +63,7 @@
<button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button> <button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button>
</div> </div>
<button class="button anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)"> <button class="button primary anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
{{ trans('components.image_select_image') }} {{ trans('components.image_select_image') }}
</button> </button>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,20 +1,32 @@
<div class="image-picker" image-picker="{{$name}}" data-default-image="{{ $defaultImage }}" data-resize-height="{{ $resizeHeight }}" data-resize-width="{{ $resizeWidth }}" data-current-id="{{ $currentId ?? '' }}" data-resize-crop="{{ $resizeCrop ?? '' }}"> <div class="image-picker @if($errors->has($name)) has-error @endif"
image-picker="{{$name}}"
data-default-image="{{ $defaultImage }}">
<div class="grid half"> <div class="grid half">
<div class="text-center"> <div class="text-center">
<img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}"> <img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
</div> </div>
<div class="text-center"> <div class="text-center">
<button class="button outline small" type="button" data-action="show-image-manager">{{ trans('components.image_select_image') }}</button>
<label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
<input type="file" class="hidden" accept="image/*" name="{{ $name }}" id="{{ $name }}">
<input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
@if(isset($removeName))
<input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
@endif
<br> <br>
<button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button> <button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
@if ($showRemove) @if(isset($removeName))
<span class="sep">|</span> <span class="sep">|</span>
<button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button> <button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
@endif @endif
</div> </div>
</div> </div>
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== 0 && $currentId !== false) ? $currentId : $currentImage}}"> @if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
@endif
</div> </div>

View File

@ -79,7 +79,7 @@
<div class="card content-wrap auto-height"> <div class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.app_customization') }}</h2> <h2 class="list-heading">{{ trans('settings.app_customization') }}</h2>
<form action="{{ baseUrl("/settings") }}" method="POST"> <form action="{{ baseUrl("/settings") }}" method="POST" enctype="multipart/form-data">
{!! csrf_field() !!} {!! csrf_field() !!}
<div class="setting-list"> <div class="setting-list">
@ -119,14 +119,12 @@
</div> </div>
<div> <div>
@include('components.image-picker', [ @include('components.image-picker', [
'resizeHeight' => '43', 'removeName' => 'setting-app-logo',
'resizeWidth' => '200', 'removeValue' => 'none',
'showRemove' => true,
'defaultImage' => baseUrl('/logo.png'), 'defaultImage' => baseUrl('/logo.png'),
'currentImage' => setting('app-logo'), 'currentImage' => setting('app-logo'),
'name' => 'setting-app-logo', 'name' => 'app_logo',
'imageClass' => 'logo-image', 'imageClass' => 'logo-image',
'currentId' => false
]) ])
</div> </div>
</div> </div>

View File

@ -26,6 +26,4 @@
</div> </div>
@include('components.image-manager', ['imageType' => 'cover'])
@stop @stop

View File

@ -16,12 +16,11 @@
<div class="card content-wrap"> <div class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.shelves_edit') }}</h1> <h1 class="list-heading">{{ trans('entities.shelves_edit') }}</h1>
<form action="{{ $shelf->getUrl() }}" method="POST"> <form action="{{ $shelf->getUrl() }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@include('shelves.form', ['model' => $shelf]) @include('shelves.form', ['model' => $shelf])
</form> </form>
</div> </div>
</div> </div>
@include('components.image-manager', ['imageType' => 'cover'])
@stop @stop

View File

@ -53,13 +53,9 @@
<p class="small">{{ trans('common.cover_image_description') }}</p> <p class="small">{{ trans('common.cover_image_description') }}</p>
@include('components.image-picker', [ @include('components.image-picker', [
'resizeHeight' => '512',
'resizeWidth' => '512',
'showRemove' => false,
'defaultImage' => baseUrl('/book_default_cover.png'), 'defaultImage' => baseUrl('/book_default_cover.png'),
'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , 'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') ,
'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0, 'name' => 'image',
'name' => 'image_id',
'imageClass' => 'cover' 'imageClass' => 'cover'
]) ])
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="card content-wrap"> <div class="card content-wrap">
<h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1> <h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
<form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post"> <form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post" enctype="multipart/form-data">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@ -29,7 +29,7 @@
'defaultImage' => baseUrl('/user_avatar.png'), 'defaultImage' => baseUrl('/user_avatar.png'),
'currentImage' => $user->getAvatar(80), 'currentImage' => $user->getAvatar(80),
'currentId' => $user->image_id, 'currentId' => $user->image_id,
'name' => 'image_id', 'name' => 'profile_image',
'imageClass' => 'avatar large' 'imageClass' => 'avatar large'
]) ])
</div> </div>
@ -87,5 +87,4 @@
@endif @endif
</div> </div>
@include('components.image-manager', ['imageType' => 'user'])
@stop @stop

View File

@ -6,7 +6,8 @@ Route::get('/robots.txt', 'HomeController@getRobots');
// Authenticated routes... // Authenticated routes...
Route::group(['middleware' => 'auth'], function () { 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', '.*$'); ->where('path', '.*$');
Route::group(['prefix' => 'pages'], function() { Route::group(['prefix' => 'pages'], function() {
@ -103,22 +104,21 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/user/{userId}', 'UserController@showProfilePage'); Route::get('/user/{userId}', 'UserController@showProfilePage');
// Image routes // Image routes
Route::group(['prefix' => 'images'], function() { Route::group(['prefix' => 'images'], function () {
// Get for user images
Route::get('/user/all', 'ImageController@getAllForUserType'); // Gallery
Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); Route::get('/gallery', 'Images\GalleryImageController@list');
// Standard get, update and deletion for all types Route::post('/gallery', 'Images\GalleryImageController@create');
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
Route::get('/base64/{id}', 'ImageController@getBase64Image'); // Drawio
Route::put('/update/{imageId}', 'ImageController@update'); Route::get('/drawio', 'Images\DrawioImageController@list');
Route::post('/drawing/upload', 'ImageController@uploadDrawing'); Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64');
Route::get('/usage/{id}', 'ImageController@usage'); Route::post('/drawio', 'Images\DrawioImageController@create');
Route::post('/{type}/upload', 'ImageController@uploadByType');
Route::get('/{type}/all', 'ImageController@getAllByType'); // Shared gallery & draw.io endpoint
Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); Route::get('/usage/{id}', 'Images\ImageController@usage');
Route::get('/{type}/search/{page}', 'ImageController@searchByType'); Route::put('/{id}', 'Images\ImageController@update');
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); Route::delete('/{id}', 'Images\ImageController@destroy');
Route::delete('/{id}', 'ImageController@destroy');
}); });
// Attachments routes // Attachments routes

View File

@ -17,14 +17,10 @@ class ImageTest extends TestCase
$admin = $this->getAdmin(); $admin = $this->getAdmin();
$this->actingAs($admin); $this->actingAs($admin);
$imageName = 'first-image.png'; $imgDetails = $this->uploadGalleryImage($page);
$relPath = $this->getTestImagePath('gallery', $imageName); $relPath = $imgDetails['path'];
$this->deleteImage($relPath);
$upload = $this->uploadImage($imageName, $page->id); $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath));
$upload->assertStatus(200);
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath));
$this->deleteImage($relPath); $this->deleteImage($relPath);
@ -35,10 +31,93 @@ class ImageTest extends TestCase
'path' => $relPath, 'path' => $relPath,
'created_by' => $admin->id, 'created_by' => $admin->id,
'updated_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 = '<img src="'.$image->url.'">';
$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() public function test_php_files_cannot_be_uploaded()
{ {
$page = Page::first(); $page = Page::first();
@ -50,7 +129,7 @@ class ImageTest extends TestCase
$this->deleteImage($relPath); $this->deleteImage($relPath);
$file = $this->getTestImage($fileName); $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); $upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); $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); $this->deleteImage($relPath);
$file = $this->getTestImage($fileName); $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); $upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); $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); $this->deleteImage($relPath);
$file = $this->getTestImage($fileName); $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); $upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded double extension file was uploaded but should have been stopped'); $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(); $this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png'); $galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::first(); $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); $upload->assertStatus(200);
$this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath);
@ -119,9 +198,9 @@ class ImageTest extends TestCase
$this->asEditor(); $this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png'); $galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::first(); $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']; $imageUrl = json_decode($upload->getContent(), true)['url'];
$page->html .= "<img src=\"{$imageUrl}\">"; $page->html .= "<img src=\"{$imageUrl}\">";
$page->save(); $page->save();
@ -139,13 +218,12 @@ class ImageTest extends TestCase
public function test_system_images_remain_public() public function test_system_images_remain_public()
{ {
config()->set('filesystems.default', 'local_secure'); config()->set('filesystems.default', 'local_secure');
$this->asEditor(); $this->asAdmin();
$galleryFile = $this->getTestImage('my-system-test-upload.png'); $galleryFile = $this->getTestImage('my-system-test-upload.png');
$page = Page::first(); $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png');
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png');
$upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []);
$upload->assertStatus(200); $upload->assertRedirect('/settings');
$this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); $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); $this->uploadImage($imageName, $page->id);
$image = Image::first(); $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([ $imageGet->assertJson([
'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII='
]); ]);
@ -196,7 +276,7 @@ class ImageTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->actingAs($editor); $this->actingAs($editor);
$upload = $this->postJson('images/drawing/upload', [ $upload = $this->postJson('images/drawio', [
'uploaded_to' => $page->id, 'uploaded_to' => $page->id,
'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' '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"); $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() public function test_user_images_deleted_on_user_deletion()
{ {
$editor = $this->getEditor(); $editor = $this->getEditor();
$this->actingAs($editor); $this->actingAs($editor);
$imageName = 'profile.png'; $file = $this->getTestProfileImage();
$relPath = $this->getTestImagePath('gallery', $imageName); $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
$this->deleteImage($relPath);
$file = $this->getTestImage($imageName);
$this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
$this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
$profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); $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 = $this->asAdmin()->delete("/settings/users/{$editor->id}");
$userDelete->assertStatus(302); $userDelete->assertStatus(302);
$this->assertDatabaseMissing('images', [ $this->assertDatabaseMissing('images', [
'type' => 'user', 'type' => 'user',
'created_by' => $editor->id 'created_by' => $editor->id
]); ]);
$this->assertDatabaseMissing('images', [
'type' => 'user',
'uploaded_to' => $editor->id
]);
$this->assertFalse(file_exists($imagePath));
} }
public function test_deleted_unused_images() public function test_deleted_unused_images()

View File

@ -1,6 +1,7 @@
<?php namespace Tests\Uploads; <?php namespace Tests\Uploads;
use BookStack\Entities\Page;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
trait UsesImages trait UsesImages
@ -41,7 +42,7 @@ trait UsesImages
*/ */
protected function getTestImagePath($type, $fileName) protected function getTestImagePath($type, $fileName)
{ {
return '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/' . $fileName; return '/uploads/images/' . $type . '/' . Date('Y-m') . '/' . $fileName;
} }
/** /**
@ -55,7 +56,33 @@ trait UsesImages
{ {
$file = $this->getTestImage($name); $file = $this->getTestImage($name);
return $this->withHeader('Content-Type', $contentType) 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
];
} }
/** /**