]> BookStack Code Mirror - bookstack/blob - app/Actions/ActivityService.php
Fix coding style
[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\Chapter;
8 use BookStack\Entities\Models\Entity;
9 use BookStack\Entities\Models\Page;
10 use BookStack\Interfaces\Loggable;
11 use Illuminate\Database\Eloquent\Builder;
12 use Illuminate\Database\Eloquent\Relations\Relation;
13 use Illuminate\Support\Facades\Log;
14 use Illuminate\Support\Facades\Request;
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         return $this->activity->newInstance()->forceFill([
60             'type'     => strtolower($type),
61             'user_id'  => user()->id,
62             'ip'       => Request::ip(),
63         ]);
64     }
65
66     /**
67      * Removes the entity attachment from each of its activities
68      * and instead uses the 'extra' field with the entities name.
69      * Used when an entity is deleted.
70      */
71     public function removeEntity(Entity $entity)
72     {
73         $entity->activity()->update([
74             'detail'       => $entity->name,
75             'entity_id'    => null,
76             'entity_type'  => null,
77         ]);
78     }
79
80     /**
81      * Gets the latest activity.
82      */
83     public function latest(int $count = 20, int $page = 0): array
84     {
85         $activityList = $this->permissionService
86             ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
87             ->orderBy('created_at', 'desc')
88             ->with(['user', 'entity'])
89             ->skip($count * $page)
90             ->take($count)
91             ->get();
92
93         return $this->filterSimilar($activityList);
94     }
95
96     /**
97      * Gets the latest activity for an entity, Filtering out similar
98      * items to prevent a message activity list.
99      */
100     public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
101     {
102         /** @var [string => int[]] $queryIds */
103         $queryIds = [$entity->getMorphClass() => [$entity->id]];
104
105         if ($entity->isA('book')) {
106             $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
107         }
108         if ($entity->isA('book') || $entity->isA('chapter')) {
109             $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
110         }
111
112         $query = $this->activity->newQuery();
113         $query->where(function (Builder $query) use ($queryIds) {
114             foreach ($queryIds as $morphClass => $idArr) {
115                 $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
116                     $innerQuery->where('entity_type', '=', $morphClass)
117                         ->whereIn('entity_id', $idArr);
118                 });
119             }
120         });
121
122         $activity = $query->orderBy('created_at', 'desc')
123             ->with(['entity' => function (Relation $query) {
124                 $query->withTrashed();
125             }, 'user.avatar'])
126             ->skip($count * ($page - 1))
127             ->take($count)
128             ->get();
129
130         return $this->filterSimilar($activity);
131     }
132
133     /**
134      * Get latest activity for a user, Filtering out similar items.
135      */
136     public function userActivity(User $user, int $count = 20, int $page = 0): array
137     {
138         $activityList = $this->permissionService
139             ->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
140             ->orderBy('created_at', 'desc')
141             ->where('user_id', '=', $user->id)
142             ->skip($count * $page)
143             ->take($count)
144             ->get();
145
146         return $this->filterSimilar($activityList);
147     }
148
149     /**
150      * Filters out similar activity.
151      *
152      * @param Activity[] $activities
153      *
154      * @return array
155      */
156     protected function filterSimilar(iterable $activities): array
157     {
158         $newActivity = [];
159         $previousItem = null;
160
161         foreach ($activities as $activityItem) {
162             if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) {
163                 $newActivity[] = $activityItem;
164             }
165
166             $previousItem = $activityItem;
167         }
168
169         return $newActivity;
170     }
171
172     /**
173      * Flashes a notification message to the session if an appropriate message is available.
174      */
175     protected function setNotification(string $type)
176     {
177         $notificationTextKey = 'activities.' . $type . '_notification';
178         if (trans()->has($notificationTextKey)) {
179             $message = trans($notificationTextKey);
180             session()->flash('success', $message);
181         }
182     }
183
184     /**
185      * Log out a failed login attempt, Providing the given username
186      * as part of the message if the '%u' string is used.
187      */
188     public function logFailedLogin(string $username)
189     {
190         $message = config('logging.failed_login.message');
191         if (!$message) {
192             return;
193         }
194
195         $message = str_replace('%u', $username, $message);
196         $channel = config('logging.failed_login.channel');
197         Log::channel($channel)->warning($message);
198     }
199 }