Showing
9 changed files
with
152 additions
and
20 deletions
| ... | @@ -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'); | ... | ... |
-
Please register or sign in to post a comment