- Now changes the images directly for user, system & cover.
- Extra permission checks added to edit & delete actions.
* 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.
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;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
+ protected $imageRepo;
/**
* BookController constructor.
* @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();
}
* @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;
}
$book = $this->entityRepo->createFromInput('book', $request->all());
+ $this->bookUpdateActions($book, $request);
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
/**
* 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());
}
$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');
}
$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();
+ }
+ }
}
/**
* Store a newly created bookshelf in storage.
- * @param Request $request
+ * @param Request $request
* @return Response
+ * @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
{
$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();
+ }
}
}
+++ /dev/null
-<?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'));
- }
-}
+++ /dev/null
-<?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');
- }
-
-}
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)
+ ]);
+ }
+
}
--- /dev/null
+<?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);
+ }
+ }
+}
+++ /dev/null
-<?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);
- }
-
-}
+++ /dev/null
-<?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);
- }
-
-}
<?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;
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
{
$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) {
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'));
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
+use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
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();
}
$this->userRepo->setUserRoles($user, $roles);
}
+ // TODO - Check this uses new profile assignment
$this->userRepo->downloadAndAssignUserAvatar($user);
return redirect('/settings/users');
/**
* 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)
{
'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);
}
}
+ // 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);
}
<?php namespace BookStack\Uploads;
+use BookStack\Entities\Page;
use BookStack\Ownable;
use Images;
{
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();
+ }
}
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;
/**
* Get an image with the given id.
* @param $id
- * @return mixed
+ * @return Image
*/
public function getById($id)
{
$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);
/**
* 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;
}
* @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.
* @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);
}
}
- /**
- * 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
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
/**
* 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);
}
$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;
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'));
}
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();
}
/**
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));
}
- }
- selectImage() {
- window.ImageManager.show(image => {
- if (!this.isResizing) {
- this.setImage(image);
- return;
- }
+ this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
+ }
- let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false');
+ fileInputChange() {
+ this.resetInput.setAttribute('disabled', 'disabled');
+ if (this.removeInput) {
+ this.removeInput.setAttribute('disabled', 'disabled');
+ }
- 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');
}
}
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 = {
}
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) {
return resp.data;
}
-export default {show, close, upload};
\ No newline at end of file
+/**
+ * Load an existing image, by fetching it as Base64 from the system.
+ * @param drawingId
+ * @returns {Promise<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};
\ No newline at end of file
display: inline-block;
}
+.hidden {
+ display: none;
+}
+
.float {
float: left;
&.right {
'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' => [
</div>
</div>
- @include('components.image-manager', ['imageType' => 'cover'])
@stop
\ No newline at end of file
<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
\ No newline at end of file
<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>
-<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>
\ No newline at end of file
<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">
</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>
</div>
- @include('components.image-manager', ['imageType' => 'cover'])
-
@stop
\ No newline at end of file
<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>
<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>
<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">
'defaultImage' => baseUrl('/user_avatar.png'),
'currentImage' => $user->getAvatar(80),
'currentId' => $user->image_id,
- 'name' => 'image_id',
+ 'name' => 'profile_image',
'imageClass' => 'avatar large'
])
</div>
@endif
</div>
- @include('components.image-manager', ['imageType' => 'user', 'uploaded_to' => $user->id])
@stop
// 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() {
// 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