- Renamed to "ActivityLogger" to be more focused in usage.
- Extracted out query elements to seperate "ActivityQueries" class.
- Removed old 'addForEntity' activity method to limit activity record
points.
--- /dev/null
+<?php
+
+namespace BookStack\Actions;
+
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Models\Entity;
+use BookStack\Interfaces\Loggable;
+use Illuminate\Support\Facades\Log;
+
+class ActivityLogger
+{
+ protected $permissionService;
+
+ public function __construct(PermissionService $permissionService)
+ {
+ $this->permissionService = $permissionService;
+ }
+
+ /**
+ * Add a generic activity event to the database.
+ *
+ * @param string|Loggable $detail
+ */
+ public function add(string $type, $detail = '')
+ {
+ $detailToStore = ($detail instanceof Loggable) ? $detail->logDescriptor() : $detail;
+
+ $activity = $this->newActivityForUser($type);
+ $activity->detail = $detailToStore;
+
+ if ($detail instanceof Entity) {
+ $activity->entity_id = $detail->id;
+ $activity->entity_type = $detail->getMorphClass();
+ }
+
+ $activity->save();
+ $this->setNotification($type);
+ }
+
+ /**
+ * Get a new activity instance for the current user.
+ */
+ protected function newActivityForUser(string $type): Activity
+ {
+ $ip = request()->ip() ?? '';
+
+ return (new Activity())->forceFill([
+ 'type' => strtolower($type),
+ 'user_id' => user()->id,
+ 'ip' => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
+ ]);
+ }
+
+ /**
+ * Removes the entity attachment from each of its activities
+ * and instead uses the 'extra' field with the entities name.
+ * Used when an entity is deleted.
+ */
+ public function removeEntity(Entity $entity)
+ {
+ $entity->activity()->update([
+ 'detail' => $entity->name,
+ 'entity_id' => null,
+ 'entity_type' => null,
+ ]);
+ }
+
+ /**
+ * Flashes a notification message to the session if an appropriate message is available.
+ */
+ protected function setNotification(string $type)
+ {
+ $notificationTextKey = 'activities.' . $type . '_notification';
+ if (trans()->has($notificationTextKey)) {
+ $message = trans($notificationTextKey);
+ session()->flash('success', $message);
+ }
+ }
+
+ /**
+ * Log out a failed login attempt, Providing the given username
+ * as part of the message if the '%u' string is used.
+ */
+ public function logFailedLogin(string $username)
+ {
+ $message = config('logging.failed_login.message');
+ if (!$message) {
+ return;
+ }
+
+ $message = str_replace('%u', $username, $message);
+ $channel = config('logging.failed_login.channel');
+ Log::channel($channel)->warning($message);
+ }
+}
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
-use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
-use Illuminate\Support\Facades\Log;
-class ActivityService
+class ActivityQueries
{
- protected $activity;
protected $permissionService;
- public function __construct(Activity $activity, PermissionService $permissionService)
+ public function __construct(PermissionService $permissionService)
{
- $this->activity = $activity;
$this->permissionService = $permissionService;
}
- /**
- * Add activity data to database for an entity.
- */
- public function addForEntity(Entity $entity, string $type)
- {
- $activity = $this->newActivityForUser($type);
- $entity->activity()->save($activity);
- $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.
- */
- protected function newActivityForUser(string $type): Activity
- {
- $ip = request()->ip() ?? '';
-
- return $this->activity->newInstance()->forceFill([
- 'type' => strtolower($type),
- 'user_id' => user()->id,
- 'ip' => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
- ]);
- }
-
- /**
- * Removes the entity attachment from each of its activities
- * and instead uses the 'extra' field with the entities name.
- * Used when an entity is deleted.
- */
- public function removeEntity(Entity $entity)
- {
- $entity->activity()->update([
- 'detail' => $entity->name,
- 'entity_id' => null,
- 'entity_type' => null,
- ]);
- }
-
/**
* Gets the latest activity.
*/
public function latest(int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
- ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
+ ->filterRestrictedEntityRelations(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['user', 'entity'])
->skip($count * $page)
$queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');
}
- $query = $this->activity->newQuery();
+ $query = Activity::query();
$query->where(function (Builder $query) use ($queryIds) {
foreach ($queryIds as $morphClass => $idArr) {
$query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
- ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
+ ->filterRestrictedEntityRelations(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->skip($count * $page)
* Filters out similar activity.
*
* @param Activity[] $activities
- *
- * @return array
*/
protected function filterSimilar(iterable $activities): array
{
return $newActivity;
}
- /**
- * Flashes a notification message to the session if an appropriate message is available.
- */
- protected function setNotification(string $type)
- {
- $notificationTextKey = 'activities.' . $type . '_notification';
- if (trans()->has($notificationTextKey)) {
- $message = trans($notificationTextKey);
- session()->flash('success', $message);
- }
- }
-
- /**
- * Log out a failed login attempt, Providing the given username
- * as part of the message if the '%u' string is used.
- */
- public function logFailedLogin(string $username)
- {
- $message = config('logging.failed_login.message');
- if (!$message) {
- return;
- }
-
- $message = str_replace('%u', $username, $message);
- $channel = config('logging.failed_login.channel');
- Log::channel($channel)->warning($message);
- }
-}
+}
\ No newline at end of file
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
- ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
+ ActivityService::add(ActivityType::COMMENTED_ON, $entity);
return $comment;
}
namespace BookStack\Auth;
-use Activity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
}
}
- /**
- * Get the latest activity for a user.
- */
- public function getActivity(User $user, int $count = 20, int $page = 0): array
- {
- return Activity::userActivity($user, $count, $page);
- }
-
/**
* Get the recently created content for this given user.
*/
use BookStack\Facades\Permissions;
use BookStack\Interfaces\Deletable;
use BookStack\Interfaces\Favouritable;
+use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
-abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable
+abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable
{
use SoftDeletes;
use HasCreatorAndUpdater;
->where('user_id', '=', user()->id)
->exists();
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function logDescriptor(): string
+ {
+ return "({$this->id}) {$this->name}";
+ }
}
{
$book = new Book();
$this->baseRepo->create($book, $input);
- Activity::addForEntity($book, ActivityType::BOOK_CREATE);
+ Activity::add(ActivityType::BOOK_CREATE, $book);
return $book;
}
public function update(Book $book, array $input): Book
{
$this->baseRepo->update($book, $input);
- Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
+ Activity::add(ActivityType::BOOK_UPDATE, $book);
return $book;
}
{
$trashCan = new TrashCan();
$trashCan->softDestroyBook($book);
- Activity::addForEntity($book, ActivityType::BOOK_DELETE);
+ Activity::add(ActivityType::BOOK_DELETE, $book);
$trashCan->autoClearOld();
}
$shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input);
$this->updateBooks($shelf, $bookIds);
- Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
+ Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
return $shelf;
}
$this->updateBooks($shelf, $bookIds);
}
- Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
+ Activity::add(ActivityType::BOOKSHELF_UPDATE, $shelf);
return $shelf;
}
{
$trashCan = new TrashCan();
$trashCan->softDestroyShelf($shelf);
- Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
+ Activity::add(ActivityType::BOOKSHELF_DELETE, $shelf);
$trashCan->autoClearOld();
}
}
$chapter->book_id = $parentBook->id;
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
- Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
+ Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
return $chapter;
}
public function update(Chapter $chapter, array $input): Chapter
{
$this->baseRepo->update($chapter, $input);
- Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
+ Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
return $chapter;
}
{
$trashCan = new TrashCan();
$trashCan->softDestroyChapter($chapter);
- Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
+ Activity::add(ActivityType::CHAPTER_DELETE, $chapter);
$trashCan->autoClearOld();
}
$chapter->changeBook($parent->id);
$chapter->rebuildPermissions();
- Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
+ Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
return $parent;
}
$draft->indexForSearch();
$draft->refresh();
- Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
+ Activity::add(ActivityType::PAGE_CREATE, $draft);
return $draft;
}
$this->savePageRevision($page, $summary);
}
- Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
+ Activity::add(ActivityType::PAGE_UPDATE, $page);
return $page;
}
{
$trashCan = new TrashCan();
$trashCan->softDestroyPage($page);
- Activity::addForEntity($page, ActivityType::PAGE_DELETE);
+ Activity::add(ActivityType::PAGE_DELETE, $page);
$trashCan->autoClearOld();
}
$summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
$this->savePageRevision($page, $summary);
- Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
+ Activity::add(ActivityType::PAGE_RESTORE, $page);
return $page;
}
$page->changeBook($newBookId);
$page->rebuildPermissions();
- Activity::addForEntity($page, ActivityType::PAGE_MOVE);
+ Activity::add(ActivityType::PAGE_MOVE, $page);
return $parent;
}
$entity->save();
$entity->rebuildPermissions();
- Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
+ Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}
/**
namespace BookStack\Http\Controllers\Auth;
-use Activity;
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
+use BookStack\Facades\Activity;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Actions\ActivityQueries;
use BookStack\Actions\ActivityType;
use BookStack\Actions\View;
use BookStack\Entities\Models\Bookshelf;
if ($bookshelf) {
$bookshelf->appendBook($book);
- Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
+ Activity::add(ActivityType::BOOKSHELF_UPDATE, $bookshelf);
}
return redirect($book->getUrl());
/**
* Display the specified book.
*/
- public function show(Request $request, string $slug)
+ public function show(Request $request, ActivityQueries $activities, string $slug)
{
$book = $this->bookRepo->getBySlug($slug);
$bookChildren = (new BookContents($book))->getTree(true);
'current' => $book,
'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
- 'activity' => Activity::entityActivity($book, 20, 1),
+ 'activity' => $activities->entityActivity($book, 20, 1),
]);
}
// Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) {
- Activity::addForEntity($book, ActivityType::BOOK_SORT);
+ Activity::add(ActivityType::BOOK_SORT, $book);
});
return redirect($book->getUrl());
namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Actions\ActivityQueries;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookshelfRepo;
*
* @throws NotFoundException
*/
- public function show(string $slug)
+ public function show(ActivityQueries $activities, string $slug)
{
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('book-view', $shelf);
'shelf' => $shelf,
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
- 'activity' => Activity::entityActivity($shelf, 20, 1),
+ 'activity' => $activities->entityActivity($shelf, 20, 1),
'order' => $order,
'sort' => $sort,
]);
namespace BookStack\Http\Controllers;
-use Activity;
+use BookStack\Actions\ActivityQueries;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\RecentlyViewed;
/**
* Display the homepage.
*/
- public function index()
+ public function index(ActivityQueries $activities)
{
- $activity = Activity::latest(10);
+ $activity = $activities->latest(10);
$draftPages = [];
if ($this->isSignedIn()) {
namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityQueries;
use BookStack\Auth\UserRepo;
class UserProfileController extends Controller
/**
* Show the user profile page.
*/
- public function show(UserRepo $repo, string $slug)
+ public function show(UserRepo $repo, ActivityQueries $activities, string $slug)
{
$user = $repo->getBySlug($slug);
- $userActivity = $repo->getActivity($user);
+ $userActivity = $activities->userActivity($user);
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
$assetCounts = $repo->getAssetCounts($user);
namespace BookStack\Providers;
-use BookStack\Actions\ActivityService;
+use BookStack\Actions\ActivityLogger;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Theming\ThemeService;
use BookStack\Uploads\ImageService;
public function register()
{
$this->app->singleton('activity', function () {
- return $this->app->make(ActivityService::class);
+ return $this->app->make(ActivityLogger::class);
});
$this->app->singleton('images', function () {
<?php
-namespace Database\Factories;
+namespace Database\Factories\Actions;
use BookStack\Actions\ActivityType;
use BookStack\Actions\Webhook;
namespace Tests\Actions;
use BookStack\Actions\Activity;
-use BookStack\Actions\ActivityService;
+use BookStack\Actions\ActivityLogger;
use BookStack\Actions\ActivityType;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Models\Chapter;
class AuditLogTest extends TestCase
{
- /** @var ActivityService */
+ /** @var ActivityLogger */
protected $activityService;
protected function setUp(): void
{
parent::setUp();
- $this->activityService = app(ActivityService::class);
+ $this->activityService = app(ActivityLogger::class);
}
public function test_only_accessible_with_right_permissions()
$admin = $this->getAdmin();
$this->actingAs($admin);
$page = Page::query()->first();
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$activity = Activity::query()->orderBy('id', 'desc')->first();
$resp = $this->get('settings/audit');
$this->actingAs($this->getAdmin());
$page = Page::query()->first();
$pageName = $page->name;
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
app(PageRepo::class)->destroy($page);
app(TrashCan::class)->empty();
$viewer = $this->getViewer();
$this->actingAs($viewer);
$page = Page::query()->first();
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$this->actingAs($this->getAdmin());
app(UserRepo::class)->destroy($viewer);
{
$this->actingAs($this->getAdmin());
$page = Page::query()->first();
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$resp = $this->get('settings/audit');
$resp->assertSeeText($page->name);
{
$this->actingAs($this->getAdmin());
$page = Page::query()->first();
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$yesterday = (Carbon::now()->subDay()->format('Y-m-d'));
$tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));
$editor = $this->getEditor();
$this->actingAs($admin);
$page = Page::query()->first();
- $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
+ $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$this->actingAs($editor);
$chapter = Chapter::query()->first();
- $this->activityService->addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
+ $this->activityService->add(ActivityType::CHAPTER_UPDATE, $chapter);
$resp = $this->actingAs($admin)->get('settings/audit?user=' . $admin->id);
$resp->assertSeeText($page->name);
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Page;
+use BookStack\Facades\Activity;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
public function test_clear_activity_command()
{
$this->asEditor();
- $page = Page::first();
- \Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
+ /** @var Page $page */
+ $page = Page::query()->first();
+ Activity::add(ActivityType::PAGE_UPDATE, $page);
$this->assertDatabaseHas('activities', [
'type' => 'page_update',
]);
DB::rollBack();
- $exitCode = \Artisan::call('bookstack:clear-activity');
+ $exitCode = Artisan::call('bookstack:clear-activity');
DB::beginTransaction();
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$newUser = User::factory()->create();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
- Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
- Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
+ Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);
+ Activity::add(ActivityType::PAGE_CREATE, $entities['page']);
$this->asAdmin()->get('/user/' . $newUser->slug)
->assertElementContains('#recent-user-activity', 'updated book')
$newUser = User::factory()->create();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
- Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
- Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
+ Activity::add(ActivityType::BOOK_UPDATE, $entities['book']);
+ Activity::add(ActivityType::PAGE_CREATE, $entities['page']);
$linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]';
$this->asAdmin()->get('/')