diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php index fb4a739cd..0b3b0f0bc 100644 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@ -5,6 +5,7 @@ use BookStack\Auth\User; use BookStack\Entities\Chapter; use BookStack\Entities\Entity; use BookStack\Entities\Page; +use BookStack\Interfaces\Loggable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Log; @@ -30,6 +31,22 @@ class ActivityService $this->setNotification($type); } + /** + * Add a generic activity event to the database. + * @param string|Loggable $detail + */ + public function add(string $type, $detail = '') + { + if ($detail instanceof Loggable) { + $detail = $detail->logDescriptor(); + } + + $activity = $this->newActivityForUser($type); + $activity->detail = $detail; + $activity->save(); + $this->setNotification($type); + } + /** * Get a new activity instance for the current user. */ diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index 5404d884c..9b4e86a16 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -7,16 +7,46 @@ class ActivityType const PAGE_DELETE = 'page_delete'; const PAGE_RESTORE = 'page_restore'; const PAGE_MOVE = 'page_move'; - const COMMENTED_ON = 'commented_on'; + const CHAPTER_CREATE = 'chapter_create'; const CHAPTER_UPDATE = 'chapter_update'; const CHAPTER_DELETE = 'chapter_delete'; const CHAPTER_MOVE = 'chapter_move'; + const BOOK_CREATE = 'book_create'; const BOOK_UPDATE = 'book_update'; const BOOK_DELETE = 'book_delete'; const BOOK_SORT = 'book_sort'; + const BOOKSHELF_CREATE = 'bookshelf_create'; const BOOKSHELF_UPDATE = 'bookshelf_update'; const BOOKSHELF_DELETE = 'bookshelf_delete'; + + const COMMENTED_ON = 'commented_on'; + const PERMISSIONS_UPDATE = 'permissions_update'; + + const SETTINGS_UPDATE = 'settings_update'; + const MAINTENANCE_ACTION_RUN = 'maintenance_action_run'; + + const RECYCLE_BIN_EMPTY = 'recycle_bin_empty'; + const RECYCLE_BIN_RESTORE = 'recycle_bin_restore'; + const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy'; + + // TODO - Implement all below + const USER_CREATE = 'user_create'; + const USER_UPDATE = 'user_update'; + const USER_DELETE = 'user_delete'; + + const API_TOKEN_CREATE = 'api_token_create'; + const API_TOKEN_UPDATE = 'api_token_update'; + const API_TOKEN_DELETE = 'api_token_delete'; + + const ROLE_CREATE = 'role_create'; + const ROLE_UPDATE = 'role_update'; + const ROLE_DELETE = 'role_delete'; + + const ACCESS_PASSWORD_RESET = 'access_password_reset_request'; + const ACCESS_PASSWORD_RESET_UPDATE = 'access_password_reset_update'; + const ACCESS_LOGIN = 'access_login'; + const ACCESS_FAILED_LOGIN = 'access_failed_login'; } \ No newline at end of file diff --git a/app/Entities/Deletion.php b/app/Entities/Deletion.php index 576862caa..dab44d4ad 100644 --- a/app/Entities/Deletion.php +++ b/app/Entities/Deletion.php @@ -1,11 +1,12 @@ <?php namespace BookStack\Entities; use BookStack\Auth\User; +use BookStack\Interfaces\Loggable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; -class Deletion extends Model +class Deletion extends Model implements Loggable { /** @@ -38,4 +39,9 @@ class Deletion extends Model return $record; } + public function logDescriptor(): string + { + $deletable = $this->deletable()->first(); + return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}"; + } } diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 7c25e4981..a9d599eb2 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -2,11 +2,12 @@ namespace BookStack\Entities\Repos; +use BookStack\Actions\ActivityType; use BookStack\Actions\TagRepo; -use BookStack\Entities\Book; use BookStack\Entities\Entity; use BookStack\Entities\HasCoverImage; use BookStack\Exceptions\ImageUploadException; +use BookStack\Facades\Activity; use BookStack\Uploads\ImageRepo; use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; @@ -18,10 +19,6 @@ class BaseRepo protected $imageRepo; - /** - * BaseRepo constructor. - * @param $tagRepo - */ public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo) { $this->tagRepo = $tagRepo; @@ -115,5 +112,6 @@ class BaseRepo $entity->save(); $entity->rebuildPermissions(); + Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE); } } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index 4650e1dde..6792f9f45 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -1,7 +1,5 @@ <?php namespace BookStack\Http\Controllers\Api; -use BookStack\Actions\ActivityType; -use BookStack\Facades\Activity; use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Bookshelf; use Exception; diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 6a1dfcb01..3ac139249 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,21 +2,20 @@ namespace BookStack\Http\Controllers; +use BookStack\Facades\Activity; +use BookStack\Interfaces\Loggable; use BookStack\Ownable; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\Exceptions\HttpResponseException; -use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Illuminate\Routing\Controller as BaseController; -use Illuminate\Validation\ValidationException; abstract class Controller extends BaseController { use DispatchesJobs, ValidatesRequests; - /** - * Controller constructor. - */ public function __construct() { // @@ -43,9 +42,8 @@ abstract class Controller extends BaseController /** * Adds the page title into the view. - * @param $title */ - public function setPageTitle($title) + public function setPageTitle(string $title) { view()->share('pageTitle', $title); } @@ -67,79 +65,59 @@ abstract class Controller extends BaseController } /** - * Checks for a permission. - * @param string $permissionName - * @return bool|\Illuminate\Http\RedirectResponse + * Checks that the current user has the given permission otherwise throw an exception. */ - protected function checkPermission($permissionName) + protected function checkPermission(string $permission): void { - if (!user() || !user()->can($permissionName)) { + if (!user() || !user()->can($permission)) { $this->showPermissionError(); } - return true; } /** - * Check the current user's permissions against an ownable item. - * @param $permission - * @param Ownable $ownable - * @return bool + * Check the current user's permissions against an ownable item otherwise throw an exception. */ - protected function checkOwnablePermission($permission, Ownable $ownable) + protected function checkOwnablePermission(string $permission, Ownable $ownable): void { - if (userCan($permission, $ownable)) { - return true; + if (!userCan($permission, $ownable)) { + $this->showPermissionError(); } - return $this->showPermissionError(); } /** - * Check if a user has a permission or bypass if the callback is true. - * @param $permissionName - * @param $callback - * @return bool + * Check if a user has a permission or bypass the permission + * check if the given callback resolves true. */ - protected function checkPermissionOr($permissionName, $callback) + protected function checkPermissionOr(string $permission, callable $callback): void { - $callbackResult = $callback(); - if ($callbackResult === false) { - $this->checkPermission($permissionName); + if ($callback() !== true) { + $this->checkPermission($permission); } - return true; } /** * Check if the current user has a permission or bypass if the provided user * id matches the current user. - * @param string $permissionName - * @param int $userId - * @return bool */ - protected function checkPermissionOrCurrentUser(string $permissionName, int $userId) + protected function checkPermissionOrCurrentUser(string $permission, int $userId): void { - return $this->checkPermissionOr($permissionName, function () use ($userId) { + $this->checkPermissionOr($permission, function () use ($userId) { return $userId === user()->id; }); } /** * Send back a json error message. - * @param string $messageText - * @param int $statusCode - * @return mixed */ - protected function jsonError($messageText = "", $statusCode = 500) + protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse { return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode); } /** * Create a response that forces a download in the browser. - * @param string $content - * @param string $fileName - * @return \Illuminate\Http\Response */ - protected function downloadResponse(string $content, string $fileName) + protected function downloadResponse(string $content, string $fileName): Response { return response()->make($content, 200, [ 'Content-Type' => 'application/octet-stream', @@ -149,31 +127,37 @@ abstract class Controller extends BaseController /** * Show a positive, successful notification to the user on next view load. - * @param string $message */ - protected function showSuccessNotification(string $message) + protected function showSuccessNotification(string $message): void { session()->flash('success', $message); } /** * Show a warning notification to the user on next view load. - * @param string $message */ - protected function showWarningNotification(string $message) + protected function showWarningNotification(string $message): void { session()->flash('warning', $message); } /** * Show an error notification to the user on next view load. - * @param string $message */ - protected function showErrorNotification(string $message) + protected function showErrorNotification(string $message): void { session()->flash('error', $message); } + /** + * Log an activity in the system. + * @param string|Loggable + */ + protected function logActivity(string $type, $detail = ''): void + { + Activity::add($type, $detail); + } + /** * Get the validation rules for image files. */ diff --git a/app/Http/Controllers/MaintenanceController.php b/app/Http/Controllers/MaintenanceController.php index 0d6265f90..a638b69cd 100644 --- a/app/Http/Controllers/MaintenanceController.php +++ b/app/Http/Controllers/MaintenanceController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers; +use BookStack\Actions\ActivityType; use BookStack\Entities\Managers\TrashCan; use BookStack\Notifications\TestEmail; use BookStack\Uploads\ImageService; @@ -35,6 +36,7 @@ class MaintenanceController extends Controller public function cleanupImages(Request $request, ImageService $imageService) { $this->checkPermission('settings-manage'); + $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images'); $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true'); $dryRun = !($request->has('confirm')); @@ -61,6 +63,7 @@ class MaintenanceController extends Controller public function sendTestEmail() { $this->checkPermission('settings-manage'); + $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email'); try { user()->notify(new TestEmail()); diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index 459dbb39d..57038046a 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -1,5 +1,6 @@ <?php namespace BookStack\Http\Controllers; +use BookStack\Actions\ActivityType; use BookStack\Entities\Deletion; use BookStack\Entities\Managers\TrashCan; @@ -56,6 +57,7 @@ class RecycleBinController extends Controller { /** @var Deletion $deletion */ $deletion = Deletion::query()->findOrFail($id); + $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion); $restoreCount = (new TrashCan())->restoreFromDeletion($deletion); $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount])); @@ -85,6 +87,7 @@ class RecycleBinController extends Controller $deletion = Deletion::query()->findOrFail($id); $deleteCount = (new TrashCan())->destroyFromDeletion($deletion); + $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); return redirect($this->recycleBinBaseUrl); } @@ -97,6 +100,7 @@ class RecycleBinController extends Controller { $deleteCount = (new TrashCan())->empty(); + $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); return redirect($this->recycleBinBaseUrl); } diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 50d91d388..92435f924 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,5 +1,6 @@ <?php namespace BookStack\Http\Controllers; +use BookStack\Actions\ActivityType; use BookStack\Auth\User; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; @@ -47,10 +48,10 @@ class SettingController extends Controller // Cycles through posted settings and update them foreach ($request->all() as $name => $value) { + $key = str_replace('setting-', '', trim($name)); if (strpos($name, 'setting-') !== 0) { continue; } - $key = str_replace('setting-', '', trim($name)); setting()->put($key, $value); } @@ -68,8 +69,10 @@ class SettingController extends Controller setting()->remove('app-logo'); } + $section = $request->get('section', ''); + $this->logActivity(ActivityType::SETTINGS_UPDATE, $section); $this->showSuccessNotification(trans('settings.settings_save_success')); - $redirectLocation = '/settings#' . $request->get('section', ''); + $redirectLocation = '/settings#' . $section; return redirect(rtrim($redirectLocation, '#')); } } diff --git a/app/Interfaces/Loggable.php b/app/Interfaces/Loggable.php new file mode 100644 index 000000000..33e1d7c95 --- /dev/null +++ b/app/Interfaces/Loggable.php @@ -0,0 +1,11 @@ +<?php + +namespace BookStack\Interfaces; + +interface Loggable +{ + /** + * Get the string descriptor for this item. + */ + public function logDescriptor(): string; +} \ No newline at end of file diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 4cac54b2a..fe937b061 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -45,4 +45,5 @@ return [ // Other 'commented_on' => 'commented on', + 'permissions_update' => 'updated permissions', ];