]> BookStack Code Mirror - bookstack/commitdiff
Added view, deletion and permissions for files
authorDan Brown <redacted>
Mon, 10 Oct 2016 19:30:27 +0000 (20:30 +0100)
committerDan Brown <redacted>
Mon, 10 Oct 2016 19:30:27 +0000 (20:30 +0100)
app/File.php
app/Http/Controllers/FileController.php
app/Services/FileService.php
database/migrations/2016_10_09_142037_create_files_table.php
resources/assets/js/controllers.js
resources/views/pages/form-toolbox.blade.php
resources/views/pages/sidebar-tree-list.blade.php
resources/views/settings/roles/form.blade.php
routes/web.php

index ebfa0a29618a0e7c2a4bda01fc0f98a34206fb9d..055f217bd309efeb464bee8292a8df48a6fc28a6 100644 (file)
@@ -7,12 +7,20 @@ class File extends Ownable
 
     /**
      * Get the page this file was uploaded to.
-     * @return mixed
+     * @return Page
      */
     public function page()
     {
         return $this->belongsTo(Page::class, 'uploaded_to');
     }
 
+    /**
+     * Get the url of this file.
+     * @return string
+     */
+    public function getUrl()
+    {
+        return '/files/' . $this->id;
+    }
 
 }
index b97112c1cf198f117a6f8b4587c0d0a23aa46ac2..e09fb98c6f140b8ff224c5ca46d938bbc906ea0f 100644 (file)
@@ -1,10 +1,7 @@
-<?php
-
-namespace BookStack\Http\Controllers;
+<?php namespace BookStack\Http\Controllers;
 
 use BookStack\Exceptions\FileUploadException;
 use BookStack\File;
-use BookStack\Page;
 use BookStack\Repos\PageRepo;
 use BookStack\Services\FileService;
 use Illuminate\Http\Request;
@@ -37,16 +34,18 @@ class FileController extends Controller
      */
     public function upload(Request $request)
     {
-        // TODO - Add file upload permission check
-        // TODO - ensure user has permission to edit relevant page.
         // TODO - ensure uploads are deleted on page delete.
-
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id'
         ]);
 
-        $uploadedFile = $request->file('file');
         $pageId = $request->get('uploaded_to');
+        $page = $this->pageRepo->getById($pageId);
+
+        $this->checkPermission('file-create-all');
+        $this->checkOwnablePermission('page-update', $page);
+
+        $uploadedFile = $request->file('file');
 
         try {
             $file = $this->fileService->saveNewUpload($uploadedFile, $pageId);
@@ -62,10 +61,10 @@ class FileController extends Controller
      * @param $pageId
      * @return mixed
      */
-    public function getFilesForPage($pageId)
+    public function listForPage($pageId)
     {
-        // TODO - check view permission on page?
         $page = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-view', $page);
         return response()->json($page->files);
     }
 
@@ -75,17 +74,47 @@ class FileController extends Controller
      * @param Request $request
      * @return mixed
      */
-    public function sortFilesForPage($pageId, Request $request)
+    public function sortForPage($pageId, Request $request)
     {
         $this->validate($request, [
             'files' => 'required|array',
             'files.*.id' => 'required|integer',
         ]);
         $page = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-update', $page);
+
         $files = $request->get('files');
         $this->fileService->updateFileOrderWithinPage($files, $pageId);
         return response()->json(['message' => 'File order updated']);
     }
 
+    /**
+     * Get a file from storage.
+     * @param $fileId
+     */
+    public function get($fileId)
+    {
+        $file = $this->file->findOrFail($fileId);
+        $page = $this->pageRepo->getById($file->uploaded_to);
+        $this->checkOwnablePermission('page-view', $page);
 
+        $fileContents = $this->fileService->getFile($file);
+        return response($fileContents, 200, [
+            'Content-Type' => 'application/octet-stream',
+            'Content-Disposition' => 'attachment; filename="'. $file->name .'"'
+        ]);
+    }
+
+    /**
+     * Delete a specific file in the system.
+     * @param $fileId
+     * @return mixed
+     */
+    public function delete($fileId)
+    {
+        $file = $this->file->findOrFail($fileId);
+        $this->checkOwnablePermission($file, 'file-delete');
+        $this->fileService->deleteFile($file);
+        return response()->json(['message' => 'File deleted']);
+    }
 }
index 689fa0600360107c85a7de815d568aa8a7158165..7429f0e6425224c7bb4fbbf5d154fa225e550e66 100644 (file)
@@ -4,12 +4,24 @@
 use BookStack\Exceptions\FileUploadException;
 use BookStack\File;
 use Exception;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Support\Collection;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class FileService extends UploadService
 {
 
+    /**
+     * Get a file from storage.
+     * @param File $file
+     * @return string
+     */
+    public function getFile(File $file)
+    {
+        $filePath = $this->getStorageBasePath() . $file->path;
+        return $this->getStorage()->get($filePath);
+    }
+
     /**
      * Store a new file upon user upload.
      * @param UploadedFile $uploadedFile
@@ -76,4 +88,22 @@ class FileService extends UploadService
         }
     }
 
+    /**
+     * Delete a file and any empty folders the deletion leaves.
+     * @param File $file
+     */
+    public function deleteFile(File $file)
+    {
+        $storedFilePath = $this->getStorageBasePath() . $file->path;
+        $storage = $this->getStorage();
+        $dirPath = dirname($storedFilePath);
+
+        $storage->delete($storedFilePath);
+        if (count($storage->allFiles($dirPath)) === 0) {
+            $storage->deleteDirectory($dirPath);
+        }
+
+        $file->delete();
+    }
+
 }
\ No newline at end of file
index 4eaa86aeb918726afdd22fb354489b5bf7a57b4b..57ddd1202141273ff045900604e51127b0baacf2 100644 (file)
@@ -28,6 +28,26 @@ class CreateFilesTable extends Migration
             $table->index('uploaded_to');
             $table->timestamps();
         });
+
+        // Get roles with permissions we need to change
+        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+
+        // Create & attach new entity permissions
+        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+        $entity = 'File';
+        foreach ($ops as $op) {
+            $permissionId = DB::table('role_permissions')->insertGetId([
+                'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
+                'display_name' => $op . ' ' . $entity . 's',
+                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+            ]);
+            DB::table('permission_role')->insert([
+                'role_id' => $adminRoleId,
+                'permission_id' => $permissionId
+            ]);
+        }
+
     }
 
     /**
@@ -38,5 +58,17 @@ class CreateFilesTable extends Migration
     public function down()
     {
         Schema::dropIfExists('files');
+
+        // Get roles with permissions we need to change
+        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+
+        // Create & attach new entity permissions
+        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+        $entity = 'File';
+        foreach ($ops as $op) {
+            $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
+            $permission = DB::table('role_permissions')->where('name', '=', $permName)->get();
+            DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();
+        }
     }
 }
index 52477a4ad3f7fd18ed6f16cc5502662e7274db9b..b5353e7d9d2b315df46456bf69336b725f2a1cc5 100644 (file)
@@ -575,9 +575,9 @@ module.exports = function (ngApp, events) {
              */
             function getFiles() {
                 let url = window.baseUrl(`/files/get/page/${pageId}`)
-                $http.get(url).then(responseData => {
-                    $scope.files = responseData.data;
-                    currentOrder = responseData.data.map(file => {return file.id}).join(':');
+                $http.get(url).then(resp => {
+                    $scope.files = resp.data;
+                    currentOrder = resp.data.map(file => {return file.id}).join(':');
                 });
             }
             getFiles();
@@ -595,6 +595,17 @@ module.exports = function (ngApp, events) {
                 events.emit('success', 'File uploaded');
             };
 
+            /**
+             * Delete a file from the server and, on success, the local listing.
+             * @param file
+             */
+            $scope.deleteFile = function(file) {
+                  $http.delete(`/files/${file.id}`).then(resp => {
+                      events.emit('success', resp.data.message);
+                      $scope.files.splice($scope.files.indexOf(file), 1);
+                  });
+            };
+
         }]);
 
 };
index 3a344b651827c737ab2fc483e8e7b743e5eb17ef..9ebf223f0a6463b6c6601807c02d895cf6fbc016 100644 (file)
@@ -46,7 +46,7 @@
                 <tr ng-repeat="file in files track by $index">
                     <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
                     <td ng-bind="file.name"></td>
-                    <td width="10" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
+                    <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
                 </tr>
                 </tbody>
             </table>
index 5fcec8731ee098620366f836c346551a35530c2c..fa9cc84aa9e2a62a59a7e835fd2516f4fae03b1a 100644 (file)
@@ -1,6 +1,15 @@
 
 <div class="book-tree" ng-non-bindable>
 
+    @if ($page->files->count() > 0)
+        <h6 class="text-muted">Attachments</h6>
+        @foreach($page->files as $file)
+            <div class="attachment">
+                <a href="{{ $file->getUrl() }}"><i class="zmdi zmdi-file"></i> {{ $file->name }}</a>
+            </div>
+        @endforeach
+    @endif
+
     @if (isset($pageNav) && $pageNav)
         <h6 class="text-muted">Page Navigation</h6>
         <div class="sidebar-page-nav menu">
@@ -10,8 +19,6 @@
                 </li>
             @endforeach
         </div>
-
-
     @endif
 
     <h6 class="text-muted">Book Navigation</h6>
index 5e653f8de0525975e1f375243fed25c9bf9d0fe9..4f1987c0387def5b9488404405693be3eb91bd71 100644 (file)
                             <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
                         </td>
                     </tr>
+                    <tr>
+                        <td>Attached <br>Files</td>
+                        <td>@include('settings/roles/checkbox', ['permission' => 'file-create-all'])</td>
+                        <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
+                        <td>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-update-own']) Own</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-update-all']) All</label>
+                        </td>
+                        <td>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-own']) Own</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-all']) All</label>
+                        </td>
+                    </tr>
                 </table>
             </div>
         </div>
index 7e05af48379ccb5bf1e06ba8398ecc8d8b0c5a70..514f82f99e9158252fcd1da9fb601e685b12690f 100644 (file)
@@ -88,9 +88,11 @@ Route::group(['middleware' => 'auth'], function () {
     });
 
     // File routes
+    Route::get('/files/{id}', 'FileController@get');
     Route::post('/files/upload', 'FileController@upload');
-    Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage');
-    Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage');
+    Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
+    Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
+    Route::delete('/files/{id}', 'FileController@delete');
 
     // AJAX routes
     Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');