Dan Brown

Added view, deletion and permissions for files

...@@ -7,12 +7,20 @@ class File extends Ownable ...@@ -7,12 +7,20 @@ class File extends Ownable
7 7
8 /** 8 /**
9 * Get the page this file was uploaded to. 9 * Get the page this file was uploaded to.
10 - * @return mixed 10 + * @return Page
11 */ 11 */
12 public function page() 12 public function page()
13 { 13 {
14 return $this->belongsTo(Page::class, 'uploaded_to'); 14 return $this->belongsTo(Page::class, 'uploaded_to');
15 } 15 }
16 16
17 + /**
18 + * Get the url of this file.
19 + * @return string
20 + */
21 + public function getUrl()
22 + {
23 + return '/files/' . $this->id;
24 + }
17 25
18 } 26 }
......
1 -<?php 1 +<?php namespace BookStack\Http\Controllers;
2 -
3 -namespace BookStack\Http\Controllers;
4 2
5 use BookStack\Exceptions\FileUploadException; 3 use BookStack\Exceptions\FileUploadException;
6 use BookStack\File; 4 use BookStack\File;
7 -use BookStack\Page;
8 use BookStack\Repos\PageRepo; 5 use BookStack\Repos\PageRepo;
9 use BookStack\Services\FileService; 6 use BookStack\Services\FileService;
10 use Illuminate\Http\Request; 7 use Illuminate\Http\Request;
...@@ -37,16 +34,18 @@ class FileController extends Controller ...@@ -37,16 +34,18 @@ class FileController extends Controller
37 */ 34 */
38 public function upload(Request $request) 35 public function upload(Request $request)
39 { 36 {
40 - // TODO - Add file upload permission check
41 - // TODO - ensure user has permission to edit relevant page.
42 // TODO - ensure uploads are deleted on page delete. 37 // TODO - ensure uploads are deleted on page delete.
43 -
44 $this->validate($request, [ 38 $this->validate($request, [
45 'uploaded_to' => 'required|integer|exists:pages,id' 39 'uploaded_to' => 'required|integer|exists:pages,id'
46 ]); 40 ]);
47 41
48 - $uploadedFile = $request->file('file');
49 $pageId = $request->get('uploaded_to'); 42 $pageId = $request->get('uploaded_to');
43 + $page = $this->pageRepo->getById($pageId);
44 +
45 + $this->checkPermission('file-create-all');
46 + $this->checkOwnablePermission('page-update', $page);
47 +
48 + $uploadedFile = $request->file('file');
50 49
51 try { 50 try {
52 $file = $this->fileService->saveNewUpload($uploadedFile, $pageId); 51 $file = $this->fileService->saveNewUpload($uploadedFile, $pageId);
...@@ -62,10 +61,10 @@ class FileController extends Controller ...@@ -62,10 +61,10 @@ class FileController extends Controller
62 * @param $pageId 61 * @param $pageId
63 * @return mixed 62 * @return mixed
64 */ 63 */
65 - public function getFilesForPage($pageId) 64 + public function listForPage($pageId)
66 { 65 {
67 - // TODO - check view permission on page?
68 $page = $this->pageRepo->getById($pageId); 66 $page = $this->pageRepo->getById($pageId);
67 + $this->checkOwnablePermission('page-view', $page);
69 return response()->json($page->files); 68 return response()->json($page->files);
70 } 69 }
71 70
...@@ -75,17 +74,47 @@ class FileController extends Controller ...@@ -75,17 +74,47 @@ class FileController extends Controller
75 * @param Request $request 74 * @param Request $request
76 * @return mixed 75 * @return mixed
77 */ 76 */
78 - public function sortFilesForPage($pageId, Request $request) 77 + public function sortForPage($pageId, Request $request)
79 { 78 {
80 $this->validate($request, [ 79 $this->validate($request, [
81 'files' => 'required|array', 80 'files' => 'required|array',
82 'files.*.id' => 'required|integer', 81 'files.*.id' => 'required|integer',
83 ]); 82 ]);
84 $page = $this->pageRepo->getById($pageId); 83 $page = $this->pageRepo->getById($pageId);
84 + $this->checkOwnablePermission('page-update', $page);
85 +
85 $files = $request->get('files'); 86 $files = $request->get('files');
86 $this->fileService->updateFileOrderWithinPage($files, $pageId); 87 $this->fileService->updateFileOrderWithinPage($files, $pageId);
87 return response()->json(['message' => 'File order updated']); 88 return response()->json(['message' => 'File order updated']);
88 } 89 }
89 90
91 + /**
92 + * Get a file from storage.
93 + * @param $fileId
94 + */
95 + public function get($fileId)
96 + {
97 + $file = $this->file->findOrFail($fileId);
98 + $page = $this->pageRepo->getById($file->uploaded_to);
99 + $this->checkOwnablePermission('page-view', $page);
90 100
101 + $fileContents = $this->fileService->getFile($file);
102 + return response($fileContents, 200, [
103 + 'Content-Type' => 'application/octet-stream',
104 + 'Content-Disposition' => 'attachment; filename="'. $file->name .'"'
105 + ]);
106 + }
107 +
108 + /**
109 + * Delete a specific file in the system.
110 + * @param $fileId
111 + * @return mixed
112 + */
113 + public function delete($fileId)
114 + {
115 + $file = $this->file->findOrFail($fileId);
116 + $this->checkOwnablePermission($file, 'file-delete');
117 + $this->fileService->deleteFile($file);
118 + return response()->json(['message' => 'File deleted']);
119 + }
91 } 120 }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 use BookStack\Exceptions\FileUploadException; 4 use BookStack\Exceptions\FileUploadException;
5 use BookStack\File; 5 use BookStack\File;
6 use Exception; 6 use Exception;
7 +use Illuminate\Contracts\Filesystem\FileNotFoundException;
7 use Illuminate\Support\Collection; 8 use Illuminate\Support\Collection;
8 use Symfony\Component\HttpFoundation\File\UploadedFile; 9 use Symfony\Component\HttpFoundation\File\UploadedFile;
9 10
...@@ -11,6 +12,17 @@ class FileService extends UploadService ...@@ -11,6 +12,17 @@ class FileService extends UploadService
11 { 12 {
12 13
13 /** 14 /**
15 + * Get a file from storage.
16 + * @param File $file
17 + * @return string
18 + */
19 + public function getFile(File $file)
20 + {
21 + $filePath = $this->getStorageBasePath() . $file->path;
22 + return $this->getStorage()->get($filePath);
23 + }
24 +
25 + /**
14 * Store a new file upon user upload. 26 * Store a new file upon user upload.
15 * @param UploadedFile $uploadedFile 27 * @param UploadedFile $uploadedFile
16 * @param int $page_id 28 * @param int $page_id
...@@ -76,4 +88,22 @@ class FileService extends UploadService ...@@ -76,4 +88,22 @@ class FileService extends UploadService
76 } 88 }
77 } 89 }
78 90
91 + /**
92 + * Delete a file and any empty folders the deletion leaves.
93 + * @param File $file
94 + */
95 + public function deleteFile(File $file)
96 + {
97 + $storedFilePath = $this->getStorageBasePath() . $file->path;
98 + $storage = $this->getStorage();
99 + $dirPath = dirname($storedFilePath);
100 +
101 + $storage->delete($storedFilePath);
102 + if (count($storage->allFiles($dirPath)) === 0) {
103 + $storage->deleteDirectory($dirPath);
104 + }
105 +
106 + $file->delete();
107 + }
108 +
79 } 109 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -28,6 +28,26 @@ class CreateFilesTable extends Migration ...@@ -28,6 +28,26 @@ class CreateFilesTable extends Migration
28 $table->index('uploaded_to'); 28 $table->index('uploaded_to');
29 $table->timestamps(); 29 $table->timestamps();
30 }); 30 });
31 +
32 + // Get roles with permissions we need to change
33 + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
34 +
35 + // Create & attach new entity permissions
36 + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
37 + $entity = 'File';
38 + foreach ($ops as $op) {
39 + $permissionId = DB::table('role_permissions')->insertGetId([
40 + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
41 + 'display_name' => $op . ' ' . $entity . 's',
42 + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
43 + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
44 + ]);
45 + DB::table('permission_role')->insert([
46 + 'role_id' => $adminRoleId,
47 + 'permission_id' => $permissionId
48 + ]);
49 + }
50 +
31 } 51 }
32 52
33 /** 53 /**
...@@ -38,5 +58,17 @@ class CreateFilesTable extends Migration ...@@ -38,5 +58,17 @@ class CreateFilesTable extends Migration
38 public function down() 58 public function down()
39 { 59 {
40 Schema::dropIfExists('files'); 60 Schema::dropIfExists('files');
61 +
62 + // Get roles with permissions we need to change
63 + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
64 +
65 + // Create & attach new entity permissions
66 + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
67 + $entity = 'File';
68 + foreach ($ops as $op) {
69 + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
70 + $permission = DB::table('role_permissions')->where('name', '=', $permName)->get();
71 + DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete();
72 + }
41 } 73 }
42 } 74 }
......
...@@ -575,9 +575,9 @@ module.exports = function (ngApp, events) { ...@@ -575,9 +575,9 @@ module.exports = function (ngApp, events) {
575 */ 575 */
576 function getFiles() { 576 function getFiles() {
577 let url = window.baseUrl(`/files/get/page/${pageId}`) 577 let url = window.baseUrl(`/files/get/page/${pageId}`)
578 - $http.get(url).then(responseData => { 578 + $http.get(url).then(resp => {
579 - $scope.files = responseData.data; 579 + $scope.files = resp.data;
580 - currentOrder = responseData.data.map(file => {return file.id}).join(':'); 580 + currentOrder = resp.data.map(file => {return file.id}).join(':');
581 }); 581 });
582 } 582 }
583 getFiles(); 583 getFiles();
...@@ -595,6 +595,17 @@ module.exports = function (ngApp, events) { ...@@ -595,6 +595,17 @@ module.exports = function (ngApp, events) {
595 events.emit('success', 'File uploaded'); 595 events.emit('success', 'File uploaded');
596 }; 596 };
597 597
598 + /**
599 + * Delete a file from the server and, on success, the local listing.
600 + * @param file
601 + */
602 + $scope.deleteFile = function(file) {
603 + $http.delete(`/files/${file.id}`).then(resp => {
604 + events.emit('success', resp.data.message);
605 + $scope.files.splice($scope.files.indexOf(file), 1);
606 + });
607 + };
608 +
598 }]); 609 }]);
599 610
600 }; 611 };
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
46 <tr ng-repeat="file in files track by $index"> 46 <tr ng-repeat="file in files track by $index">
47 <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> 47 <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
48 <td ng-bind="file.name"></td> 48 <td ng-bind="file.name"></td>
49 - <td width="10" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td> 49 + <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
50 </tr> 50 </tr>
51 </tbody> 51 </tbody>
52 </table> 52 </table>
......
1 1
2 <div class="book-tree" ng-non-bindable> 2 <div class="book-tree" ng-non-bindable>
3 3
4 + @if ($page->files->count() > 0)
5 + <h6 class="text-muted">Attachments</h6>
6 + @foreach($page->files as $file)
7 + <div class="attachment">
8 + <a href="{{ $file->getUrl() }}"><i class="zmdi zmdi-file"></i> {{ $file->name }}</a>
9 + </div>
10 + @endforeach
11 + @endif
12 +
4 @if (isset($pageNav) && $pageNav) 13 @if (isset($pageNav) && $pageNav)
5 <h6 class="text-muted">Page Navigation</h6> 14 <h6 class="text-muted">Page Navigation</h6>
6 <div class="sidebar-page-nav menu"> 15 <div class="sidebar-page-nav menu">
...@@ -10,8 +19,6 @@ ...@@ -10,8 +19,6 @@
10 </li> 19 </li>
11 @endforeach 20 @endforeach
12 </div> 21 </div>
13 -
14 -
15 @endif 22 @endif
16 23
17 <h6 class="text-muted">Book Navigation</h6> 24 <h6 class="text-muted">Book Navigation</h6>
......
...@@ -106,6 +106,19 @@ ...@@ -106,6 +106,19 @@
106 <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label> 106 <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
107 </td> 107 </td>
108 </tr> 108 </tr>
109 + <tr>
110 + <td>Attached <br>Files</td>
111 + <td>@include('settings/roles/checkbox', ['permission' => 'file-create-all'])</td>
112 + <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
113 + <td>
114 + <label>@include('settings/roles/checkbox', ['permission' => 'file-update-own']) Own</label>
115 + <label>@include('settings/roles/checkbox', ['permission' => 'file-update-all']) All</label>
116 + </td>
117 + <td>
118 + <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-own']) Own</label>
119 + <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-all']) All</label>
120 + </td>
121 + </tr>
109 </table> 122 </table>
110 </div> 123 </div>
111 </div> 124 </div>
......
...@@ -88,9 +88,11 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -88,9 +88,11 @@ Route::group(['middleware' => 'auth'], function () {
88 }); 88 });
89 89
90 // File routes 90 // File routes
91 + Route::get('/files/{id}', 'FileController@get');
91 Route::post('/files/upload', 'FileController@upload'); 92 Route::post('/files/upload', 'FileController@upload');
92 - Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage'); 93 + Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
93 - Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage'); 94 + Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
95 + Route::delete('/files/{id}', 'FileController@delete');
94 96
95 // AJAX routes 97 // AJAX routes
96 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); 98 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
......