1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Tinyissue package. |
5
|
|
|
* |
6
|
|
|
* (c) Mohamed Alsharaf <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Tinyissue\Model\Project\Issue; |
13
|
|
|
|
14
|
|
|
use Illuminate\Database\Eloquent\Collection; |
15
|
|
|
use Tinyissue\Model\Message; |
16
|
|
|
use Tinyissue\Model\Message\Queue; |
17
|
|
|
use Tinyissue\Model\Project; |
18
|
|
|
use Tinyissue\Model\Project\Issue; |
19
|
|
|
use Tinyissue\Model\Tag; |
20
|
|
|
use Tinyissue\Model\User; |
21
|
|
|
use Tinyissue\Services\SendMessagesAbstract; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* SendMessages is a class to manage & process of sending messages about issue changes. |
25
|
|
|
* |
26
|
|
|
* @author Mohamed Alsharaf <[email protected]> |
27
|
|
|
*/ |
28
|
|
|
class SendMessages extends SendMessagesAbstract |
29
|
|
|
{ |
30
|
|
|
protected $template = 'update_issue'; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Collection of tags. |
34
|
|
|
* |
35
|
|
|
* @var Collection |
36
|
|
|
*/ |
37
|
|
|
protected $tags; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Returns an instance of Issue. |
41
|
|
|
* |
42
|
|
|
* @return bool |
43
|
|
|
*/ |
44
|
4 |
|
protected function getIssue() |
45
|
|
|
{ |
46
|
4 |
|
if (null === $this->issue) { |
47
|
4 |
|
$this->issue = $this->getModel(); |
|
|
|
|
48
|
|
|
} |
49
|
|
|
|
50
|
4 |
|
return $this->issue; |
|
|
|
|
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Returns an instance of Project. |
55
|
|
|
* |
56
|
|
|
* @return Project |
57
|
|
|
*/ |
58
|
4 |
|
protected function getProject() |
59
|
|
|
{ |
60
|
4 |
|
return $this->getIssue()->project; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Returns the project id. |
65
|
|
|
* |
66
|
|
|
* @return int |
67
|
|
|
*/ |
68
|
4 |
|
protected function getProjectId() |
69
|
|
|
{ |
70
|
4 |
|
return $this->getIssue()->project_id; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Returns message data belongs to adding an issue. |
75
|
|
|
* |
76
|
|
|
* @param Queue $queue |
77
|
|
|
* |
78
|
|
|
* @return array |
79
|
|
|
*/ |
80
|
4 |
|
protected function getMessageDataForAddIssue(Queue $queue) |
81
|
|
|
{ |
82
|
4 |
|
$messageData = ['changes' => []]; |
83
|
4 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' created a new issue'; |
|
|
|
|
84
|
4 |
|
if ($this->issue->assigned) { |
|
|
|
|
85
|
2 |
|
$messageData['changes']['assignee'] = $this->issue->assigned->fullname; |
|
|
|
|
86
|
|
|
} |
87
|
|
|
|
88
|
4 |
|
$tags = $this->issue->tags()->with('parent')->get(); |
89
|
4 |
|
foreach ($tags as $tag) { |
90
|
2 |
|
$tagArray = $tag->toShortArray(); |
91
|
2 |
|
$tagArray['now'] = $tagArray['name']; |
92
|
2 |
|
$messageData['changes'][$tag->parent->name] = $tagArray; |
93
|
|
|
} |
94
|
|
|
|
95
|
4 |
|
if ($this->issue->time_quote && !$this->issue->isQuoteLocked()) { |
96
|
|
|
$messageData['changes']['time_quote'] = \Html::duration($this->issue->time_quote); |
97
|
|
|
} |
98
|
|
|
|
99
|
4 |
|
return $messageData; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Returns message data belongs to updating an issue. |
104
|
|
|
* |
105
|
|
|
* @param Queue $queue |
106
|
|
|
* |
107
|
|
|
* @return array |
108
|
|
|
*/ |
109
|
1 |
|
protected function getMessageDataForUpdateIssue(Queue $queue) |
110
|
|
|
{ |
111
|
1 |
|
$messageData = []; |
112
|
1 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' updated an issue'; |
|
|
|
|
113
|
1 |
|
$whiteListFields = ['change_by', 'title', 'body', 'assignee', 'status', 'type', 'resolution', 'time_quote']; |
114
|
|
|
|
115
|
1 |
|
foreach ($queue->payload['dirty'] as $field => $value) { |
|
|
|
|
116
|
|
|
// Skip fields that not part of the white list or quote is locked |
117
|
1 |
|
if (!in_array($field, $whiteListFields) || ($field == 'time_quote' && $this->issue->isQuoteLocked())) { |
118
|
|
|
continue; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// Format quote to readable time |
122
|
1 |
|
$value = $field === 'time_quote' ? \Html::duration($value) : $value; |
123
|
1 |
|
$value = $field === 'body' ? \Html::format($value) : $value; |
124
|
1 |
|
$messageData['changes'][$field] = [ |
125
|
1 |
|
'now' => $value, |
126
|
1 |
|
'was' => $queue->getDataFromPayload('origin.' . $field), |
127
|
|
|
]; |
128
|
|
|
} |
129
|
|
|
|
130
|
1 |
|
return $messageData; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Returns message data belongs to reopening an issue. |
135
|
|
|
* |
136
|
|
|
* @param Queue $queue |
137
|
|
|
* |
138
|
|
|
* @return array |
139
|
|
|
*/ |
140
|
1 |
|
protected function getMessageDataForReopenIssue(Queue $queue) |
141
|
|
|
{ |
142
|
1 |
|
$messageData = []; |
143
|
1 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' reopened an issue'; |
|
|
|
|
144
|
1 |
|
$statusNow = $this->issue |
145
|
1 |
|
->tags()->with('parent')->get()->where('parent.name', Tag::GROUP_STATUS)->last(); |
146
|
1 |
|
$messageData['changes']['status'] = [ |
147
|
1 |
|
'was' => trans('tinyissue.closed'), |
148
|
1 |
|
'now' => ($statusNow ? $statusNow->fullname : ''), |
149
|
1 |
|
'id' => ($statusNow ? $statusNow->id : ''), |
150
|
|
|
]; |
151
|
|
|
|
152
|
1 |
|
return $messageData; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Returns message data belongs to closing an issue. |
157
|
|
|
* |
158
|
|
|
* @param Queue $queue |
159
|
|
|
* |
160
|
|
|
* @return array |
161
|
|
|
*/ |
162
|
2 |
|
protected function getMessageDataForCloseIssue(Queue $queue) |
163
|
|
|
{ |
164
|
2 |
|
$messageData = []; |
165
|
2 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' closed an issue'; |
|
|
|
|
166
|
2 |
|
$statusWas = $this->issue |
167
|
2 |
|
->tags()->with('parent')->get()->where('parent.name', Tag::GROUP_STATUS)->last(); |
168
|
2 |
|
$messageData['changes']['status'] = [ |
169
|
2 |
|
'was' => ($statusWas ? $statusWas->fullname : ''), |
170
|
2 |
|
'now' => trans('tinyissue.closed'), |
171
|
|
|
]; |
172
|
|
|
|
173
|
2 |
|
return $messageData; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Returns message data belongs to assigning an issue to a user. |
178
|
|
|
* |
179
|
|
|
* @param Queue $queue |
180
|
|
|
* @param array $extraData |
181
|
|
|
* |
182
|
|
|
* @return array |
183
|
|
|
*/ |
184
|
1 |
|
protected function getMessageDataForAssignIssue(Queue $queue, array $extraData) |
185
|
|
|
{ |
186
|
1 |
|
$messageData = []; |
187
|
1 |
|
if (!array_key_exists('now', $extraData)) { |
188
|
1 |
|
$assignTo = $this->getUserById($queue->getDataFromPayload('dirty.assigned_to')); |
189
|
1 |
|
$extraData = ['now' => $assignTo->fullname]; |
190
|
|
|
} |
191
|
|
|
|
192
|
1 |
|
$messageData['changes']['assignee'] = $extraData; |
193
|
1 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' assigned an issue to ' . $extraData['now']; |
|
|
|
|
194
|
|
|
|
195
|
1 |
|
return $messageData; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Returns message data belongs to changing an issue tags. |
200
|
|
|
* |
201
|
|
|
* @param Queue $queue |
202
|
|
|
* |
203
|
|
|
* @return array |
204
|
|
|
*/ |
205
|
3 |
|
protected function getMessageDataForChangeTagIssue(Queue $queue) |
206
|
|
|
{ |
207
|
3 |
|
$messageData = []; |
208
|
3 |
|
$messageData['changeByHeading'] = $queue->changeBy->fullname . ' changed an issue tag'; |
|
|
|
|
209
|
|
|
|
210
|
3 |
|
foreach ($queue->payload['added'] as $tag) { |
|
|
|
|
211
|
3 |
|
$group = strtolower($tag['group']); |
212
|
3 |
|
$messageData['changes'][$group] = [ |
213
|
3 |
|
'now' => $tag['name'], |
214
|
3 |
|
'id' => $tag['id'], |
215
|
3 |
|
'message_limit' => $tag['message_limit'], |
216
|
|
|
]; |
217
|
|
|
} |
218
|
|
|
|
219
|
3 |
|
foreach ($queue->payload['removed'] as $tag) { |
|
|
|
|
220
|
2 |
|
$group = strtolower($tag['group']); |
221
|
2 |
|
$messageData['changes'][$group]['was'] = $tag['name']; |
222
|
|
|
} |
223
|
|
|
|
224
|
3 |
|
return $messageData; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Check that the issue is load. |
229
|
|
|
* |
230
|
|
|
* @return bool |
231
|
|
|
*/ |
232
|
4 |
|
protected function validateData() |
233
|
|
|
{ |
234
|
|
|
// if issue closed and last issue not closed, then something is wrong skip |
235
|
4 |
|
if (!$this->issue->isOpen() && $this->latestMessage->event !== Queue::CLOSE_ISSUE) { |
236
|
|
|
return false; |
237
|
|
|
} |
238
|
|
|
|
239
|
4 |
|
return $this->issue; |
|
|
|
|
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Populate assigned relation in the current issue. |
244
|
|
|
* |
245
|
|
|
* @return void |
246
|
|
|
*/ |
247
|
4 |
View Code Duplication |
protected function populateData() |
|
|
|
|
248
|
|
|
{ |
249
|
4 |
|
$this->issue->setRelation('assigned', $this->getUserById($this->issue->assigned_to)); |
250
|
4 |
|
$creator = $this->getUserById($this->issue->created_by); |
251
|
4 |
|
if ($creator) { |
252
|
4 |
|
$this->issue->setRelation('user', $creator); |
253
|
|
|
} |
254
|
4 |
|
$this->loadIssueCreatorToProjectUsers(); |
255
|
4 |
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Check if the latest message is about closing or reopening an issue. |
259
|
|
|
* |
260
|
|
|
* @return bool |
261
|
|
|
*/ |
262
|
4 |
|
public function isStatusMessage() |
263
|
|
|
{ |
264
|
4 |
|
return ((!$this->issue->isOpen() && $this->latestMessage->event === Queue::CLOSE_ISSUE) |
265
|
4 |
|
|| ($this->issue->isOpen() && $this->latestMessage->event === Queue::REOPEN_ISSUE)); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Process assign to user message. Send direct message to new and previous users and full subscribers. |
270
|
|
|
* |
271
|
|
|
* @return void |
272
|
|
|
*/ |
273
|
4 |
|
protected function processDirectMessages() |
274
|
|
|
{ |
275
|
|
|
// Stop if issue is closed |
276
|
4 |
|
if (!$this->getIssue()->isOpen()) { |
|
|
|
|
277
|
2 |
|
return; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
// Fetch all of the assign issue changes |
281
|
4 |
|
$assignMessages = $this->allMessages->where('event', Queue::ASSIGN_ISSUE); |
282
|
|
|
|
283
|
|
|
// Skip if no changes |
284
|
4 |
|
if ($assignMessages->isEmpty()) { |
285
|
4 |
|
return; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
// Fetch the latest assignee |
289
|
|
|
/** @var Queue $assignMessage */ |
290
|
1 |
|
$assignMessage = $assignMessages->first(); |
291
|
|
|
|
292
|
|
|
// Fetch the user details of the new assignee & previous assignee if this isn't new issue |
293
|
1 |
|
$assigns = []; |
294
|
1 |
|
$assigns['new'] = (int) $assignMessage->getDataFromPayload('dirty.assigned_to'); |
295
|
1 |
|
if (!$this->addMessage) { |
296
|
1 |
|
$previousAssignMessage = $assignMessages->last(); |
297
|
1 |
|
$assigns['old'] = (int) $previousAssignMessage->getDataFromPayload('origin.assigned_to'); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
// Fetch users objects for old and new assignee |
301
|
|
|
/** @var \Illuminate\Database\Eloquent\Collection $assignObjects */ |
302
|
1 |
|
$assignObjects = (new User())->whereIn('id', $assigns)->get(); |
|
|
|
|
303
|
|
|
|
304
|
|
|
// If for what ever reason the user does not exists, skip this change |
305
|
|
|
// or if there is only one user and is not matching the new assignee. |
306
|
|
|
// then skip this message |
307
|
1 |
|
if ($assignObjects->count() === 0 |
308
|
1 |
|
|| ($assignObjects->count() === 1 && (int) $assignObjects->first()->id !== $assigns['new']) |
309
|
|
|
) { |
310
|
|
|
return; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
// Get the object of the new assignee |
314
|
1 |
|
$assignTo = $assignObjects->where('id', $assigns['new'], false)->first(); |
315
|
1 |
|
$users = collect([$this->createProjectUserObject($assignTo)]); |
316
|
|
|
|
317
|
|
|
// Exclude the user from any other message for this issue |
318
|
1 |
|
$this->addToExcludeUsers($assignTo); |
319
|
|
|
|
320
|
|
|
// Data about new and previous assignee |
321
|
|
|
$extraMessageData = [ |
322
|
1 |
|
'now' => $assignTo->fullname, |
323
|
|
|
]; |
324
|
|
|
|
325
|
|
|
// Make sure that the previous assignee was not the same as the new user |
326
|
1 |
|
if (array_key_exists('old', $assigns) && $assigns['old'] > 0 && $assigns['new'] !== $assigns['old']) { |
327
|
|
|
$previousAssign = $assignObjects->where('id', $assigns['old'], false)->first(); |
328
|
|
|
if ($previousAssign) { |
329
|
|
|
$extraMessageData['was'] = $previousAssign->fullname; |
330
|
|
|
$users->push($this->createProjectUserObject($previousAssign)); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
// Get message data needed for the message & send |
335
|
1 |
|
$messageData = $this->getMessageData($assignMessage, $extraMessageData); |
336
|
1 |
|
$this->sendMessages($users, $messageData); |
337
|
1 |
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Create user project object for a user. |
341
|
|
|
* |
342
|
|
|
* @param User $user |
343
|
|
|
* |
344
|
|
|
* @return Project\User |
345
|
|
|
*/ |
346
|
1 |
|
protected function createProjectUserObject(User $user) |
347
|
|
|
{ |
348
|
1 |
|
$userProject = new Project\User([ |
349
|
1 |
|
'user_id' => $user->id, |
350
|
1 |
|
'project_id' => $this->getProjectId(), |
351
|
|
|
]); |
352
|
1 |
|
$userProject->setRelation('user', $user); |
353
|
1 |
|
$userProject->setRelation('project', $this->getProject()); |
354
|
|
|
|
355
|
1 |
|
return $userProject; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Get collection of tags or one by ID. |
360
|
|
|
* |
361
|
|
|
* @param null|int $tagId |
362
|
|
|
* |
363
|
|
|
* @return \Illuminate\Support\Collection|Tag |
364
|
|
|
*/ |
365
|
3 |
|
protected function getTags($tagId = null) |
366
|
|
|
{ |
367
|
3 |
|
if (null === $this->tags) { |
368
|
3 |
|
$this->tags = collect([]); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
// Load & extract tag by ID |
372
|
3 |
|
if (null !== $tagId) { |
373
|
3 |
|
$tag = $this->tags->where('id', $tagId, false)->first(); |
374
|
3 |
|
if (!$tag) { |
375
|
3 |
|
$tag = (new Tag())->find($tagId); |
|
|
|
|
376
|
3 |
|
$this->tags->push($tag); |
377
|
|
|
} |
378
|
|
|
|
379
|
3 |
|
return $tag; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
return $this->tags; |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
/** |
386
|
|
|
* Whether or not the user wants to receive the message. |
387
|
|
|
* |
388
|
|
|
* @param Project\User $user |
389
|
|
|
* @param array $data |
390
|
|
|
* |
391
|
|
|
* @return bool |
392
|
|
|
*/ |
393
|
4 |
|
protected function wantToReceiveMessage(Project\User $user, array $data) |
394
|
|
|
{ |
395
|
4 |
|
$status = parent::wantToReceiveMessage($user, $data); |
396
|
|
|
|
397
|
4 |
|
if (!$status) { |
398
|
4 |
|
return false; |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
// Search for tag changes and verify if the user can receive messages |
402
|
4 |
|
foreach ($data['changes'] as $label => $change) { |
403
|
|
|
// Skip other changes that are not related to tag |
404
|
4 |
|
if (!in_array($label, Tag::getCoreGroups())) { |
405
|
4 |
|
continue; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
// If the change was only remove a tag |
409
|
3 |
|
if (!array_key_exists('now', $change) && array_key_exists('was', $change)) { |
410
|
|
|
return true; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
// If tag id not found |
414
|
3 |
|
if (!array_key_exists('id', $change)) { |
415
|
2 |
|
return true; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
// Fetch tag details |
419
|
3 |
|
$tag = $this->getTags($change['id']); |
420
|
|
|
|
421
|
|
|
// Check if user allowed to receive the message |
422
|
3 |
|
if (!$tag->allowMessagesToUser($user->user)) { |
|
|
|
|
423
|
3 |
|
return false; |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
|
427
|
4 |
|
return true; |
428
|
|
|
} |
429
|
|
|
} |
430
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.