]> BookStack Code Mirror - bookstack/blob - app/Actions/ActivityService.php
33ed44b32efa0dd64715af5a227e90a642cc0574
[bookstack] / app / Actions / ActivityService.php
1 <?php
2
3 namespace BookStack\Actions;
4
5 use BookStack\Auth\Permissions\PermissionService;
6 use BookStack\Auth\User;
7 use BookStack\Entities\Models\Book;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\Entity;
10 use BookStack\Entities\Models\Page;
11 use BookStack\Interfaces\Loggable;
12 use Illuminate\Database\Eloquent\Builder;
13 use Illuminate\Database\Eloquent\Relations\Relation;
14 use Illuminate\Support\Facades\Log;
15
16 class ActivityService
17 {
18     protected $activity;
19     protected $permissionService;
20
21     public function __construct(Activity $activity, PermissionService $permissionService)
22     {
23         $this->activity = $activity;
24         $this->permissionService = $permissionService;
25     }
26
27     /**
28      * Add activity data to database for an entity.
29      */
30     public function addForEntity(Entity $entity, string $type)
31     {
32         $activity = $this->newActivityForUser($type);
33         $entity->activity()->save($activity);
34         $this->setNotification($type);
35     }
36
37     /**
38      * Add a generic activity event to the database.
39      *
40      * @param string|Loggable $detail
41      */
42     public function add(string $type, $detail = '')
43     {
44         if ($detail instanceof Loggable) {
45             $detail = $detail->logDescriptor();
46         }
47
48         $activity = $this->newActivityForUser($type);
49         $activity->detail = $detail;
50         $activity->save();
51         $this->setNotification($type);
52     }
53
54     /**
55      * Get a new activity instance for the current user.
56      */
57     protected function newActivityForUser(string $type): Activity
58     {
59         $ip = request()->ip() ?? '';
60
61         return $this->activity->newInstance()->forceFill([
62             'type'     => strtolower($type),
63             'user_id'  => user()->id,
64             'ip'       => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
65         ]);
66     }
67
68     /**
69      * Removes the entity attachment from each of its activities
70      * and instead uses the 'extra' field with the entities name.
71      * Used when an entity is deleted.
72      */
73     public function removeEntity(Entity $entity)
74     {
75         $entity->activity()->update([
76             'detail'       => $entity->name,
77             'entity_id'    => null,
78             'entity_type'  => null,
79         ]);
80     }
81
82     /**
83      * Gets the latest activity.
84      */
85     public function latest(int $count = 20, int $page = 0): array
86     {
87         $activityList = $this->permissionService
88             ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
89             ->orderBy('created_at', 'desc')
90             ->with(['user', 'entity'])
91             ->skip($count * $page)
92             ->take($count)
93             ->get();
94
95         return $this->filterSimilar($activityList);
96     }
97
98     /**
99      * Gets the latest activity for an entity, Filtering out similar
100      * items to prevent a message activity list.
101      */
102     public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
103     {
104         /** @var array<string, int[]> $queryIds */
105         $queryIds = [$entity->getMorphClass() => [$entity->id]];
106
107         if ($entity instanceof Book) {
108             $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
109         }
110         if ($entity instanceof Book || $entity instanceof Chapter) {
111             $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
112         }
113
114         $query = $this->activity->newQuery();
115         $query->where(function (Builder $query) use ($queryIds) {
116             foreach ($queryIds as $morphClass => $idArr) {
117                 $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
118                     $innerQuery->where('entity_type', '=', $morphClass)
119                         ->whereIn('entity_id', $idArr);
120                 });
121             }
122         });
123
124         $activity = $query->orderBy('created_at', 'desc')
125             ->with(['entity' => function (Relation $query) {
126                 $query->withTrashed();
127             }, 'user.avatar'])
128             ->skip($count * ($page - 1))
129             ->take($count)
130             ->get();
131
132         return $this->filterSimilar($activity);
133     }
134
135     /**
136      * Get latest activity for a user, Filtering out similar items.
137      */
138     public function userActivity(User $user, int $count = 20, int $page = 0): array
139     {
140         $activityList = $this->permissionService
141             ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
142             ->orderBy('created_at', 'desc')
143             ->where('user_id', '=', $user->id)
144             ->skip($count * $page)
145             ->take($count)
146             ->get();
147
148         return $this->filterSimilar($activityList);
149     }
150
151     /**
152      * Filters out similar activity.
153      *
154      * @param Activity[] $activities
155      *
156      * @return array
157      */
158     protected function filterSimilar(iterable $activities): array
159     {
160         $newActivity = [];
161         $previousItem = null;
162
163         foreach ($activities as $activityItem) {
164             if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) {
165                 $newActivity[] = $activityItem;
166             }
167
168             $previousItem = $activityItem;
169         }
170
171         return $newActivity;
172     }
173
174     /**
175      * Flashes a notification message to the session if an appropriate message is available.
176      */
177     protected function setNotification(string $type)
178     {
179         $notificationTextKey = 'activities.' . $type . '_notification';
180         if (trans()->has($notificationTextKey)) {
181             $message = trans($notificationTextKey);
182             session()->flash('success', $message);
183         }
184     }
185
186     /**
187      * Log out a failed login attempt, Providing the given username
188      * as part of the message if the '%u' string is used.
189      */
190     public function logFailedLogin(string $username)
191     {
192         $message = config('logging.failed_login.message');
193         if (!$message) {
194             return;
195         }
196
197         $message = str_replace('%u', $username, $message);
198         $channel = config('logging.failed_login.channel');
199         Log::channel($channel)->warning($message);
200     }
201 }