In progress, Need to implement much of the logging in controllers.
Also cleaned up base controller along the way.
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;
$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.
*/
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
<?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
{
/**
return $record;
}
+ public function logDescriptor(): string
+ {
+ $deletable = $this->deletable()->first();
+ return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
+ }
}
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;
protected $imageRepo;
- /**
- * BaseRepo constructor.
- * @param $tagRepo
- */
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
{
$this->tagRepo = $tagRepo;
$entity->save();
$entity->rebuildPermissions();
+ Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
}
}
<?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;
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()
{
//
/**
* Adds the page title into the view.
- * @param $title
*/
- public function setPageTitle($title)
+ public function setPageTitle(string $title)
{
view()->share('pageTitle', $title);
}
}
/**
- * 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',
/**
* 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.
*/
namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Entities\Managers\TrashCan;
use BookStack\Notifications\TestEmail;
use BookStack\Uploads\ImageService;
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'));
public function sendTestEmail()
{
$this->checkPermission('settings-manage');
+ $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
try {
user()->notify(new TestEmail());
<?php namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Entities\Deletion;
use BookStack\Entities\Managers\TrashCan;
{
/** @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]));
$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);
}
{
$deleteCount = (new TrashCan())->empty();
+ $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
return redirect($this->recycleBinBaseUrl);
}
<?php namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
// 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);
}
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, '#'));
}
}
--- /dev/null
+<?php
+
+namespace BookStack\Interfaces;
+
+interface Loggable
+{
+ /**
+ * Get the string descriptor for this item.
+ */
+ public function logDescriptor(): string;
+}
\ No newline at end of file
// Other
'commented_on' => 'commented on',
+ 'permissions_update' => 'updated permissions',
];