mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-08-26 04:05:57 +00:00
Merge pull request #1410 from BookStackApp/image_management_rewrite
Image management rewrite
This commit is contained in:
commit
15786e2630
@ -732,18 +732,21 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where permissions are granted.
|
||||
* @param $entityType
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||
{
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
$pageMorphClass = $this->entityProvider->page->getMorphClass();
|
||||
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
@ -761,7 +764,9 @@ class PermissionService
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
* The attributes that are mass assignable.
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
protected $fillable = ['name', 'email'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
|
@ -198,7 +198,7 @@ class UserRepo
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
|
||||
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroy($image);
|
||||
}
|
||||
|
@ -762,7 +762,7 @@ class EntityRepo
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
|
@ -6,6 +6,7 @@ use BookStack\Entities\Book;
|
||||
use BookStack\Entities\EntityContextManager;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\ExportService;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
@ -17,6 +18,7 @@ class BookController extends Controller
|
||||
protected $userRepo;
|
||||
protected $exportService;
|
||||
protected $entityContextManager;
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
@ -24,17 +26,20 @@ class BookController extends Controller
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
* @param EntityContextManager $entityContextManager
|
||||
* @param ImageRepo $imageRepo
|
||||
*/
|
||||
public function __construct(
|
||||
EntityRepo $entityRepo,
|
||||
UserRepo $userRepo,
|
||||
ExportService $exportService,
|
||||
EntityContextManager $entityContextManager
|
||||
EntityContextManager $entityContextManager,
|
||||
ImageRepo $imageRepo
|
||||
) {
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
$this->imageRepo = $imageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@ -101,13 +106,15 @@ class BookController extends Controller
|
||||
* @param string $shelfSlug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function store(Request $request, string $shelfSlug = null)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
'description' => 'string|max:1000',
|
||||
'image' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
$bookshelf = null;
|
||||
@ -117,6 +124,7 @@ class BookController extends Controller
|
||||
}
|
||||
|
||||
$book = $this->entityRepo->createFromInput('book', $request->all());
|
||||
$this->bookUpdateActions($book, $request);
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
|
||||
if ($bookshelf) {
|
||||
@ -170,20 +178,27 @@ class BookController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified book in storage.
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @param $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function update(Request $request, $slug)
|
||||
public function update(Request $request, string $slug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
'description' => 'string|max:1000',
|
||||
'image' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
$this->bookUpdateActions($book, $request);
|
||||
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@ -311,7 +326,12 @@ class BookController extends Controller
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
Activity::addMessage('book_delete', 0, $book->name);
|
||||
|
||||
if ($book->cover) {
|
||||
$this->imageRepo->destroyImage($book->cover);
|
||||
}
|
||||
$this->entityRepo->destroyBook($book);
|
||||
|
||||
return redirect('/books');
|
||||
}
|
||||
|
||||
@ -383,4 +403,28 @@ class BookController extends Controller
|
||||
$textContent = $this->exportService->bookToPlainText($book);
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Common actions to run on book update.
|
||||
* Handles updating the cover image.
|
||||
* @param Book $book
|
||||
* @param Request $request
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
protected function bookUpdateActions(Book $book, Request $request)
|
||||
{
|
||||
// Update the cover image if in request
|
||||
if ($request->has('image')) {
|
||||
$newImage = $request->file('image');
|
||||
$image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
|
||||
$book->image_id = $image->id;
|
||||
$book->save();
|
||||
}
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$this->imageRepo->destroyImage($book->cover);
|
||||
$book->image_id = 0;
|
||||
$book->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\EntityContextManager;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
@ -15,18 +16,21 @@ class BookshelfController extends Controller
|
||||
protected $entityRepo;
|
||||
protected $userRepo;
|
||||
protected $entityContextManager;
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param EntityContextManager $entityContextManager
|
||||
* @param ImageRepo $imageRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager)
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
$this->imageRepo = $imageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@ -82,8 +86,9 @@ class BookshelfController extends Controller
|
||||
|
||||
/**
|
||||
* Store a newly created bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
@ -91,13 +96,14 @@ class BookshelfController extends Controller
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
'image' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
||||
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
|
||||
Activity::add($bookshelf, 'bookshelf_create');
|
||||
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
||||
$this->shelfUpdateActions($shelf, $request);
|
||||
|
||||
return redirect($bookshelf->getUrl());
|
||||
Activity::add($shelf, 'bookshelf_create');
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
|
||||
@ -109,19 +115,19 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function show(string $slug)
|
||||
{
|
||||
/** @var Bookshelf $bookshelf */
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('book-view', $bookshelf);
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('book-view', $shelf);
|
||||
|
||||
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
Views::add($bookshelf);
|
||||
$this->entityContextManager->setShelfContext($bookshelf->id);
|
||||
$books = $this->entityRepo->getBookshelfChildren($shelf);
|
||||
Views::add($shelf);
|
||||
$this->entityContextManager->setShelfContext($shelf->id);
|
||||
|
||||
$this->setPageTitle($bookshelf->getShortName());
|
||||
$this->setPageTitle($shelf->getShortName());
|
||||
return view('shelves.show', [
|
||||
'shelf' => $bookshelf,
|
||||
'shelf' => $shelf,
|
||||
'books' => $books,
|
||||
'activity' => Activity::entityActivity($bookshelf, 20, 1)
|
||||
'activity' => Activity::entityActivity($shelf, 20, 1)
|
||||
]);
|
||||
}
|
||||
|
||||
@ -133,19 +139,19 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function edit(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
|
||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
|
||||
$shelfBookIds = $shelfBooks->pluck('id');
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$books = $books->filter(function ($book) use ($shelfBookIds) {
|
||||
return !$shelfBookIds->contains($book->id);
|
||||
});
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
|
||||
return view('shelves.edit', [
|
||||
'shelf' => $bookshelf,
|
||||
'shelf' => $shelf,
|
||||
'books' => $books,
|
||||
'shelfBooks' => $shelfBooks,
|
||||
]);
|
||||
@ -154,10 +160,11 @@ class BookshelfController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @param Request $request
|
||||
* @param string $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function update(Request $request, string $slug)
|
||||
{
|
||||
@ -166,10 +173,12 @@ class BookshelfController extends Controller
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
'image' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
||||
$this->shelfUpdateActions($shelf, $request);
|
||||
|
||||
Activity::add($shelf, 'bookshelf_update');
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
@ -184,11 +193,11 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function showDelete(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
|
||||
return view('shelves.delete', ['shelf' => $bookshelf]);
|
||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
||||
return view('shelves.delete', ['shelf' => $shelf]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,10 +209,15 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function destroy(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
||||
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
|
||||
$this->entityRepo->destroyBookshelf($bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||
Activity::addMessage('bookshelf_delete', 0, $shelf->name);
|
||||
|
||||
if ($shelf->cover) {
|
||||
$this->imageRepo->destroyImage($shelf->cover);
|
||||
}
|
||||
$this->entityRepo->destroyBookshelf($shelf);
|
||||
|
||||
return redirect('/shelves');
|
||||
}
|
||||
|
||||
@ -215,12 +229,12 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function showPermissions(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('shelves.permissions', [
|
||||
'shelf' => $bookshelf,
|
||||
'shelf' => $shelf,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
@ -235,12 +249,12 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function permissions(string $slug, Request $request)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
|
||||
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
||||
return redirect($bookshelf->getUrl());
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -251,11 +265,37 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function copyPermissions(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||
|
||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
|
||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
|
||||
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||
return redirect($bookshelf->getUrl());
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Common actions to run on bookshelf update.
|
||||
* @param Bookshelf $shelf
|
||||
* @param Request $request
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
|
||||
{
|
||||
// Update the books that the shelf references
|
||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
||||
|
||||
// Update the cover image if in request
|
||||
if ($request->has('image')) {
|
||||
$newImage = $request->file('image');
|
||||
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
|
||||
$shelf->image_id = $image->id;
|
||||
$shelf->save();
|
||||
}
|
||||
|
||||
if ($request->has('image_reset')) {
|
||||
$this->imageRepo->destroyImage($shelf->cover);
|
||||
$shelf->image_id = 0;
|
||||
$shelf->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
89
app/Http/Controllers/Images/DrawioImageController.php
Normal file
89
app/Http/Controllers/Images/DrawioImageController.php
Normal 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)
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
65
app/Http/Controllers/Images/GalleryImageController.php
Normal file
65
app/Http/Controllers/Images/GalleryImageController.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
115
app/Http/Controllers/Images/ImageController.php
Normal file
115
app/Http/Controllers/Images/ImageController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@ -8,6 +9,19 @@ use Setting;
|
||||
|
||||
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.
|
||||
* @return Response
|
||||
@ -35,6 +49,9 @@ class SettingController extends Controller
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->validate($request, [
|
||||
'app_logo' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
// Cycles through posted settings and update them
|
||||
foreach ($request->all() as $name => $value) {
|
||||
@ -42,7 +59,21 @@ class SettingController extends Controller
|
||||
continue;
|
||||
}
|
||||
$key = str_replace('setting-', '', trim($name));
|
||||
Setting::put($key, $value);
|
||||
setting()->put($key, $value);
|
||||
}
|
||||
|
||||
// Update logo image if set
|
||||
if ($request->has('app_logo')) {
|
||||
$logoFile = $request->file('app_logo');
|
||||
$this->imageRepo->destroyByType('system');
|
||||
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
|
||||
setting()->put('app-logo', $image->url);
|
||||
}
|
||||
|
||||
// Clear logo image if requested
|
||||
if ($request->get('app_logo_reset', null)) {
|
||||
$this->imageRepo->destroyByType('system');
|
||||
setting()->remove('app-logo');
|
||||
}
|
||||
|
||||
session()->flash('success', trans('settings.settings_save_success'));
|
||||
|
@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
@ -12,16 +13,19 @@ class UserController extends Controller
|
||||
|
||||
protected $user;
|
||||
protected $userRepo;
|
||||
protected $imageRepo;
|
||||
|
||||
/**
|
||||
* UserController constructor.
|
||||
* @param User $user
|
||||
* @param User $user
|
||||
* @param UserRepo $userRepo
|
||||
* @param ImageRepo $imageRepo
|
||||
*/
|
||||
public function __construct(User $user, UserRepo $userRepo)
|
||||
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->imageRepo = $imageRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@ -107,9 +111,7 @@ class UserController extends Controller
|
||||
*/
|
||||
public function edit($id, SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
|
||||
@ -123,24 +125,24 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified user in storage.
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return Response
|
||||
* @throws UserUpdateException
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'setting' => 'array'
|
||||
'setting' => 'array',
|
||||
'profile_image' => $this->imageRepo->getImageValidationRules(),
|
||||
]);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
@ -170,10 +172,23 @@ class UserController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Save profile image if in request
|
||||
if ($request->has('profile_image')) {
|
||||
$imageUpload = $request->file('profile_image');
|
||||
$this->imageRepo->destroyImage($user->avatar);
|
||||
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
|
||||
$user->image_id = $image->id;
|
||||
}
|
||||
|
||||
// Delete the profile image if set to
|
||||
if ($request->has('profile_image_reset')) {
|
||||
$this->imageRepo->destroyImage($user->avatar);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
session()->flash('success', trans('settings.users_edit_success'));
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
|
||||
return redirect($redirectUrl);
|
||||
}
|
||||
|
||||
@ -184,9 +199,7 @@ class UserController extends Controller
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||
@ -202,9 +215,7 @@ class UserController extends Controller
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->preventAccessForDemoUsers();
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Ownable;
|
||||
use Images;
|
||||
|
||||
@ -20,4 +21,14 @@ class Image extends Ownable
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageRepo
|
||||
@ -19,7 +20,12 @@ class ImageRepo
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
* @param \BookStack\Entities\Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
public function __construct(
|
||||
Image $image,
|
||||
ImageService $imageService,
|
||||
PermissionService $permissionService,
|
||||
Page $page
|
||||
)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageService = $imageService;
|
||||
@ -31,7 +37,7 @@ class ImageRepo
|
||||
/**
|
||||
* Get an image with the given id.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
* @return Image
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
@ -44,95 +50,115 @@ class ImageRepo
|
||||
* @param $query
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool $filterOnPage
|
||||
* @return array
|
||||
*/
|
||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||
private function returnPaginated($query, $page = 1, $pageSize = 24)
|
||||
{
|
||||
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
||||
$images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
|
||||
$hasMore = count($images) > $pageSize;
|
||||
|
||||
$returnImages = $images->take(24);
|
||||
$returnImages = $images->take($pageSize);
|
||||
$returnImages->each(function ($image) {
|
||||
$this->loadThumbs($image);
|
||||
});
|
||||
|
||||
return [
|
||||
'images' => $returnImages,
|
||||
'hasMore' => $hasMore
|
||||
'has_more' => $hasMore
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a load images paginated, filtered by image type.
|
||||
* Fetch a list of images in a paginated format, filtered by image type.
|
||||
* Can be filtered by uploaded to and also by name.
|
||||
* @param string $type
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param bool|int $userFilter
|
||||
* @param int $uploadedTo
|
||||
* @param string|null $search
|
||||
* @param callable|null $whereClause
|
||||
* @return array
|
||||
*/
|
||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
||||
public function getPaginatedByType(
|
||||
string $type,
|
||||
int $page = 0,
|
||||
int $pageSize = 24,
|
||||
int $uploadedTo = null,
|
||||
string $search = null,
|
||||
callable $whereClause = null
|
||||
)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type));
|
||||
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
|
||||
|
||||
if ($userFilter !== false) {
|
||||
$images = $images->where('created_by', '=', $userFilter);
|
||||
if ($uploadedTo !== null) {
|
||||
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
if ($search !== null) {
|
||||
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
|
||||
}
|
||||
|
||||
// Filter by page access
|
||||
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
|
||||
|
||||
if ($whereClause !== null) {
|
||||
$imageQuery = $imageQuery->where($whereClause);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($imageQuery, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for images by query, of a particular type.
|
||||
* Get paginated gallery images within a specific page or book.
|
||||
* @param string $type
|
||||
* @param string $filterType
|
||||
* @param int $page
|
||||
* @param int $pageSize
|
||||
* @param string $searchTerm
|
||||
* @param int|null $uploadedTo
|
||||
* @param string|null $search
|
||||
* @return array
|
||||
*/
|
||||
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
|
||||
public function getEntityFiltered(
|
||||
string $type,
|
||||
string $filterType = null,
|
||||
int $page = 0,
|
||||
int $pageSize = 24,
|
||||
int $uploadedTo = null,
|
||||
string $search = null
|
||||
)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
}
|
||||
$contextPage = $this->page->findOrFail($uploadedTo);
|
||||
$parentFilter = null;
|
||||
|
||||
/**
|
||||
* Get gallery images with a particular filter criteria such as
|
||||
* being within the current book or page.
|
||||
* @param $filter
|
||||
* @param $pageId
|
||||
* @param int $pageNum
|
||||
* @param int $pageSize
|
||||
* @return array
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->image->where('type', '=', 'gallery');
|
||||
|
||||
$page = $this->page->findOrFail($pageId);
|
||||
|
||||
if ($filter === 'page') {
|
||||
$images = $images->where('uploaded_to', '=', $page->id);
|
||||
} elseif ($filter === 'book') {
|
||||
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
||||
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||
if ($filterType === 'book' || $filterType === 'page') {
|
||||
$parentFilter = function(Builder $query) use ($filterType, $contextPage) {
|
||||
if ($filterType === 'page') {
|
||||
$query->where('uploaded_to', '=', $contextPage->id);
|
||||
} elseif ($filterType === 'book') {
|
||||
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
|
||||
$query->whereIn('uploaded_to', $validPageIds);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $pageNum, $pageSize);
|
||||
return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new image into storage and return the new image.
|
||||
* @param UploadedFile $uploadFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @param int|null $resizeWidth
|
||||
* @param int|null $resizeHeight
|
||||
* @param bool $keepRatio
|
||||
* @return Image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
|
||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true)
|
||||
{
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
|
||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
|
||||
$this->loadThumbs($image);
|
||||
return $image;
|
||||
}
|
||||
@ -175,12 +201,27 @@ class ImageRepo
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroyImage(Image $image)
|
||||
public function destroyImage(Image $image = null)
|
||||
{
|
||||
$this->imageService->destroy($image);
|
||||
if ($image) {
|
||||
$this->imageService->destroy($image);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all images of a certain type.
|
||||
* @param string $imageType
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroyByType(string $imageType)
|
||||
{
|
||||
$images = $this->image->where('type', '=', $imageType)->get();
|
||||
foreach ($images as $image) {
|
||||
$this->destroyImage($image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load thumbnails onto an image object.
|
||||
@ -191,8 +232,8 @@ class ImageRepo
|
||||
protected function loadThumbs(Image $image)
|
||||
{
|
||||
$image->thumbs = [
|
||||
'gallery' => $this->getThumbnail($image, 150, 150),
|
||||
'display' => $this->getThumbnail($image, 840, 0, true)
|
||||
'gallery' => $this->getThumbnail($image, 150, 150, false),
|
||||
'display' => $this->getThumbnail($image, 840, null, true)
|
||||
];
|
||||
}
|
||||
|
||||
@ -208,7 +249,7 @@ class ImageRepo
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
try {
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
@ -232,13 +273,11 @@ class ImageRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the provided image type is valid.
|
||||
* @param $type
|
||||
* @return bool
|
||||
* Get the validation rules for image files.
|
||||
* @return string
|
||||
*/
|
||||
public function isValidType($type)
|
||||
public function getImageValidationRules()
|
||||
{
|
||||
$validTypes = ['gallery', 'cover', 'system', 'user'];
|
||||
return in_array($type, $validTypes);
|
||||
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use phpDocumentor\Reflection\Types\Integer;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService extends UploadService
|
||||
@ -57,15 +58,30 @@ class ImageService extends UploadService
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @param int|null $resizeWidth
|
||||
* @param int|null $resizeHeight
|
||||
* @param bool $keepRatio
|
||||
* @return mixed
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
|
||||
public function saveNewFromUpload(
|
||||
UploadedFile $uploadedFile,
|
||||
string $type,
|
||||
int $uploadedTo = 0,
|
||||
int $resizeWidth = null,
|
||||
int $resizeHeight = null,
|
||||
bool $keepRatio = true
|
||||
)
|
||||
{
|
||||
$imageName = $uploadedFile->getClientOriginalName();
|
||||
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
if ($resizeWidth !== null || $resizeHeight !== null) {
|
||||
$imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
|
||||
}
|
||||
|
||||
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||
}
|
||||
|
||||
@ -122,7 +138,7 @@ class ImageService extends UploadService
|
||||
$secureUploads = setting('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
|
||||
|
||||
while ($storage->exists($imagePath . $imageName)) {
|
||||
$imageName = str_random(3) . $imageName;
|
||||
@ -201,8 +217,28 @@ class ImageService extends UploadService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
|
||||
|
||||
$storage->put($thumbFilePath, $thumbData);
|
||||
$storage->setVisibility($thumbFilePath, 'public');
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize image data.
|
||||
* @param string $imageData
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
|
||||
{
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($imagePath));
|
||||
$thumb = $this->imageTool->make($imageData);
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
@ -211,20 +247,14 @@ class ImageService extends UploadService
|
||||
}
|
||||
|
||||
if ($keepRatio) {
|
||||
$thumb->resize($width, null, function ($constraint) {
|
||||
$thumb->resize($width, $height, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
} else {
|
||||
$thumb->fit($width, $height);
|
||||
}
|
||||
|
||||
$thumbData = (string)$thumb->encode();
|
||||
$storage->put($thumbFilePath, $thumbData);
|
||||
$storage->setVisibility($thumbFilePath, 'public');
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
return (string)$thumb->encode();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,6 +336,7 @@ class ImageService extends UploadService
|
||||
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
$image->updated_by = $user->id;
|
||||
$image->uploaded_to = $user->id;
|
||||
$image->save();
|
||||
|
||||
return $image;
|
||||
|
@ -4,54 +4,50 @@ class ImagePicker {
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.imageElem = elem.querySelector('img');
|
||||
this.input = elem.querySelector('input');
|
||||
this.imageInput = elem.querySelector('input[type=file]');
|
||||
this.resetInput = elem.querySelector('input[data-reset-input]');
|
||||
this.removeInput = elem.querySelector('input[data-remove-input]');
|
||||
|
||||
this.isUsingIds = elem.getAttribute('data-current-id') !== '';
|
||||
this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width');
|
||||
this.isResizeCropping = elem.getAttribute('data-resize-crop') !== '';
|
||||
this.defaultImage = elem.getAttribute('data-default-image');
|
||||
|
||||
let selectButton = elem.querySelector('button[data-action="show-image-manager"]');
|
||||
selectButton.addEventListener('click', this.selectImage.bind(this));
|
||||
|
||||
let resetButton = elem.querySelector('button[data-action="reset-image"]');
|
||||
const resetButton = elem.querySelector('button[data-action="reset-image"]');
|
||||
resetButton.addEventListener('click', this.reset.bind(this));
|
||||
|
||||
let removeButton = elem.querySelector('button[data-action="remove-image"]');
|
||||
const removeButton = elem.querySelector('button[data-action="remove-image"]');
|
||||
if (removeButton) {
|
||||
removeButton.addEventListener('click', this.removeImage.bind(this));
|
||||
}
|
||||
|
||||
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
|
||||
}
|
||||
|
||||
selectImage() {
|
||||
window.ImageManager.show(image => {
|
||||
if (!this.isResizing) {
|
||||
this.setImage(image);
|
||||
return;
|
||||
}
|
||||
fileInputChange() {
|
||||
this.resetInput.setAttribute('disabled', 'disabled');
|
||||
if (this.removeInput) {
|
||||
this.removeInput.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false');
|
||||
|
||||
window.$http.get(window.baseUrl(requestString)).then(resp => {
|
||||
image.url = resp.data.url;
|
||||
this.setImage(image);
|
||||
});
|
||||
});
|
||||
for (let file of this.imageInput.files) {
|
||||
this.imageElem.src = window.URL.createObjectURL(file);
|
||||
}
|
||||
this.imageElem.classList.remove('none');
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')});
|
||||
}
|
||||
|
||||
setImage(image) {
|
||||
this.imageElem.src = image.url;
|
||||
this.input.value = this.isUsingIds ? image.id : image.url;
|
||||
this.imageInput.value = '';
|
||||
this.imageElem.src = this.defaultImage;
|
||||
this.resetInput.removeAttribute('disabled');
|
||||
if (this.removeInput) {
|
||||
this.removeInput.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
this.imageElem.classList.remove('none');
|
||||
}
|
||||
|
||||
removeImage() {
|
||||
this.imageElem.src = this.elem.getAttribute('data-default-image');
|
||||
this.imageInput.value = '';
|
||||
this.imageElem.classList.add('none');
|
||||
this.input.value = 'none';
|
||||
this.removeInput.removeAttribute('disabled');
|
||||
this.resetInput.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -394,9 +394,7 @@ class MarkdownEditor {
|
||||
const drawingId = imgContainer.getAttribute('drawio-diagram');
|
||||
|
||||
DrawIO.show(() => {
|
||||
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
|
||||
return `data:image/png;base64,${resp.data.content}`;
|
||||
});
|
||||
return DrawIO.load(drawingId);
|
||||
}, (pngData) => {
|
||||
|
||||
let data = {
|
||||
|
@ -257,39 +257,38 @@ function drawIoPlugin() {
|
||||
DrawIO.show(drawingInit, updateContent);
|
||||
}
|
||||
|
||||
function updateContent(pngData) {
|
||||
let id = "image-" + Math.random().toString(16).slice(2);
|
||||
let loadingImage = window.baseUrl('/loading.gif');
|
||||
let data = {
|
||||
image: pngData,
|
||||
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
|
||||
};
|
||||
async function updateContent(pngData) {
|
||||
const id = "image-" + Math.random().toString(16).slice(2);
|
||||
const loadingImage = window.baseUrl('/loading.gif');
|
||||
const pageId = Number(document.getElementById('page-editor').getAttribute('page-id'));
|
||||
|
||||
// Handle updating an existing image
|
||||
if (currentNode) {
|
||||
DrawIO.close();
|
||||
let imgElem = currentNode.querySelector('img');
|
||||
window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => {
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id);
|
||||
}).catch(err => {
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, pageId);
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||
} catch (err) {
|
||||
window.$events.emit('error', trans('errors.image_upload_error'));
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
|
||||
DrawIO.close();
|
||||
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
|
||||
pageEditor.dom.setAttrib(id, 'src', resp.data.url);
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id);
|
||||
}).catch(err => {
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, pageId);
|
||||
pageEditor.dom.setAttrib(id, 'src', img.url);
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||
} catch (err) {
|
||||
pageEditor.dom.remove(id);
|
||||
window.$events.emit('error', trans('errors.image_upload_error'));
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}, 5);
|
||||
}
|
||||
|
||||
@ -300,9 +299,7 @@ function drawIoPlugin() {
|
||||
}
|
||||
|
||||
let drawingId = currentNode.getAttribute('drawio-diagram');
|
||||
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
|
||||
return `data:image/png;base64,${resp.data.content}`;
|
||||
});
|
||||
return DrawIO.load(drawingId);
|
||||
}
|
||||
|
||||
window.tinymce.PluginManager.add('drawio', function(editor, url) {
|
||||
|
@ -66,4 +66,23 @@ function drawPostMessage(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};
|
@ -1,7 +1,7 @@
|
||||
import * as Dates from "../services/dates";
|
||||
import dropzone from "./components/dropzone";
|
||||
|
||||
let page = 0;
|
||||
let page = 1;
|
||||
let previousClickTime = 0;
|
||||
let previousClickImage = 0;
|
||||
let dataLoaded = false;
|
||||
@ -20,7 +20,7 @@ const data = {
|
||||
selectedImage: false,
|
||||
dependantPages: false,
|
||||
showing: false,
|
||||
view: 'all',
|
||||
filter: null,
|
||||
hasMore: false,
|
||||
searching: false,
|
||||
searchTerm: '',
|
||||
@ -56,32 +56,37 @@ const methods = {
|
||||
this.$el.children[0].components.overlay.hide();
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
let url = baseUrl + page;
|
||||
let query = {};
|
||||
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
|
||||
if (this.searching) query.term = this.searchTerm;
|
||||
async fetchData() {
|
||||
let query = {
|
||||
page,
|
||||
search: this.searching ? this.searchTerm : null,
|
||||
uploaded_to: this.uploadedTo || null,
|
||||
filter_type: this.filter,
|
||||
};
|
||||
|
||||
this.$http.get(url, {params: query}).then(response => {
|
||||
this.images = this.images.concat(response.data.images);
|
||||
this.hasMore = response.data.hasMore;
|
||||
page++;
|
||||
});
|
||||
const {data} = await this.$http.get(baseUrl, {params: query});
|
||||
this.images = this.images.concat(data.images);
|
||||
this.hasMore = data.has_more;
|
||||
page++;
|
||||
},
|
||||
|
||||
setView(viewName) {
|
||||
this.view = viewName;
|
||||
setFilterType(filterType) {
|
||||
this.filter = filterType;
|
||||
this.resetState();
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
resetState() {
|
||||
this.cancelSearch();
|
||||
this.resetListView();
|
||||
this.deleteConfirm = false;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}`);
|
||||
},
|
||||
|
||||
resetListView() {
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
this.deleteConfirm = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`);
|
||||
page = 1;
|
||||
},
|
||||
|
||||
searchImages() {
|
||||
@ -94,10 +99,7 @@ const methods = {
|
||||
}
|
||||
|
||||
this.searching = true;
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
|
||||
this.resetListView();
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
@ -110,10 +112,10 @@ const methods = {
|
||||
},
|
||||
|
||||
imageSelect(image) {
|
||||
let dblClickTime = 300;
|
||||
let currentTime = Date.now();
|
||||
let timeDiff = currentTime - previousClickTime;
|
||||
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
||||
const dblClickTime = 300;
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - previousClickTime;
|
||||
const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
||||
|
||||
if (isDblClick) {
|
||||
this.callbackAndHide(image);
|
||||
@ -132,11 +134,11 @@ const methods = {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
saveImageDetails() {
|
||||
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
|
||||
this.$http.put(url, this.selectedImage).then(response => {
|
||||
this.$events.emit('success', trans('components.image_update_success'));
|
||||
}).catch(error => {
|
||||
async saveImageDetails() {
|
||||
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
||||
try {
|
||||
await this.$http.put(url, this.selectedImage)
|
||||
} catch (error) {
|
||||
if (error.response.status === 422) {
|
||||
let errors = error.response.data;
|
||||
let message = '';
|
||||
@ -145,27 +147,29 @@ const methods = {
|
||||
});
|
||||
this.$events.emit('error', message);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteImage() {
|
||||
async deleteImage() {
|
||||
|
||||
if (!this.deleteConfirm) {
|
||||
let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
|
||||
this.$http.get(url).then(resp => {
|
||||
this.dependantPages = resp.data;
|
||||
}).catch(console.error).then(() => {
|
||||
this.deleteConfirm = true;
|
||||
});
|
||||
const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
|
||||
try {
|
||||
const {data} = await this.$http.get(url);
|
||||
this.dependantPages = data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
this.deleteConfirm = true;
|
||||
return;
|
||||
}
|
||||
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
||||
this.$http.delete(url).then(resp => {
|
||||
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
||||
this.selectedImage = false;
|
||||
this.$events.emit('success', trans('components.image_delete_success'));
|
||||
this.deleteConfirm = false;
|
||||
});
|
||||
|
||||
const url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
||||
await this.$http.delete(url);
|
||||
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
||||
this.selectedImage = false;
|
||||
this.$events.emit('success', trans('components.image_delete_success'));
|
||||
this.deleteConfirm = false;
|
||||
},
|
||||
|
||||
getDate(stringDate) {
|
||||
@ -180,7 +184,7 @@ const methods = {
|
||||
|
||||
const computed = {
|
||||
uploadUrl() {
|
||||
return window.baseUrl(`/images/${this.imageType}/upload`);
|
||||
return window.baseUrl(`/images/${this.imageType}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -188,7 +192,7 @@ function mounted() {
|
||||
window.ImageManager = this;
|
||||
this.imageType = this.$el.getAttribute('image-type');
|
||||
this.uploadedTo = this.$el.getAttribute('uploaded-to');
|
||||
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
|
||||
baseUrl = window.baseUrl('/images/' + this.imageType)
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -251,7 +251,7 @@ input[type=date] {
|
||||
}
|
||||
|
||||
.form-group {
|
||||
.text-pos, .text-neg {
|
||||
div.text-pos, div.text-neg, p.text-post, p.text-neg {
|
||||
padding: $-xs 0;
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +140,10 @@ body.flexbox {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.float {
|
||||
float: left;
|
||||
&.right {
|
||||
|
@ -71,6 +71,7 @@ return [
|
||||
'timezone' => 'The :attribute must be a valid zone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'url' => 'The :attribute format is invalid.',
|
||||
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
|
||||
|
||||
// Custom validation lines
|
||||
'custom' => [
|
||||
|
@ -33,5 +33,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
@stop
|
@ -16,12 +16,10 @@
|
||||
|
||||
<div class="content-wrap card">
|
||||
<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">
|
||||
@include('books.form', ['model' => $book])
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
@stop
|
@ -18,13 +18,9 @@
|
||||
<p class="small">{{ trans('common.cover_image_description') }}</p>
|
||||
|
||||
@include('components.image-picker', [
|
||||
'resizeHeight' => '512',
|
||||
'resizeWidth' => '512',
|
||||
'showRemove' => false,
|
||||
'defaultImage' => baseUrl('/book_default_cover.png'),
|
||||
'currentImage' => isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
|
||||
'currentId' => isset($model) && $model->image_id ? $model->image_id : 0,
|
||||
'name' => 'image_id',
|
||||
'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : baseUrl('/book_default_cover.png') ,
|
||||
'name' => 'image',
|
||||
'imageClass' => 'cover'
|
||||
])
|
||||
</div>
|
||||
|
@ -10,12 +10,12 @@
|
||||
<div class="flex-fill image-manager-body">
|
||||
|
||||
<div class="image-manager-content">
|
||||
<div v-if="imageType === 'gallery'" 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_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_page_title') }}" :class="{selected: (view=='page')}" @click="setView('page')">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</div>
|
||||
<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: !filter}" @click="setFilterType(null)">@icon('images') {{ trans('components.image_all') }}</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: (filter=='page')}" @click="setFilterType('page')">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</div>
|
||||
</div>
|
||||
<div v-show="view === 'all'" >
|
||||
<div>
|
||||
<form @submit.prevent="searchImages" class="contained-search-box">
|
||||
<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>
|
||||
@ -63,7 +63,7 @@
|
||||
<button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button>
|
||||
|
||||
</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') }}
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
|
@ -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="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') }}">
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
|
||||
@endif
|
||||
</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>
|
@ -79,7 +79,7 @@
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<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() !!}
|
||||
|
||||
<div class="setting-list">
|
||||
@ -119,14 +119,12 @@
|
||||
</div>
|
||||
<div>
|
||||
@include('components.image-picker', [
|
||||
'resizeHeight' => '43',
|
||||
'resizeWidth' => '200',
|
||||
'showRemove' => true,
|
||||
'removeName' => 'setting-app-logo',
|
||||
'removeValue' => 'none',
|
||||
'defaultImage' => baseUrl('/logo.png'),
|
||||
'currentImage' => setting('app-logo'),
|
||||
'name' => 'setting-app-logo',
|
||||
'name' => 'app_logo',
|
||||
'imageClass' => 'logo-image',
|
||||
'currentId' => false
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,6 +26,4 @@
|
||||
|
||||
</div>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
|
||||
@stop
|
@ -16,12 +16,11 @@
|
||||
|
||||
<div class="card content-wrap">
|
||||
<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">
|
||||
@include('shelves.form', ['model' => $shelf])
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'cover'])
|
||||
@stop
|
@ -53,13 +53,9 @@
|
||||
<p class="small">{{ trans('common.cover_image_description') }}</p>
|
||||
|
||||
@include('components.image-picker', [
|
||||
'resizeHeight' => '512',
|
||||
'resizeWidth' => '512',
|
||||
'showRemove' => false,
|
||||
'defaultImage' => baseUrl('/book_default_cover.png'),
|
||||
'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') ,
|
||||
'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0,
|
||||
'name' => 'image_id',
|
||||
'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') ,
|
||||
'name' => 'image',
|
||||
'imageClass' => 'cover'
|
||||
])
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<div class="card content-wrap">
|
||||
<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() !!}
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
'defaultImage' => baseUrl('/user_avatar.png'),
|
||||
'currentImage' => $user->getAvatar(80),
|
||||
'currentId' => $user->image_id,
|
||||
'name' => 'image_id',
|
||||
'name' => 'profile_image',
|
||||
'imageClass' => 'avatar large'
|
||||
])
|
||||
</div>
|
||||
@ -87,5 +87,4 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@include('components.image-manager', ['imageType' => 'user'])
|
||||
@stop
|
||||
|
@ -6,7 +6,8 @@ Route::get('/robots.txt', 'HomeController@getRobots');
|
||||
// Authenticated routes...
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
Route::get('/uploads/images/{path}', 'ImageController@showImage')
|
||||
// Secure images routing
|
||||
Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
|
||||
->where('path', '.*$');
|
||||
|
||||
Route::group(['prefix' => 'pages'], function() {
|
||||
@ -103,22 +104,21 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/user/{userId}', 'UserController@showProfilePage');
|
||||
|
||||
// Image routes
|
||||
Route::group(['prefix' => 'images'], function() {
|
||||
// Get for user images
|
||||
Route::get('/user/all', 'ImageController@getAllForUserType');
|
||||
Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
|
||||
// Standard get, update and deletion for all types
|
||||
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
|
||||
Route::get('/base64/{id}', 'ImageController@getBase64Image');
|
||||
Route::put('/update/{imageId}', 'ImageController@update');
|
||||
Route::post('/drawing/upload', 'ImageController@uploadDrawing');
|
||||
Route::get('/usage/{id}', 'ImageController@usage');
|
||||
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
||||
Route::get('/{type}/all', 'ImageController@getAllByType');
|
||||
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
||||
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
|
||||
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
|
||||
Route::delete('/{id}', 'ImageController@destroy');
|
||||
Route::group(['prefix' => 'images'], function () {
|
||||
|
||||
// Gallery
|
||||
Route::get('/gallery', 'Images\GalleryImageController@list');
|
||||
Route::post('/gallery', 'Images\GalleryImageController@create');
|
||||
|
||||
// Drawio
|
||||
Route::get('/drawio', 'Images\DrawioImageController@list');
|
||||
Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64');
|
||||
Route::post('/drawio', 'Images\DrawioImageController@create');
|
||||
|
||||
// Shared gallery & draw.io endpoint
|
||||
Route::get('/usage/{id}', 'Images\ImageController@usage');
|
||||
Route::put('/{id}', 'Images\ImageController@update');
|
||||
Route::delete('/{id}', 'Images\ImageController@destroy');
|
||||
});
|
||||
|
||||
// Attachments routes
|
||||
|
@ -17,14 +17,10 @@ class ImageTest extends TestCase
|
||||
$admin = $this->getAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$imageName = 'first-image.png';
|
||||
$relPath = $this->getTestImagePath('gallery', $imageName);
|
||||
$this->deleteImage($relPath);
|
||||
$imgDetails = $this->uploadGalleryImage($page);
|
||||
$relPath = $imgDetails['path'];
|
||||
|
||||
$upload = $this->uploadImage($imageName, $page->id);
|
||||
$upload->assertStatus(200);
|
||||
|
||||
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath));
|
||||
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath));
|
||||
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
@ -35,10 +31,93 @@ class ImageTest extends TestCase
|
||||
'path' => $relPath,
|
||||
'created_by' => $admin->id,
|
||||
'updated_by' => $admin->id,
|
||||
'name' => $imageName
|
||||
'name' => $imgDetails['name'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_image_edit()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$imgDetails = $this->uploadGalleryImage();
|
||||
$image = Image::query()->first();
|
||||
|
||||
$newName = str_random();
|
||||
$update = $this->put('/images/' . $image->id, ['name' => $newName]);
|
||||
$update->assertSuccessful();
|
||||
$update->assertJson([
|
||||
'id' => $image->id,
|
||||
'name' => $newName,
|
||||
'type' => 'gallery',
|
||||
]);
|
||||
|
||||
$this->deleteImage($imgDetails['path']);
|
||||
|
||||
$this->assertDatabaseHas('images', [
|
||||
'type' => 'gallery',
|
||||
'name' => $newName
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_gallery_get_list_format()
|
||||
{
|
||||
$this->asEditor();
|
||||
|
||||
$imgDetails = $this->uploadGalleryImage();
|
||||
$image = Image::query()->first();
|
||||
|
||||
$emptyJson = ['images' => [], 'has_more' => false];
|
||||
$resultJson = [
|
||||
'images' => [
|
||||
[
|
||||
'id' => $image->id,
|
||||
'name' => $imgDetails['name'],
|
||||
]
|
||||
],
|
||||
'has_more' => false,
|
||||
];
|
||||
|
||||
$pageId = $imgDetails['page']->id;
|
||||
$firstPageRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}");
|
||||
$firstPageRequest->assertSuccessful()->assertJson($resultJson);
|
||||
|
||||
$secondPageRequest = $this->get("/images/gallery?page=2&uploaded_to={$pageId}");
|
||||
$secondPageRequest->assertSuccessful()->assertExactJson($emptyJson);
|
||||
|
||||
$namePartial = substr($imgDetails['name'], 0, 3);
|
||||
$searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
|
||||
$searchHitRequest->assertSuccessful()->assertJson($resultJson);
|
||||
|
||||
$namePartial = str_random(16);
|
||||
$searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
|
||||
$searchHitRequest->assertSuccessful()->assertExactJson($emptyJson);
|
||||
}
|
||||
|
||||
public function test_image_usage()
|
||||
{
|
||||
$page = Page::first();
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$imgDetails = $this->uploadGalleryImage($page);
|
||||
|
||||
$image = Image::query()->first();
|
||||
$page->html = '<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()
|
||||
{
|
||||
$page = Page::first();
|
||||
@ -50,7 +129,7 @@ class ImageTest extends TestCase
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
$file = $this->getTestImage($fileName);
|
||||
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload->assertStatus(302);
|
||||
|
||||
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
|
||||
@ -72,7 +151,7 @@ class ImageTest extends TestCase
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
$file = $this->getTestImage($fileName);
|
||||
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload->assertStatus(302);
|
||||
|
||||
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
|
||||
@ -89,7 +168,7 @@ class ImageTest extends TestCase
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
$file = $this->getTestImage($fileName);
|
||||
$upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
|
||||
$upload->assertStatus(302);
|
||||
|
||||
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded double extension file was uploaded but should have been stopped');
|
||||
@ -101,9 +180,9 @@ class ImageTest extends TestCase
|
||||
$this->asEditor();
|
||||
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
|
||||
$page = Page::first();
|
||||
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
|
||||
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png');
|
||||
|
||||
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||
$upload->assertStatus(200);
|
||||
|
||||
$this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath);
|
||||
@ -119,9 +198,9 @@ class ImageTest extends TestCase
|
||||
$this->asEditor();
|
||||
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
|
||||
$page = Page::first();
|
||||
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
|
||||
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png');
|
||||
|
||||
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||
$imageUrl = json_decode($upload->getContent(), true)['url'];
|
||||
$page->html .= "<img src=\"{$imageUrl}\">";
|
||||
$page->save();
|
||||
@ -139,13 +218,12 @@ class ImageTest extends TestCase
|
||||
public function test_system_images_remain_public()
|
||||
{
|
||||
config()->set('filesystems.default', 'local_secure');
|
||||
$this->asEditor();
|
||||
$this->asAdmin();
|
||||
$galleryFile = $this->getTestImage('my-system-test-upload.png');
|
||||
$page = Page::first();
|
||||
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png');
|
||||
$expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png');
|
||||
|
||||
$upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
|
||||
$upload->assertStatus(200);
|
||||
$upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []);
|
||||
$upload->assertRedirect('/settings');
|
||||
|
||||
$this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath);
|
||||
|
||||
@ -183,8 +261,10 @@ class ImageTest extends TestCase
|
||||
|
||||
$this->uploadImage($imageName, $page->id);
|
||||
$image = Image::first();
|
||||
$image->type = 'drawio';
|
||||
$image->save();
|
||||
|
||||
$imageGet = $this->getJson("/images/base64/{$image->id}");
|
||||
$imageGet = $this->getJson("/images/drawio/base64/{$image->id}");
|
||||
$imageGet->assertJson([
|
||||
'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII='
|
||||
]);
|
||||
@ -196,7 +276,7 @@ class ImageTest extends TestCase
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$upload = $this->postJson('images/drawing/upload', [
|
||||
$upload = $this->postJson('images/drawio', [
|
||||
'uploaded_to' => $page->id,
|
||||
'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII='
|
||||
]);
|
||||
@ -217,28 +297,58 @@ class ImageTest extends TestCase
|
||||
$this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected");
|
||||
}
|
||||
|
||||
protected function getTestProfileImage()
|
||||
{
|
||||
$imageName = 'profile.png';
|
||||
$relPath = $this->getTestImagePath('user', $imageName);
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
return $this->getTestImage($imageName);
|
||||
}
|
||||
|
||||
public function test_user_image_upload()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
$admin = $this->getAdmin();
|
||||
$this->actingAs($admin);
|
||||
|
||||
$file = $this->getTestProfileImage();
|
||||
$this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
|
||||
|
||||
$this->assertDatabaseHas('images', [
|
||||
'type' => 'user',
|
||||
'uploaded_to' => $editor->id,
|
||||
'created_by' => $admin->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_user_images_deleted_on_user_deletion()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$imageName = 'profile.png';
|
||||
$relPath = $this->getTestImagePath('gallery', $imageName);
|
||||
$this->deleteImage($relPath);
|
||||
|
||||
$file = $this->getTestImage($imageName);
|
||||
$this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
|
||||
$this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
|
||||
$file = $this->getTestProfileImage();
|
||||
$this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
|
||||
|
||||
$profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get();
|
||||
$this->assertTrue($profileImages->count() === 2, "Found profile images does not match upload count");
|
||||
$this->assertTrue($profileImages->count() === 1, "Found profile images does not match upload count");
|
||||
|
||||
$imagePath = public_path($profileImages->first()->path);
|
||||
$this->assertTrue(file_exists($imagePath));
|
||||
|
||||
$userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}");
|
||||
$userDelete->assertStatus(302);
|
||||
|
||||
$this->assertDatabaseMissing('images', [
|
||||
'type' => 'user',
|
||||
'created_by' => $editor->id
|
||||
]);
|
||||
$this->assertDatabaseMissing('images', [
|
||||
'type' => 'user',
|
||||
'uploaded_to' => $editor->id
|
||||
]);
|
||||
|
||||
$this->assertFalse(file_exists($imagePath));
|
||||
}
|
||||
|
||||
public function test_deleted_unused_images()
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace Tests\Uploads;
|
||||
|
||||
|
||||
use BookStack\Entities\Page;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
trait UsesImages
|
||||
@ -41,7 +42,7 @@ trait UsesImages
|
||||
*/
|
||||
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);
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user