Change image-selector to not use manager

- Now changes the images directly for user, system & cover.
- Extra permission checks added to edit & delete actions.
This commit is contained in:
Dan Brown 2019-05-04 15:48:15 +01:00
parent cb832a2c10
commit 79f6dc00a3
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
30 changed files with 415 additions and 625 deletions

View File

@ -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.

View File

@ -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();
}
}
}

View File

@ -86,8 +86,9 @@ class BookshelfController extends Controller
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
{
@ -284,10 +285,17 @@ class BookshelfController extends Controller
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
// Update the cover image if in request
if ($request->has('image') && userCan('image-create-all')) {
$image = $this->imageRepo->saveNew($request->file('image'), 'cover', $shelf->id);
if ($request->has('image')) {
$newImage = $request->file('image');
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
$shelf->image_id = $image->id;
$shelf->save();
}
if ($request->has('image_reset')) {
$this->imageRepo->destroyImage($shelf->cover);
$shelf->image_id = 0;
$shelf->save();
}
}
}

View File

@ -1,242 +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 Request $request
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAllByType(Request $request, $type, $page = 0)
{
$uploadedToFilter = $request->get('uploaded_to', null);
// For user profile request, check access to user images
if ($type === 'user') {
$this->checkPermissionOrCurrentUser('users-manage', $uploadedToFilter ?? 0);
}
$imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, $uploadedToFilter);
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType(Request $request, $type, $page = 0)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
return response()->json($imgData);
}
public function uploadUserImage(Request $request)
{
// TODO
}
public function uploadSystemImage(Request $request)
{
// TODO
}
public function uploadCoverImage(Request $request)
{
// TODO
}
/**
* Upload a draw.io image into the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawioImage(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$uploadedTo = $request->get('uploaded_to', 0);
$page = $this->
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
]);
if (!$this->imageRepo->isValidType($type)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->get('uploaded_to', 0);
// For user profile request, check access to user images
if ($type === 'user') {
$this->checkPermissionOrCurrentUser('users-manage', $uploadedTo ?? 0);
}
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
* @param $width
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
}
/**
* Update image details
* @param integer $id
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($id, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@ -1,99 +0,0 @@
<?php
namespace BookStack\Http\Controllers\Images;
// TODO - Replace this with entity-level handling
// Since won't be part of image manager handling
// Added some to bookshelf controller already
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class CoverImageController extends Controller
{
protected $imageRepo;
protected $entityRepo;
/**
* CoverImageController constructor.
* @param ImageRepo $imageRepo
* @param EntityRepo $entityRepo
*/
public function __construct(ImageRepo $imageRepo, EntityRepo $entityRepo)
{
$this->imageRepo = $imageRepo;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Get a list of cover images, in a list.
* Can be paged and filtered by entity.
* @param Request $request
* @param string $entity
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request, $entity)
{
if (!$this->isValidEntityTypeForCover($entity)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$type = 'cover_' . $entity;
$imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, null, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new cover image in the system.
* @param Request $request
* @param string $entity
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request, $entity)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => $this->imageRepo->getImageValidationRules(),
'uploaded_to' => 'required|integer'
]);
if (!$this->isValidEntityTypeForCover($entity)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$uploadedTo = $request->get('uploaded_to', 0);
$entityInstance = $this->entityRepo->getById($entity, $uploadedTo);
$this->checkOwnablePermission($entity . '-update', $entityInstance);
try {
$type = 'cover_' . $entity;
$imageUpload = $request->file('file');
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Check if the given entity type is valid entity to have cover images.
* @param string $entityType
* @return bool
*/
protected function isValidEntityTypeForCover(string $entityType)
{
return ($entityType === 'book' || $entityType === 'bookshelf');
}
}

View File

@ -64,4 +64,26 @@ class DrawioImageController extends Controller
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAsBase64($id)
{
$image = $this->imageRepo->getById($id);
$page = $image->getPage();
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
return $this->jsonError("Image data could not be found");
}
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
}

View File

@ -0,0 +1,115 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Update image details
* @param integer $id
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($id, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->checkImagePermission($image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
/**
* Check related page permission and ensure type is drawio or gallery.
* @param Image $image
*/
protected function checkImagePermission(Image $image)
{
if ($image->type !== 'drawio' || $image->type !== 'gallery') {
$this->showPermissionError();
}
$relatedPage = $image->getPage();
if ($relatedPage) {
$this->checkOwnablePermission('page-view', $relatedPage);
}
}
}

View File

@ -1,67 +0,0 @@
<?php
// TODO - Replace this with setting-level handling
// Since won't be part of image manager handling
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class SystemImageController extends Controller
{
protected $imageRepo;
/**
* SystemImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Get a list of system images, in a list.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$this->checkPermission('settings-manage');
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$imgData = $this->imageRepo->getPaginatedByType('system', $page, 24, null, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new system image.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->checkPermission('settings-manage');
$this->validate($request, [
'file' => $this->imageRepo->getImageValidationRules()
]);
try {
$imageUpload = $request->file('file');
$image = $this->imageRepo->saveNew($imageUpload, 'system', 0);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace BookStack\Http\Controllers\Images;
// TODO - Replace this with user-controller-level handling
// Since won't be part of image manager handling
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class UserImageController extends Controller
{
protected $imageRepo;
/**
* UserImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Get a list of user profile images, in a list.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$userId = $request->get('uploaded_to', null);
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $userId, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new user profile image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'uploaded_to' => 'required|integer',
'file' => $this->imageRepo->getImageValidationRules()
]);
$userId = $request->get('uploaded_to', null);
$this->checkPermissionOrCurrentUser('users-manage', $userId);
try {
$imageUpload = $request->file('file');
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
}

View File

@ -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'));

View File

@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -12,16 +13,19 @@ class UserController extends Controller
protected $user;
protected $userRepo;
protected $imageRepo;
/**
* UserController constructor.
* @param User $user
* @param User $user
* @param UserRepo $userRepo
* @param ImageRepo $imageRepo
*/
public function __construct(User $user, UserRepo $userRepo)
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@ -94,6 +98,7 @@ class UserController extends Controller
$this->userRepo->setUserRoles($user, $roles);
}
// TODO - Check this uses new profile assignment
$this->userRepo->downloadAndAssignUserAvatar($user);
return redirect('/settings/users');
@ -121,10 +126,11 @@ class UserController extends Controller
/**
* Update the specified user in storage.
* @param Request $request
* @param int $id
* @param Request $request
* @param int $id
* @return Response
* @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function update(Request $request, $id)
{
@ -136,7 +142,8 @@ class UserController extends Controller
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
]);
$user = $this->userRepo->getById($id);
@ -166,10 +173,23 @@ class UserController extends Controller
}
}
// Save profile image if in request
if ($request->has('profile_image')) {
$imageUpload = $request->file('profile_image');
$this->imageRepo->destroyImage($user->avatar);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
$user->image_id = $image->id;
}
// Delete the profile image if set to
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
}
$user->save();
session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
}

View File

@ -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();
}
}

View File

@ -2,7 +2,6 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Page;
use BookStack\Http\Requests\Request;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -38,7 +37,7 @@ class ImageRepo
/**
* Get an image with the given id.
* @param $id
* @return mixed
* @return Image
*/
public function getById($id)
{
@ -100,16 +99,8 @@ class ImageRepo
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
}
// Filter by page access if gallery
if ($type === 'gallery') {
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
}
// Filter by entity if cover
if (strpos($type, 'cover_') === 0) {
$entityType = explode('_', $type)[1];
$imageQuery = $this->restrictionService->filterRelatedEntity($entityType, $imageQuery, 'images', 'uploaded_to');
}
// Filter by page access
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
if ($whereClause !== null) {
$imageQuery = $imageQuery->where($whereClause);
@ -157,15 +148,17 @@ class ImageRepo
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
return $image;
}
@ -208,12 +201,27 @@ class ImageRepo
* @return bool
* @throws \Exception
*/
public function destroyImage(Image $image)
public function destroyImage(Image $image = null)
{
$this->imageService->destroy($image);
if ($image) {
$this->imageService->destroy($image);
}
return true;
}
/**
* Destroy all images of a certain type.
* @param string $imageType
* @throws \Exception
*/
public function destroyByType(string $imageType)
{
$images = $this->image->where('type', '=', $imageType)->get();
foreach ($images as $image) {
$this->destroyImage($image);
}
}
/**
* Load thumbnails onto an image object.
@ -241,7 +249,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
@ -264,18 +272,6 @@ class ImageRepo
}
}
/**
* Check if the provided image type is valid.
* @param $type
* @return bool
*/
public function isValidType($type)
{
// TODO - To delete?
$validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
}
/**
* Get the validation rules for image files.
* @return string

View File

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

View File

@ -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');
}
}

View File

@ -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 = {

View File

@ -299,9 +299,7 @@ function drawIoPlugin() {
}
let drawingId = currentNode.getAttribute('drawio-diagram');
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
return `data:image/png;base64,${resp.data.content}`;
});
return DrawIO.load(drawingId);
}
window.tinymce.PluginManager.add('drawio', function(editor, url) {

View File

@ -75,4 +75,14 @@ async function upload(imageData, pageUploadedToId) {
return resp.data;
}
export default {show, close, upload};
/**
* Load an existing image, by fetching it as Base64 from the system.
* @param drawingId
* @returns {Promise<string>}
*/
async function load(drawingId) {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
}
export default {show, close, upload, load};

View File

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

View File

@ -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' => [

View File

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

View File

@ -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', 'uploaded_to'])
@stop

View File

@ -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>

View File

@ -1,22 +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}}">
{{-- TODO - Revamp to be custom file upload button, instead of being linked to image manager--}}
{{-- TODO - Remove image manager use where this is used and clean image manager for drawing/gallery use.--}}
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
@endif
</div>

View File

@ -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>

View File

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

View File

@ -16,7 +16,7 @@
<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>

View File

@ -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>

View File

@ -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', 'uploaded_to' => $user->id])
@stop

View File

@ -6,7 +6,8 @@ Route::get('/robots.txt', 'HomeController@getRobots');
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {
Route::get('/uploads/images/{path}', 'ImageController@showImage')
// Secure images routing
Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
->where('path', '.*$');
Route::group(['prefix' => 'pages'], function() {
@ -105,27 +106,19 @@ Route::group(['middleware' => 'auth'], function () {
// Image routes
Route::group(['prefix' => 'images'], function () {
// TODO - Check auth on these
// TODO - Maybe check types for only gallery or drawing
// Standard get, update and deletion for all types
Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail');
Route::get('/base64/{id}', 'ImageController@getBase64Image');
Route::get('/usage/{id}', 'ImageController@usage');
// Gallery
Route::get('/gallery', 'Images\GalleryImageController@list');
Route::post('/gallery', 'Images\GalleryImageController@create');
// Drawio
Route::get('/drawio', 'Images\DrawioImageController@list');
Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64');
Route::post('/drawio', 'Images\DrawioImageController@create');
// TODO - Check auth on these
// TODO - Maybe check types for only gallery or drawing
// Or add to gallery/drawio controllers
Route::put('/{id}', 'ImageController@update');
Route::delete('/{id}', 'ImageController@destroy');
// Shared gallery & draw.io endpoint
Route::get('/usage/{id}', 'Images\ImageController@usage');
Route::put('/{id}', 'Images\ImageController@update');
Route::delete('/{id}', 'Images\ImageController@destroy');
});
// Attachments routes