]> BookStack Code Mirror - bookstack/commitdiff
Started rewriting back-end image managment
authorDan Brown <redacted>
Sun, 21 Apr 2019 14:52:29 +0000 (15:52 +0100)
committerDan Brown <redacted>
Sun, 21 Apr 2019 14:52:29 +0000 (15:52 +0100)
app/Auth/UserRepo.php
app/Http/Controllers/ImageController.php
app/Uploads/ImageRepo.php
database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php [new file with mode: 0644]
resources/assets/js/vues/image-manager.js
resources/views/users/edit.blade.php
routes/web.php
tests/Uploads/ImageTest.php

index 1a73d0072fb8a51fb5059d98d4943f62205d0fa7..94ebb27ab7c2eb927cfc93195b9f1b5f5fc3e130 100644 (file)
@@ -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);
         }
index 4d6f759b336eddb97e0516c6ddb69eb8034c9639..ae2d743052fe1b1e70e014f16dbb2e270c964caa 100644 (file)
@@ -45,13 +45,21 @@ class ImageController extends Controller
 
     /**
      * Get all images for a specific type, Paginated
+     * @param Request $request
      * @param string $type
      * @param int $page
      * @return \Illuminate\Http\JsonResponse
      */
-    public function getAllByType($type, $page = 0)
+    public function getAllByType(Request $request, $type, $page = 0)
     {
-        $imgData = $this->imageRepo->getPaginatedByType($type, $page);
+        $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);
     }
 
@@ -73,17 +81,6 @@ class ImageController extends Controller
         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
@@ -94,7 +91,7 @@ class ImageController extends Controller
     public function getGalleryFiltered(Request $request, $filter, $page = 0)
     {
         $this->validate($request, [
-            'page_id' => 'required|integer'
+            'uploaded_to' => 'required|integer'
         ]);
 
         $validFilters = collect(['page', 'book']);
@@ -102,35 +99,50 @@ class ImageController extends Controller
             return response('Invalid filter', 500);
         }
 
-        $pageId = $request->get('page_id');
+        $pageId = $request->get('uploaded_to');
         $imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
 
         return response()->json($imgData);
     }
 
+    public function uploadGalleryImage(Request $request)
+    {
+        // TODO
+    }
+
+    public function uploadUserImage(Request $request)
+    {
+        // TODO
+    }
+
+    public function uploadSystemImage(Request $request)
+    {
+        // TODO
+    }
+
+    public function uploadCoverImage(Request $request)
+    {
+        // TODO
+    }
+
     /**
-     * Handles image uploads for use on pages.
-     * @param string $type
+     * Upload a draw.io image into the system.
      * @param Request $request
-     * @return \Illuminate\Http\JsonResponse
-     * @throws \Exception
+     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
      */
-    public function uploadByType($type, Request $request)
+    public function uploadDrawioImage(Request $request)
     {
-        $this->checkPermission('image-create-all');
         $this->validate($request, [
-            'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
+            'image' => 'required|string',
+            'uploaded_to' => 'required|integer'
         ]);
-
-        if (!$this->imageRepo->isValidType($type)) {
-            return $this->jsonError(trans('errors.image_upload_type_error'));
-        }
-
-        $imageUpload = $request->file('file');
+        $uploadedTo = $request->get('uploaded_to', 0);
+        $page = $this->
+        $this->checkPermission('image-create-all');
+        $imageBase64Data = $request->get('image');
 
         try {
-            $uploadedTo = $request->get('uploaded_to', 0);
-            $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
+            $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
         } catch (ImageUploadException $e) {
             return response($e->getMessage(), 500);
         }
@@ -139,29 +151,40 @@ class ImageController extends Controller
     }
 
     /**
-     * Upload a drawing to the system.
+     * Handles image uploads for use on pages.
+     * @param string $type
      * @param Request $request
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+     * @return \Illuminate\Http\JsonResponse
+     * @throws \Exception
      */
-    public function uploadDrawing(Request $request)
+    public function uploadByType($type, Request $request)
     {
+        $this->checkPermission('image-create-all');
         $this->validate($request, [
-            'image' => 'required|string',
-            'uploaded_to' => 'required|integer'
+            'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
         ]);
-        $this->checkPermission('image-create-all');
-        $imageBase64Data = $request->get('image');
+
+        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->saveDrawing($imageBase64Data, $uploadedTo);
+
+            // 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
@@ -199,19 +222,21 @@ class ImageController extends Controller
 
     /**
      * Update image details
-     * @param integer $imageId
+     * @param integer $id
      * @param Request $request
      * @return \Illuminate\Http\JsonResponse
      * @throws ImageUploadException
      * @throws \Exception
      */
-    public function update($imageId, Request $request)
+    public function update($id, Request $request)
     {
         $this->validate($request, [
             'name' => 'required|min:2|string'
         ]);
-        $image = $this->imageRepo->getById($imageId);
+
+        $image = $this->imageRepo->getById($id);
         $this->checkOwnablePermission('image-update', $image);
+
         $image = $this->imageRepo->updateImageDetails($image, $request->all());
         return response()->json($image);
     }
index 0ef8cad48020dcd8314b19669e9a3f4e2567eb4e..c13d995bdae266f6e2fedbb2e4bb5d04e2d3ce3c 100644 (file)
@@ -2,6 +2,7 @@
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Page;
+use BookStack\Http\Requests\Request;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class ImageRepo
@@ -44,12 +45,12 @@ class ImageRepo
      * @param $query
      * @param int $page
      * @param int $pageSize
+     * @param bool $filterOnPage
      * @return array
      */
     private function returnPaginated($query, $page = 0, $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)->take($pageSize + 1)->get();
         $hasMore = count($images) > $pageSize;
 
         $returnImages = $images->take(24);
@@ -68,15 +69,20 @@ class ImageRepo
      * @param string $type
      * @param int $page
      * @param int $pageSize
-     * @param bool|int $userFilter
+     * @param int $uploadedTo
      * @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)
     {
-        $images = $this->image->where('type', '=', strtolower($type));
+        $images = $this->image->newQuery()->where('type', '=', strtolower($type));
+
+        if ($uploadedTo !== null) {
+            $images = $images->where('uploaded_to', '=', $uploadedTo);
+        }
 
-        if ($userFilter !== false) {
-            $images = $images->where('created_by', '=', $userFilter);
+        // Filter by page access if gallery
+        if ($type === 'gallery') {
+            $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to');
         }
 
         return $this->returnPaginated($images, $page, $pageSize);
@@ -90,9 +96,17 @@ class ImageRepo
      * @param string $searchTerm
      * @return array
      */
-    public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
+    public function searchPaginatedByType(Request $request, $type, $searchTerm, $page = 0, $pageSize = 24)
     {
-        $images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
+        // TODO - Filter by uploaded_to
+        $images = $this->image->newQuery()
+            ->where('type', '=', strtolower($type))
+            ->where('name', 'LIKE', '%' . $searchTerm . '%');
+
+        if ($type === 'gallery') {
+            $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to');
+        }
+
         return $this->returnPaginated($images, $page, $pageSize);
     }
 
@@ -118,6 +132,7 @@ class ImageRepo
             $images = $images->whereIn('uploaded_to', $validPageIds);
         }
 
+        $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to');
         return $this->returnPaginated($images, $pageNum, $pageSize);
     }
 
diff --git a/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php b/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php
new file mode 100644 (file)
index 0000000..f1d00b3
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class SetUserProfileImagesUploadedTo extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        DB::table('images')
+            ->where('type', '=', 'user')
+            ->update([
+                'uploaded_to' => DB::raw('`created_by`')
+            ]);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        DB::table('images')
+            ->where('type', '=', 'user')
+            ->update([
+                'uploaded_to' => 0
+            ]);
+    }
+}
index 6bfc2662df250a66f6d221308dcd04ebb88d1500..843aae378ace9661436b724ae5382d50c9f0944a 100644 (file)
@@ -59,7 +59,7 @@ const methods = {
     fetchData() {
         let url = baseUrl + page;
         let query = {};
-        if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
+        if (this.uploadedTo !== false) query.uploaded_to = this.uploadedTo;
         if (this.searching) query.term = this.searchTerm;
 
         this.$http.get(url, {params: query}).then(response => {
@@ -133,7 +133,7 @@ const methods = {
     },
 
     saveImageDetails() {
-        let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
+        let url = window.baseUrl(`/images/${this.selectedImage.id}`);
         this.$http.put(url, this.selectedImage).then(response => {
             this.$events.emit('success', trans('components.image_update_success'));
         }).catch(error => {
index b9ad052c7b1d93925a3f8dd529660f5e6e868678..7c8175d9ae0d1aae98f47e9e260c55df6dc28fba 100644 (file)
@@ -87,5 +87,5 @@
         @endif
     </div>
 
-    @include('components.image-manager', ['imageType' => 'user'])
+    @include('components.image-manager', ['imageType' => 'user', 'uploaded_to' => $user->id])
 @stop
index 695f61654a13c97b68f0dba8bf84a2226971ca80..933179aa7ec2d7e2e661c5d4bbcb27c7ba39e3f7 100644 (file)
@@ -105,19 +105,28 @@ Route::group(['middleware' => 'auth'], function () {
     // Image routes
     Route::group(['prefix' => 'images'], function() {
         // Get for user images
-        Route::get('/user/all', 'ImageController@getAllForUserType');
-        Route::get('/user/all/{page}', 'ImageController@getAllForUserType');
+//        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');
+
+        // TODO - Remove use of abstract "Type" variable (Above)
+        // TODO - Clearly define each endpoint so logic for each is clear
+        // TODO - Move into per-type controllers
+        // TODO - Test and fully think about permissions and each stage
+        Route::post('/drawio', 'ImageController@uploadDrawioImage');
+        Route::post('/gallery', 'ImageController@uploadGalleryImage');
+        Route::post('/user', 'ImageController@uploadUserImage');
+        Route::post('/system', 'ImageController@uploadSystemImage');
+        Route::post('/cover', 'ImageController@uploadCoverImage');
+
+        Route::put('/{id}', 'ImageController@update');
         Route::delete('/{id}', 'ImageController@destroy');
     });
 
index 8373a809c2962738a0895301aa64ed3ad4e5198f..c2e21f95c865a8e62df5b24cff77455982b5e640 100644 (file)
@@ -217,18 +217,97 @@ class ImageTest extends TestCase
         $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected");
     }
 
-    public function test_user_images_deleted_on_user_deletion()
+    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('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []);
+
+        $this->assertDatabaseHas('images', [
+            'type' => 'user',
+            'uploaded_to' => $editor->id,
+            'created_by' => $admin->id,
+        ]);
+    }
+
+    public function test_standard_user_with_manage_users_permission_can_view_other_profile_images()
     {
         $editor = $this->getEditor();
+        $this->giveUserPermissions($editor, ['users-manage']);
+
+        $admin = $this->getAdmin();
+
+        $this->actingAs($admin);
+        $file = $this->getTestProfileImage();
+        $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []);
+
+        $expectedJson = [
+            'name' => 'profile.png',
+            'uploaded_to' => $admin->id,
+            'type' => 'user'
+        ];
+
         $this->actingAs($editor);
+        $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id);
+        $adminImagesGet->assertStatus(200)->assertJsonFragment($expectedJson);
 
-        $imageName = 'profile.png';
-        $relPath = $this->getTestImagePath('gallery', $imageName);
-        $this->deleteImage($relPath);
+        $allImagesGet = $this->get("/images/user/all/0");
+        $allImagesGet->assertStatus(200)->assertJsonFragment($expectedJson);
+    }
 
-        $file = $this->getTestImage($imageName);
-        $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
-        $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []);
+    public function test_standard_user_cant_view_other_profile_images()
+    {
+        $editor = $this->getEditor();
+        $admin = $this->getAdmin();
+
+        $this->actingAs($admin);
+        $file = $this->getTestProfileImage();
+        $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []);
+
+        $this->actingAs($editor);
+        $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id);
+        $adminImagesGet->assertStatus(302);
+
+        $allImagesGet = $this->get("/images/user/all/0");
+        $allImagesGet->assertStatus(302);
+    }
+
+    public function test_standard_user_cant_upload_other_profile_images()
+    {
+        $editor = $this->getEditor();
+        $admin = $this->getAdmin();
+
+        $this->actingAs($editor);
+        $file = $this->getTestProfileImage();
+        $upload = $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []);
+        $upload->assertStatus(302);
+
+        $this->assertDatabaseMissing('images', [
+            'type' => 'user',
+            'uploaded_to' => $admin->id,
+        ]);
+    }
+
+    public function test_user_images_deleted_on_user_deletion()
+    {
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+
+        $file = $this->getTestProfileImage();
+        $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []);
+        $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $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");
@@ -239,6 +318,10 @@ class ImageTest extends TestCase
             'type' => 'user',
             'created_by' => $editor->id
         ]);
+        $this->assertDatabaseMissing('images', [
+            'type' => 'user',
+            'uploaded_to' => $editor->id
+        ]);
     }
 
     public function test_deleted_unused_images()