Added view, deletion and permissions for files

This commit is contained in:
Dan Brown 2016-10-10 20:30:27 +01:00
parent 673c74ddfc
commit ac0b29fb6d
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 152 additions and 20 deletions

View File

@ -7,12 +7,20 @@ class File extends Ownable
/** /**
* Get the page this file was uploaded to. * Get the page this file was uploaded to.
* @return mixed * @return Page
*/ */
public function page() public function page()
{ {
return $this->belongsTo(Page::class, 'uploaded_to'); return $this->belongsTo(Page::class, 'uploaded_to');
} }
/**
* Get the url of this file.
* @return string
*/
public function getUrl()
{
return '/files/' . $this->id;
}
} }

View File

@ -1,10 +1,7 @@
<?php <?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\File; use BookStack\File;
use BookStack\Page;
use BookStack\Repos\PageRepo; use BookStack\Repos\PageRepo;
use BookStack\Services\FileService; use BookStack\Services\FileService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -37,16 +34,18 @@ class FileController extends Controller
*/ */
public function upload(Request $request) 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. // TODO - ensure uploads are deleted on page delete.
$this->validate($request, [ $this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id' 'uploaded_to' => 'required|integer|exists:pages,id'
]); ]);
$uploadedFile = $request->file('file');
$pageId = $request->get('uploaded_to'); $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 { try {
$file = $this->fileService->saveNewUpload($uploadedFile, $pageId); $file = $this->fileService->saveNewUpload($uploadedFile, $pageId);
@ -62,10 +61,10 @@ class FileController extends Controller
* @param $pageId * @param $pageId
* @return mixed * @return mixed
*/ */
public function getFilesForPage($pageId) public function listForPage($pageId)
{ {
// TODO - check view permission on page?
$page = $this->pageRepo->getById($pageId); $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->files); return response()->json($page->files);
} }
@ -75,17 +74,47 @@ class FileController extends Controller
* @param Request $request * @param Request $request
* @return mixed * @return mixed
*/ */
public function sortFilesForPage($pageId, Request $request) public function sortForPage($pageId, Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'files' => 'required|array', 'files' => 'required|array',
'files.*.id' => 'required|integer', 'files.*.id' => 'required|integer',
]); ]);
$page = $this->pageRepo->getById($pageId); $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$files = $request->get('files'); $files = $request->get('files');
$this->fileService->updateFileOrderWithinPage($files, $pageId); $this->fileService->updateFileOrderWithinPage($files, $pageId);
return response()->json(['message' => 'File order updated']); 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']);
}
} }

View File

@ -4,12 +4,24 @@
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\File; use BookStack\File;
use Exception; use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class FileService extends UploadService 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. * Store a new file upon user upload.
* @param UploadedFile $uploadedFile * @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();
}
} }

View File

@ -28,6 +28,26 @@ class CreateFilesTable extends Migration
$table->index('uploaded_to'); $table->index('uploaded_to');
$table->timestamps(); $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() public function down()
{ {
Schema::dropIfExists('files'); 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();
}
} }
} }

View File

@ -575,9 +575,9 @@ module.exports = function (ngApp, events) {
*/ */
function getFiles() { function getFiles() {
let url = window.baseUrl(`/files/get/page/${pageId}`) let url = window.baseUrl(`/files/get/page/${pageId}`)
$http.get(url).then(responseData => { $http.get(url).then(resp => {
$scope.files = responseData.data; $scope.files = resp.data;
currentOrder = responseData.data.map(file => {return file.id}).join(':'); currentOrder = resp.data.map(file => {return file.id}).join(':');
}); });
} }
getFiles(); getFiles();
@ -595,6 +595,17 @@ module.exports = function (ngApp, events) {
events.emit('success', 'File uploaded'); 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);
});
};
}]); }]);
}; };

View File

@ -46,7 +46,7 @@
<tr ng-repeat="file in files track by $index"> <tr ng-repeat="file in files track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td> <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
<td ng-bind="file.name"></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> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,6 +1,15 @@
<div class="book-tree" ng-non-bindable> <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) @if (isset($pageNav) && $pageNav)
<h6 class="text-muted">Page Navigation</h6> <h6 class="text-muted">Page Navigation</h6>
<div class="sidebar-page-nav menu"> <div class="sidebar-page-nav menu">
@ -10,8 +19,6 @@
</li> </li>
@endforeach @endforeach
</div> </div>
@endif @endif
<h6 class="text-muted">Book Navigation</h6> <h6 class="text-muted">Book Navigation</h6>

View File

@ -106,6 +106,19 @@
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label> <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
</td> </td>
</tr> </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> </table>
</div> </div>
</div> </div>

View File

@ -88,9 +88,11 @@ Route::group(['middleware' => 'auth'], function () {
}); });
// File routes // File routes
Route::get('/files/{id}', 'FileController@get');
Route::post('/files/upload', 'FileController@upload'); Route::post('/files/upload', 'FileController@upload');
Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage'); Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage'); Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
Route::delete('/files/{id}', 'FileController@delete');
// AJAX routes // AJAX routes
Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');