GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Tracker_Artifact_Changeset   F
last analyzed

Complexity

Total Complexity 208

Size/Duplication

Total Lines 1386
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 37
Metric Value
wmc 208
lcom 1
cbo 37
dl 0
loc 1386
rs 0.5217

83 Methods

Rating   Name   Duplication   Size   Complexity  
A setFieldValue() 0 3 1
A getSubmittedOn() 0 3 1
A getSubmittedBy() 0 3 1
A getEmail() 0 3 1
A getValueDao() 0 3 1
A getFormElementFactory() 0 3 1
A getFollowUpDate() 0 3 1
A getHTMLAvatar() 0 3 1
A getAvatarUrl() 0 3 1
A getDateSubmittedOn() 0 3 1
A userCanDeletePermanently() 0 4 1
A userCanEdit() 0 7 4
A getReferenceManager() 0 3 1
A setLatestComment() 0 3 1
A getChangesetDao() 0 3 1
A getCommentDao() 0 3 1
A mailDiffToPrevious() 0 3 1
A modalDiffToPrevious() 0 3 1
A getUserManager() 0 3 1
A getTracker() 0 3 1
A getRecipientFactory() 0 3 1
A getTextAssignedToFilter() 0 15 4
A removeRecipientsThatHaveUnsubscribedArtifactNotification() 0 11 4
A getUserHelper() 0 3 1
A getArtifact() 0 3 1
A getId() 0 3 1
A getRESTFieldValues() 0 11 4
A getUri() 0 3 1
A __construct() 0 7 1
A getValue() 0 6 2
A getChangesetValueFromDB() 0 8 3
A getValues() 0 6 2
A forceFetchAllValues() 0 9 3
A delete() 0 7 2
A deleteValues() 0 10 3
B getFollowupContent() 0 23 5
A fetchFollowUp() 0 20 1
A fetchChangesetActionButtons() 0 15 3
A fetchEditButton() 0 12 2
A fetchIncomingMailButton() 0 20 3
A getImage() 0 12 1
A getSubmitter() 0 10 2
A getSubmitterUrl() 0 12 2
B getFollowUpClassnames() 0 20 7
A shouldBeDisplayedAsChange() 0 8 3
A userCanDelete() 0 7 2
A updateComment() 0 5 2
B updateCommentWithoutNotification() 0 37 3
A getComment() 0 18 3
B hasChanges() 0 21 6
C diffToPrevious() 0 44 12
A displayDiff() 0 16 2
A getTrackerPluginConfig() 0 5 1
A getTrackerConfigNotificationAssignedTo() 0 5 1
A isNotificationAssignedToEnabled() 0 5 1
B notify() 0 41 5
A getLogger() 0 9 1
B buildOneMessageForMultipleRecipients() 0 28 4
B buildAMessagePerRecipient() 0 32 3
A getAnonymousHeaders() 0 6 1
A getDefaultEmailSenderAddress() 0 9 2
A getMessageId() 0 6 1
A getCustomReplyToHeader() 0 16 3
B getMessageContent() 0 28 2
B getUserFromRecipientName() 0 26 3
B sendNotification() 0 48 4
A getTextBodyFilter() 0 6 1
A getHTMLBodyFilter() 0 8 1
A getHTMLAssignedToFilter() 0 14 3
B removeRecipientsThatMayReceiveAnEmptyNotification() 0 16 7
B userCanReadAtLeastOneChangedField() 0 13 6
C getRecipients() 0 40 11
B getBodyText() 0 30 3
C getBodyHtml() 0 75 8
A fetchHtmlAnswerButton() 0 7 1
A getUnsubscribeLink() 0 7 1
A getSubject() 0 6 1
A getSubjectAssignedTo() 0 9 3
A exportCommentToSOAP() 0 7 2
A getSoapMetadata() 0 8 1
B getSoapValue() 0 16 5
B getRESTValue() 0 17 5
A getEmailForUndefinedSubmitter() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like Tracker_Artifact_Changeset often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Tracker_Artifact_Changeset, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) Xerox Corporation, Codendi Team, 2001-2009. All rights reserved
4
 *
5
 * This file is a part of Codendi.
6
 *
7
 * Codendi is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * Codendi is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with Codendi. If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
require_once('common/date/DateHelper.class.php');
22
require_once('common/mail/MailManager.class.php');
23
require_once('common/language/BaseLanguageFactory.class.php');
24
require_once('utils.php');
25
26
class Tracker_Artifact_Changeset extends Tracker_Artifact_Followup_Item {
27
    const DEFAULT_MAIL_SENDER = 'forge__artifacts';
28
29
    const FIELDS_ALL      = 'all';
30
    const FIELDS_COMMENTS = 'comments';
31
32
    public $id;
33
    public $artifact;
34
    public $submitted_by;
35
    public $submitted_on;
36
    public $email;
37
38
    protected $values;
39
40
    /**
41
     * @var Tracker_Artifact_Changeset_Comment
42
     */
43
    private $latest_comment;
44
45
    /**
46
     * Constructor
47
     *
48
     * @param int              $id           The changeset Id
49
     * @param Tracker_Artifact $artifact     The artifact
50
     * @param int              $submitted_by The id of the owner of this changeset
51
     * @param int              $submitted_on The timestamp
52
     * @param string           $email        The email of the submitter if anonymous mode
53
     */
54
    public function __construct($id, $artifact, $submitted_by, $submitted_on, $email) {
55
        $this->id           = $id;
56
        $this->artifact     = $artifact;
57
        $this->submitted_by = $submitted_by;
58
        $this->submitted_on = $submitted_on;
59
        $this->email        = $email;
60
    }
61
62
    /**
63
     * Return the value of a field in the current changeset
64
     *
65
     * @param Tracker_FormElement_Field $field The field
66
     *
67
     * @return Tracker_Artifact_ChangesetValue, or null if not found
0 ignored issues
show
Documentation introduced by
The doc-type Tracker_Artifact_ChangesetValue, could not be parsed: Expected "|" or "end of type", but got "," at position 31. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
68
     */
69
    public function getValue(Tracker_FormElement_Field $field) {
70
        if (! isset($this->values[$field->getId()])) {
71
            $this->values[$field->getId()] = $this->getChangesetValueFromDB($field);
72
        }
73
        return $this->values[$field->getId()];
74
    }
75
76
    private function getChangesetValueFromDB(Tracker_FormElement_Field $field) {
77
        $dar = $this->getValueDao()->searchByFieldId($this->id, $field->getId());
78
        if ($dar && count($dar)) {
79
            $row = $dar->getRow();
80
            return $field->getChangesetValue($this, $row['id'], $row['has_changed']);
81
        }
82
        return null;
83
    }
84
85
    public function setFieldValue(Tracker_FormElement_Field $field, Tracker_Artifact_ChangesetValue $value = null) {
86
        $this->values[$field->getId()] = $value;
87
    }
88
89
    /**
90
     * Returns the submission date of this changeset (timestamp)
91
     *
92
     * @return int The submission date of this changeset (timestamp)
93
     */
94
    public function getSubmittedOn() {
95
        return $this->submitted_on;
96
    }
97
98
    /**
99
     * Returns the author of this changeset
100
     *
101
     * @return int The user id or 0/null if anonymous
102
     */
103
    public function getSubmittedBy() {
104
        return $this->submitted_by;
105
    }
106
107
    /**
108
     * Returns the author's email of this changeset
109
     *
110
     * @return string an email
111
     */
112
    public function getEmail() {
113
        return $this->email;
114
    }
115
116
    /**
117
     * Return the changeset values of this changeset
118
     *
119
     * @return Tracker_Artifact_ChangesetValue[] or empty array if not found
120
     */
121
    public function getValues() {
122
        if (! $this->values) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->values of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
123
            $this->forceFetchAllValues();
124
        }
125
        return $this->values;
126
    }
127
128
    public function forceFetchAllValues() {
129
        $this->values = array();
130
        $factory = $this->getFormElementFactory();
131
        foreach ($this->getValueDao()->searchById($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getValueDao()->searchById($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
132
            if ($field = $factory->getFieldById($row['field_id'])) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $factory->getFieldById($row['field_id']) (which targets Tracker_FormElementFactory::getFieldById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
133
                $this->values[$field->getId()] = $field->getChangesetValue($this, $row['id'], $row['has_changed']);
134
            }
135
        }
136
    }
137
138
    /**
139
     * Delete the changeset
140
     *
141
     * @param PFUser $user the user who wants to delete the changeset
142
     *
143
     * @return void
144
     */
145
    public function delete(PFUser $user) {
146
        if ($this->userCanDeletePermanently($user)) {
147
            $this->getChangesetDao()->delete($this->id);
148
            $this->getCommentDao()->delete($this->id);
149
            $this->deleteValues();
150
        }
151
    }
152
153
    protected function deleteValues() {
154
        $value_dao = $this->getValueDao();
155
        $factory = $this->getFormElementFactory();
156
        foreach ($value_dao->searchById($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $value_dao->searchById($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
157
            if ($field = $factory->getFieldById($row['field_id'])) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $factory->getFieldById($row['field_id']) (which targets Tracker_FormElementFactory::getFieldById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
158
                $field->deleteChangesetValue($row['id']);
159
            }
160
        }
161
        $value_dao->delete($this->id);
162
    }
163
164
    /**
165
     * Returns the ValueDao
166
     *
167
     * @return Tracker_Artifact_Changeset_ValueDao The dao
168
     */
169
    protected function getValueDao() {
170
        return new Tracker_Artifact_Changeset_ValueDao();
171
    }
172
173
    /**
174
     * Returns the Form Element Factory
175
     *
176
     * @return Tracker_FormElementFactory The factory
177
     */
178
    protected function getFormElementFactory() {
179
        return Tracker_FormElementFactory::instance();
180
    }
181
182
    public function getFollowUpDate() {
183
        return $this->submitted_on;
184
    }
185
186
    public function getFollowupContent() {
187
        $html = '';
188
189
        //The comment
190
        if ($comment = $this->getComment()) {
191
            $html .= '<div class="tracker_artifact_followup_comment">';
192
            $html .= $comment->fetchFollowUp();
193
            $html .= '</div>';
194
195
            if ($comment->fetchFollowUp() && $this->diffToPrevious()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $comment->fetchFollowUp() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
196
                $html .= '<hr size="1" />';
197
            }
198
        }
199
200
        //The changes
201
        if ($changes = $this->diffToPrevious()) {
202
            $html .= '<ul class="tracker_artifact_followup_changes">';
203
            $html .= $changes;
204
            $html .= '</ul>';
205
        }
206
207
        return $html;
208
    }
209
210
    /**
211
     * Fetch followup
212
     *
213
     * @return string html
214
     */
215
    public function fetchFollowUp() {
216
        $html = '';
217
218
        $html .= $this->getAvatarIfEnabled();
219
220
        $html .= '<div class="tracker_artifact_followup_header">';
221
        $html .= $this->getPermalink();
222
        $html .= $this->fetchChangesetActionButtons();
223
        $html .= $this->getUserLink();
224
        $html .= $this->getTimeAgo();
225
        $html .= '</div>';
226
227
        // The content
228
        $html .= '<div class="tracker_artifact_followup_content">';
229
        $html .= $this->getFollowupContent();
230
        $html .= '</div>';
231
232
        $html .= '<div style="clear:both;"></div>';
233
        return $html;
234
    }
235
236
    private function fetchChangesetActionButtons() {
237
        $html        = '';
238
        $edit_button = $this->fetchEditButton();
239
        $mail_button = $this->fetchIncomingMailButton();
240
241
        if ($edit_button || $mail_button) {
242
            $html .= '<div class="tracker_artifact_followup_comment_controls">';
243
            $html .= $mail_button;
244
            $html .= ' ';
245
            $html .= $edit_button;
246
            $html .= '</div>';
247
        }
248
249
        return $html;
250
    }
251
252
    private function fetchEditButton() {
253
        if (! $this->userCanEdit()) {
254
            return '';
255
        }
256
257
        $html  = '';
258
        $html .= '<a href="#" class="tracker_artifact_followup_comment_controls_edit">';
259
        $html .= '<button class="btn btn-mini"><i class="icon-edit"></i> ' . $GLOBALS['Language']->getText('plugin_tracker_fieldeditor', 'edit') . '</button>';
260
        $html .= '</a>';
261
262
        return $html;
263
    }
264
265
    private function fetchIncomingMailButton() {
266
        if (! $this->getUserManager()->getCurrentUser()->isSuperUser()) {
267
            return '';
268
        }
269
270
        $retriever = Tracker_Artifact_Changeset_IncomingMailGoldenRetriever::instance();
271
        $raw_mail  = $retriever->getRawMailThatCreatedChangeset($this);
272
        if (! $raw_mail) {
273
            return '';
274
        }
275
276
        $raw_email_button_title = $GLOBALS['Language']->getText('plugin_tracker', 'raw_email_button_title');
277
        $raw_mail               = Codendi_HTMLPurifier::instance()->purify($raw_mail);
278
279
        $html = '<button type="button" class="btn btn-mini tracker_artifact_followup_comment_controls_raw_email" data-raw-email="'. $raw_mail .'">
280
                      <i class="icon-envelope"></i> '. $raw_email_button_title .'
281
                 </button>';
282
283
        return $html;
284
    }
285
286
    public function getImage() {
287
        return $GLOBALS['HTML']->getImage(
288
            'ic/comment.png',
289
            array(
290
                'border' => 0,
291
                'alt'   => 'permalink',
292
                'class' => 'tracker_artifact_followup_permalink',
293
                'style' => 'vertical-align:middle',
294
                'title' => 'Link to this followup - #'. (int) $this->id
295
            )
296
        );
297
    }
298
299
    /**
300
     * @return PFUser
301
     */
302
    public function getSubmitter() {
303
        if ($this->submitted_by) {
304
            return UserManager::instance()->getUserById($this->submitted_by);
305
        } else {
306
            $submitter = UserManager::instance()->getUserAnonymous();
307
            $submitter->setEmail($this->email);
308
309
            return $submitter;
310
        }
311
    }
312
313
    /**
314
     * @return string html
315
     */
316
    public function getSubmitterUrl() {
317
        if ($this->submitted_by) {
318
            $submitter = $this->getSubmitter();
319
            $uh = UserHelper::instance();
320
            $submitter_url = $uh->getLinkOnUser($submitter);
321
        } else {
322
            $hp = Codendi_HTMLPurifier::instance();
323
            $submitter_url = $hp->purify($this->email, CODENDI_PURIFIER_BASIC);
324
        }
325
326
        return $submitter_url;
327
    }
328
329
    /**
330
     * @return string
331
     */
332
    public function getHTMLAvatar() {
333
        return $this->getSubmitter()->fetchHtmlAvatar();
334
    }
335
336
    /**
337
     * @return string
338
     */
339
    public function getAvatarUrl() {
340
        return $this->getSubmitter()->getAvatarUrl();
341
    }
342
343
    /**
344
     * @return string html
345
     */
346
    public function getDateSubmittedOn() {
347
        return DateHelper::timeAgoInWords($this->submitted_on, false, true);
348
    }
349
350
    /**
351
     * @return string
352
     */
353
    public function getFollowUpClassnames() {
354
        $classnames = '';
355
356
        $comment = $this->getComment();
357
        $changes = $this->diffToPrevious();
358
359
        if ($changes || $this->shouldBeDisplayedAsChange($changes, $comment)) {
360
            $classnames .= ' tracker_artifact_followup-with_changes ';
361
        }
362
363
        if ($comment && ! $comment->hasEmptyBody()) {
364
            $classnames .= ' tracker_artifact_followup-with_comment ';
365
        }
366
367
        if ($this->submitted_by && $this->submitted_by < 100) {
368
            $classnames .= ' tracker_artifact_followup-by_system_user ';
369
        }
370
371
        return $classnames;
372
    }
373
374
375
    // This function is used to cover a bug previously introduced where
376
    // artifacts can be updated without changes nor comment. We want to
377
    // display such changesets as if they were only containing changes,
378
    // so we introduced this function to determine wether we're in this
379
    // case or not.
380
    private function shouldBeDisplayedAsChange($changes, $comment) {
381
        if ($comment) {
382
            // Not comment AND no changes
383
            return $comment->hasEmptyBody() && ! $changes;
384
        }
385
386
        return true;
387
    }
388
389
    /**
390
     * Say if a user can permanently (no restore) delete a changeset
391
     *
392
     * @param PFUser $user The user who does the delete
393
     *
394
     * @return boolean true if the user can delete
395
     */
396
    protected function userCanDeletePermanently(PFUser $user) {
397
        // Only tracker admin can edit a comment
398
        return $this->artifact->getTracker()->userIsAdmin($user);
399
    }
400
401
    /**
402
     * Say if a user can delete a changeset
403
     *
404
     * @param PFUser $user The user. If null, the current logged in user will be used.
405
     *
406
     * @return boolean true if the user can delete
407
     */
408
    protected function userCanDelete(PFUser $user = null) {
409
        if (!$user) {
410
            $user = $this->getUserManager()->getCurrentUser();
411
        }
412
        // Only tracker admin can edit a comment
413
        return $user->isSuperUser();
414
    }
415
416
    /**
417
     * Say if a user can edit a comment
418
     *
419
     * @param PFUser $user The user. If null, the current logged in user will be used.
420
     *
421
     * @return boolean true if the user can edit
422
     */
423
    public function userCanEdit(PFUser $user = null) {
424
        if (!$user) {
425
            $user = $this->getUserManager()->getCurrentUser();
426
        }
427
        // Only tracker admin and original submitter (minus anonymous) can edit a comment
428
        return $this->artifact->getTracker()->userIsAdmin($user) || ((int)$this->submitted_by && $user->getId() == $this->submitted_by);
429
    }
430
431
    /**
432
     * Update the content
433
     *
434
     * @param string  $body          The new content
435
     * @param PFUser    $user          The user
436
     * @param String  $comment_format Format of the comment
437
     *
438
     * @return void
439
     */
440
    public function updateComment($body, $user, $comment_format, $timestamp) {
441
        if ($this->updateCommentWithoutNotification($body, $user, $comment_format, $timestamp)) {
442
            $this->notify();
443
        }
444
    }
445
446
    public function updateCommentWithoutNotification($body, $user, $comment_format, $timestamp) {
447
        if ($this->userCanEdit($user)) {
448
            $commentUpdated = $this->getCommentDao()->createNewVersion(
449
                $this->id,
450
                $body,
451
                $user->getId(),
452
                $timestamp,
453
                $this->getComment()->id,
454
                $comment_format
455
            );
456
457
            unset($this->latest_comment);
458
459
            if ($commentUpdated) {
460
                $reference_manager = $this->getReferenceManager();
461
                $reference_manager->extractCrossRef(
462
                    $body,
463
                    $this->artifact->getId(),
464
                    Tracker_Artifact::REFERENCE_NATURE,
465
                    $this->artifact->getTracker()->getGroupID(),
466
                    $user->getId(),
467
                    $this->artifact->getTracker()->getItemName()
468
                );
469
470
                $params = array('group_id'     => $this->getArtifact()->getTracker()->getGroupId(),
471
                                'artifact'     => $this->getArtifact(),
472
                                'changeset_id' => $this->getId(),
473
                                'text'         => $body);
474
475
                EventManager::instance()->processEvent('tracker_followup_event_update', $params);
476
477
                return true;
478
            }
479
        }
480
481
        return false;
482
    }
483
484
    /**
485
     * @return ReferenceManager
486
     */
487
    protected function getReferenceManager() {
488
        return ReferenceManager::instance();
489
    }
490
491
    /**
492
     * Get the comment (latest version)
493
     *
494
     * @return Tracker_Artifact_Changeset_Comment The comment of this changeset, or null if no comments
495
     */
496
    public function getComment() {
497
        if (isset($this->latest_comment)) {
498
            return $this->latest_comment;
499
        }
500
501
        if ($row = $this->getCommentDao()->searchLastVersion($this->id)->getRow()) {
502
            $this->latest_comment = new Tracker_Artifact_Changeset_Comment($row['id'],
503
                                                    $this,
504
                                                    $row['comment_type_id'],
505
                                                    $row['canned_response_id'],
506
                                                    $row['submitted_by'],
507
                                                    $row['submitted_on'],
508
                                                    $row['body'],
509
                                                    $row['body_format'],
510
                                                    $row['parent_id']);
511
        }
512
        return $this->latest_comment;
513
    }
514
515
    /**
516
     *
517
     * @param Tracker_Artifact_Changeset_Comment|-1 $comment
0 ignored issues
show
Documentation introduced by
The doc-type Tracker_Artifact_Changeset_Comment|-1 could not be parsed: Unknown type name "-1" at position 35. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
518
     */
519
    public function setLatestComment($comment) {
520
        $this->latest_comment = $comment;
521
    }
522
523
    /**
524
     * Return the ChangesetDao
525
     *
526
     * @return Tracker_Artifact_ChangesetDao The Dao
527
     */
528
    protected function getChangesetDao() {
529
        return new Tracker_Artifact_ChangesetDao();
530
    }
531
532
    /**
533
     * Returns the comment dao
534
     *
535
     * @return Tracker_Artifact_ChangesetCommentDao The dao
536
     */
537
    protected function getCommentDao() {
538
        return new Tracker_Artifact_Changeset_CommentDao();
539
    }
540
541
    /**
542
     * Returns true if there are changes in fields_data regarding this changeset, false if nothing has changed
543
     *
544
     * @param array $fields_data The data submitted (array of 'field_id' => 'value')
545
     *
546
     * @return boolean true if there are changes in fields_data regarding this changeset, false if nothing has changed
547
     */
548
    public function hasChanges($fields_data) {
549
        $has_changes = false;
550
        $used_fields = $this->getFormElementFactory()->getUsedFields($this->artifact->getTracker());
551
        reset($used_fields);
552
        while (!$has_changes && (list(,$field) = each($used_fields))) {
553
            if (!is_a($field, 'Tracker_FormElement_Field_ReadOnly')) {
554
               if (array_key_exists($field->id, $fields_data)) {
555
                   $current_value = $this->getValue($field);
556
                    if ($current_value) {
557
                        $has_changes = $field->hasChanges($current_value, $fields_data[$field->id]);
558
                    } else {
559
                        //There is no current value in the changeset for the submitted field
560
                        //It means that the field has been added afterwards.
561
                        //Then consider that there is at least one change (the value of the new field).
562
                        $has_changes = true;
563
                    }
564
                }
565
            }
566
        }
567
        return $has_changes;
568
    }
569
570
    /**
571
     * Return mail format diff between this changeset and previous one (HTML code)
572
     *
573
     * @return string The field difference between the previous changeset. or false if no changes
574
     */
575
    public function mailDiffToPrevious($format = 'html', $user = null, $ignore_perms = false) {
576
        return $this->diffToPrevious($format, $user, $ignore_perms, true);
577
    }
578
579
    /**
580
     * Return modal format diff between this changeset and previous one (HTML code)
581
     *
582
     * @return string The field difference between the previous changeset. or false if no changes
583
     */
584
    public function modalDiffToPrevious($format = 'html', $user = null, $ignore_perms = false) {
585
        return $this->diffToPrevious($format, $user, $ignore_perms, false, true);
586
    }
587
588
    /**
589
     * Return diff between this changeset and previous one (HTML code)
590
     *
591
     * @return string The field difference between the previous changeset. or false if no changes
592
     */
593
    public function diffToPrevious($format = 'html', $user = null, $ignore_perms = false, $for_mail = false, $for_modal = false) {
594
        $result             = '';
595
        $factory            = $this->getFormElementFactory();
596
        $previous_changeset = $this->getArtifact()->getPreviousChangeset($this->getId());
597
598
        if (! $previous_changeset) {
599
            return $result;
600
        }
601
602
        foreach ($this->getValues() as $field_id => $current_changeset_value) {
603
            $field = $factory->getFieldById($field_id);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $factory->getFieldById($field_id) (which targets Tracker_FormElementFactory::getFieldById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
604
            if (! $field) {
605
                continue;
606
            }
607
608
            if ( (! $ignore_perms && ! $field->userCanRead($user) ) || ! $current_changeset_value) {
609
                continue;
610
            }
611
612
            if (! $current_changeset_value->hasChanged()) {
613
                continue;
614
            }
615
616
            $previous_changeset_value = $previous_changeset->getValue($field);
617
618
            if (! $previous_changeset_value) {//Case : field added later (ie : artifact already exists) => no value
619
                $diff = $current_changeset_value->nodiff();
620
            } elseif ($for_mail) {
621
                $artifact_id  = $this->getArtifact()->getId();
622
                $changeset_id = $this->getId();
623
624
                $diff = $current_changeset_value->mailDiff($previous_changeset_value, $format, $user, $artifact_id, $changeset_id);
625
            } elseif ($for_modal) {
626
                $diff = $current_changeset_value->modalDiff($previous_changeset_value, $format, $user);
627
            } else {
628
                $diff = $current_changeset_value->diff($previous_changeset_value, $format, $user);
629
            }
630
631
            if ($diff) {
632
                $result .= $this->displayDiff($diff, $format, $field);
633
            }
634
        }
635
        return $result;
636
    }
637
638
    /**
639
    * Display diff messsage
640
    *
641
    * @param String $diff
642
    *
643
    */
644
    public function displayDiff($diff, $format, $field) {
645
        $result = false;
646
        switch($format) {
647
            case 'html':
648
                $result .= '<li>';
649
                $result .= '<span class="tracker_artifact_followup_changes_field"><b>'. $field->getLabel() .'</b></span> ';
650
                $result .= '<span class="tracker_artifact_followup_changes_changes">'. $diff .'</span>';
651
                $result .= '</li>';
652
            break;
653
            default://text
654
                $result .= ' * '.$field->getLabel().' : '.PHP_EOL;
655
                $result .= $diff . PHP_EOL;
656
            break;
657
        }
658
        return $result;
659
    }
660
661
    /**
662
     * Get an instance of UserManager
663
     *
664
     * @return UserManager
665
     */
666
    public function getUserManager() {
667
        return UserManager::instance();
668
    }
669
670
    public function getTracker() {
671
        return $this->artifact->getTracker();
672
    }
673
674
    /**
675
     * @protected for testing purpose
676
     */
677
    protected function getTrackerPluginConfig() {
678
        return new TrackerPluginConfig(
679
            new TrackerPluginConfigDao()
680
        );
681
    }
682
683
    /**
684
     * @return ConfigNotificationAssignedTo
685
     */
686
    private function getTrackerConfigNotificationAssignedTo() {
687
        return new ConfigNotificationAssignedTo(
688
            new ConfigNotificationAssignedToDao()
689
        );
690
    }
691
692
    /**
693
     * @return bool
694
     */
695
    protected function isNotificationAssignedToEnabled() {
696
        $config_notification_assignedto = $this->getTrackerConfigNotificationAssignedTo();
697
        $project                        = $this->getTracker()->getProject();
698
        return $config_notification_assignedto->isAssignedToSubjectEnabled($project->getID());
699
    }
700
701
    /**
702
     * notify people
703
     *
704
     * @return void
705
     */
706
    public function notify() {
707
        $tracker = $this->getTracker();
708
        if ( ! $tracker->isNotificationStopped()) {
709
            $logger = $this->getLogger();
710
            $logger->debug('Start notification');
711
712
            $this->getArtifact()->forceFetchAllChangesets();
713
714
            // 0. Is update
715
            $is_update = ! $this->getArtifact()->isFirstChangeset($this);
716
717
            // 1. Get the recipients list
718
            $recipients = $this->getRecipients($is_update);
719
            $logger->debug('Recipients '.implode(', ', array_keys($recipients)));
720
721
            // 2. Compute the body of the message + headers
722
            $messages = array();
723
724
            $config = $this->getTrackerPluginConfig();
725
            if ($config->isTokenBasedEmailgatewayEnabled() || $this->isNotificationAssignedToEnabled()) {
726
                $messages = $this->buildAMessagePerRecipient($recipients, $is_update);
727
            } else {
728
                $messages = $this->buildOneMessageForMultipleRecipients($recipients, $is_update);
729
            }
730
731
            // 3. Send the notification
732
            foreach ($messages as $message) {
733
                $logger->debug('Notify '.implode(', ', $message['recipients']));
734
                $this->sendNotification(
735
                    $message['recipients'],
736
                    $message['headers'],
737
                    $message['from'],
738
                    $message['subject'],
739
                    $message['htmlBody'],
740
                    $message['txtBody'],
741
                    $message['message-id']
742
                );
743
            }
744
            $logger->debug('End notification');
745
        }
746
    }
747
748
    protected function getLogger() {
749
        return new WrapperLogger(
750
            new TruncateLevelLogger(
751
                new BackendLogger(),
752
                ForgeConfig::get('sys_logger_level')
753
            ),
754
            'art #'.$this->getArtifact()->getId().' - cs #'.$this->getId()
755
        );
756
    }
757
758
    public function buildOneMessageForMultipleRecipients(array $recipients, $is_update) {
759
        $messages = array();
760
        foreach ($recipients as $recipient => $check_perms) {
761
            $user = $this->getUserFromRecipientName($recipient);
762
763
            if ($user) {
764
                $ignore_perms = !$check_perms;
765
                $recipient_mail = $user->getEmail();
766
                $message_content = $this->getMessageContent($user, $is_update, $check_perms);
767
                $headers = array_filter(array($this->getCustomReplyToHeader()));
768
                $hash = md5($message_content['htmlBody'] . $message_content['txtBody'] . serialize($message_content['subject']));
769
770
                if (isset($messages[$hash])) {
771
                    $messages[$hash]['recipients'][] = $recipient_mail;
772
                } else {
773
                    $messages[$hash] = $message_content;
774
775
                    $messages[$hash]['message-id'] = null;
776
                    $messages[$hash]['headers']    = $headers;
777
                    $messages[$hash]['recipients'] = array($recipient_mail);
778
                }
779
780
                $messages[$hash]['from'] = $this->getDefaultEmailSenderAddress();
781
            }
782
        }
783
784
        return $messages;
785
    }
786
787
    public function buildAMessagePerRecipient(array $recipients, $is_update) {
788
        $messages       = array();
789
        $anonymous_mail = 0;
790
791
        foreach ($recipients as $recipient => $check_perms) {
792
            $user = $this->getUserFromRecipientName($recipient);
793
794
            if (! $user->isAnonymous()) {
795
                $headers    = array_filter(array($this->getCustomReplyToHeader()));
796
                $message_id = $this->getMessageId($user);
797
798
                $messages[$message_id]               = $this->getMessageContent($user, $is_update, $check_perms);
799
                $messages[$message_id]['from']       = ForgeConfig::get('sys_name') . '<' .$this->getArtifact()->getTokenBasedEmailAddress() . '>';
800
                $messages[$message_id]['message-id'] = $message_id;
801
                $messages[$message_id]['headers']    = $headers;
802
                $messages[$message_id]['recipients'] = array($user->getEmail());
803
804
            } else {
805
                $headers = array($this->getAnonymousHeaders());
806
807
                $messages[$anonymous_mail]               = $this->getMessageContent($user, $is_update, $check_perms);
808
                $messages[$anonymous_mail]['from']       = $this->getDefaultEmailSenderAddress();
809
                $messages[$anonymous_mail]['message-id'] = null;
810
                $messages[$anonymous_mail]['headers']    = $headers;
811
                $messages[$anonymous_mail]['recipients'] = array($user->getEmail());
812
813
                $anonymous_mail += 1;
814
            }
815
        }
816
817
        return $messages;
818
    }
819
820
    /**
821
     * @return array
822
     */
823
    private function getAnonymousHeaders() {
824
        return array(
825
            "name" => "Reply-to",
826
            "value" => ForgeConfig::get('sys_noreply')
827
        );
828
    }
829
830
    private function getDefaultEmailSenderAddress() {
831
        $email_domain = ForgeConfig::get('sys_default_mail_domain');
832
833
        if (! $email_domain) {
834
            $email_domain = ForgeConfig::get('sys_default_domain');
835
        }
836
837
        return ForgeConfig::get('sys_name') . '<' . self::DEFAULT_MAIL_SENDER . '@' . $email_domain . '>';
838
    }
839
840
    private function getMessageId(PFUser $user) {
841
        $recipient_factory = $this->getRecipientFactory();
842
        $recipient         = $recipient_factory->getFromUserAndChangeset($user, $this);
843
844
        return $recipient->getEmail();
845
    }
846
847
    /**
848
     * @return Tracker_Artifact_MailGateway_RecipientFactory
849
     */
850
    protected function getRecipientFactory() {
851
        return Tracker_Artifact_MailGateway_RecipientFactory::build();
852
    }
853
854
    private function getCustomReplyToHeader() {
855
        $config         = $this->getTrackerPluginConfig();
856
        $artifactbymail = new Tracker_ArtifactByEmailStatus($config);
857
858
        if ($config->isTokenBasedEmailgatewayEnabled()) {
859
            return array(
860
                "name" => "Reply-to",
861
                "value" => $this->getArtifact()->getTokenBasedEmailAddress()
862
            );
863
        } else if ($artifactbymail->canUpdateArtifactInInsecureMode($this->getArtifact()->getTracker())) {
864
            return array(
865
                "name" => "Reply-to",
866
                "value" => $this->getArtifact()->getInsecureEmailAddress()
867
            );
868
        }
869
    }
870
871
    private function getMessageContent($user, $is_update, $check_perms) {
872
        $ignore_perms = !$check_perms;
873
874
        $lang        = $user->getLanguage();
875
876
        $mailManager = new MailManager();
877
        $format      = $mailManager->getMailPreferencesByUser($user);
878
879
880
        $htmlBody = '';
881
        if ($format == Codendi_Mail_Interface::FORMAT_HTML) {
882
            $htmlBody .= $this->getBodyHtml($is_update, $user, $lang, $ignore_perms);
883
            $htmlBody .= $this->getHTMLAssignedToFilter($user);
884
        }
885
886
        $txtBody  = $this->getBodyText($is_update, $user, $lang, $ignore_perms);
887
        $txtBody .= $this->getTextAssignedToFilter($user);
888
        $subject  = $this->getSubject($user, $ignore_perms);
889
890
        $message = array();
891
892
        $message['htmlBody'] = $htmlBody;
893
        $message['txtBody']  = $txtBody;
894
        $message['subject']  = $subject;
895
896
        return $message;
897
898
    }
899
900
    protected function getUserFromRecipientName($recipient_name) {
901
        $um   = $this->getUserManager();
902
        $user = null;
903
        if ( strpos($recipient_name, '@') !== false ) {
904
            //check for registered
905
            $user = $um->getUserByEmail($recipient_name);
906
907
            //user does not exist (not registered/mailing list) then it is considered as an anonymous
908
            if ( ! $user ) {
909
                // don't call $um->getUserAnonymous() as it will always return the same instance
910
                // we don't want to override previous emails
911
                // So create new anonymous instance by hand
912
                $user = $um->getUserInstanceFromRow(
913
                    array(
914
                        'user_id' => 0,
915
                        'email'   => $recipient_name,
916
                    )
917
                );
918
            }
919
        } else {
920
            //is a login
921
            $user = $um->getUserByUserName($recipient_name);
922
        }
923
924
        return $user;
925
    }
926
927
    /**
928
     * Send a notification
929
     *
930
     * @param array  $recipients the list of recipients
931
     * @param array  $headers    the additional headers
932
     * @param string $from       the mail of the sender
933
     * @param string $subject    the subject of the message
934
     * @param string $htmlBody   the html content of the message
935
     * @param string $txtBody    the text content of the message
936
     * @param string $message_id the id of the message
937
     *
938
     * @return void
939
     */
940
    protected function sendNotification($recipients, $headers, $from, $subject, $htmlBody, $txtBody, $message_id) {
941
        $hp                = Codendi_HTMLPurifier::instance();
942
        $breadcrumbs       = array();
943
        $tracker           = $this->getTracker();
944
        $project           = $tracker->getProject();
945
        $artifactId        = $this->getArtifact()->getID();
946
        $project_unix_name = $project->getUnixName(true);
947
        $tracker_name      = $tracker->getItemName();
948
        $mail_enhancer     = new MailEnhancer();
949
950
        if($message_id) {
951
            $mail_enhancer->setMessageId($message_id);
952
        }
953
954
        $breadcrumbs[] = '<a href="'. get_server_url() .'/projects/'. $project_unix_name .'" />'. $project->getPublicName() .'</a>';
955
        $breadcrumbs[] = '<a href="'. get_server_url() .'/plugins/tracker/?tracker='. (int)$tracker->getId() .'" />'. $hp->purify($this->getTracker()->getName()) .'</a>';
956
        $breadcrumbs[] = '<a href="'. get_server_url().'/plugins/tracker/?aid='.(int)$artifactId.'" />'. $hp->purify($this->getTracker()->getName().' #'.$artifactId) .'</a>';
957
958
        $mail_enhancer->addPropertiesToLookAndFeel('breadcrumbs', $breadcrumbs);
959
        $mail_enhancer->addPropertiesToLookAndFeel('unsubscribe_link', $this->getUnsubscribeLink());
960
        $mail_enhancer->addPropertiesToLookAndFeel('title', $hp->purify($subject));
961
        $mail_enhancer->addHeader("X-Codendi-Project",     $project->getUnixName());
962
        $mail_enhancer->addHeader("X-Codendi-Tracker",     $tracker_name);
963
        $mail_enhancer->addHeader("X-Codendi-Artifact-ID", $this->artifact->getId());
964
        $mail_enhancer->addHeader('From', $from);
965
966
        foreach($headers as $header) {
967
            $mail_enhancer->addHeader($header['name'], $header['value']);
968
        }
969
970
        if ($htmlBody) {
971
            $htmlBody .= $this->getHTMLBodyFilter($project_unix_name, $tracker_name);
972
        }
973
974
        $txtBody .= $this->getTextBodyFilter($project_unix_name, $tracker_name);
975
976
        $mail_notification_builder = new MailNotificationBuilder(new MailBuilder(TemplateRendererFactory::build()));
977
        $mail_notification_builder->buildAndSendEmail(
978
            $project,
979
            $recipients,
980
            $subject,
981
            $htmlBody,
982
            $txtBody,
983
            get_server_url().$this->getUri(),
984
            trackerPlugin::TRUNCATED_SERVICE_NAME,
985
            $mail_enhancer
986
        );
987
    }
988
989
    private function getTextBodyFilter($project_name, $tracker_name) {
990
        $project_filter = '=PROJECT='.$project_name;
991
        $tracker_filter = '=TRACKER='.$tracker_name;
992
993
        return PHP_EOL . $project_filter . PHP_EOL . $tracker_filter . PHP_EOL;
994
    }
995
996
    private function getHTMLBodyFilter($project_name, $tracker_name) {
997
        $filter  = '<div style="display: none !important;">';
998
        $filter .= '=PROJECT=' . $project_name . '<br>';
999
        $filter .= '=TRACKER=' . $tracker_name . '<br>';
1000
        $filter .= '</div>';
1001
1002
        return $filter;
1003
    }
1004
1005
    /**
1006
     * @return string
1007
     */
1008
    private function getTextAssignedToFilter(PFUser $recipient) {
1009
        $filter = '';
1010
1011
        if ($this->isNotificationAssignedToEnabled()) {
1012
            $users = $this->getArtifact()->getAssignedTo($recipient);
1013
            foreach ($users as $user) {
1014
                $filter .= PHP_EOL . '=ASSIGNED_TO=' . $user->getUserName();
1015
            }
1016
            if ($filter !== '') {
1017
                $filter .= PHP_EOL;
1018
            }
1019
        }
1020
1021
        return $filter;
1022
    }
1023
1024
    /**
1025
     * @return string
1026
     */
1027
    private function getHTMLAssignedToFilter(PFUser $recipient) {
1028
        $filter = '';
1029
1030
        if ($this->isNotificationAssignedToEnabled()) {
1031
            $filter = '<div style="display: none !important;">';
1032
            $users = $this->getArtifact()->getAssignedTo($recipient);
1033
            foreach ($users as $user) {
1034
                $filter .= '=ASSIGNED_TO=' . $user->getUserName() . '<br>';
1035
            }
1036
            $filter .= '</div>';
1037
        }
1038
1039
        return $filter;
1040
    }
1041
1042
    public function removeRecipientsThatMayReceiveAnEmptyNotification(array &$recipients) {
1043
        if ($this->getComment() && ! $this->getComment()->hasEmptyBody()) {
1044
            return;
1045
        }
1046
1047
        foreach ($recipients as $recipient => $check_perms) {
1048
            if ( ! $check_perms) {
1049
                continue;
1050
            }
1051
1052
            $user = $this->getUserFromRecipientName($recipient);
1053
            if ( ! $user || ! $this->userCanReadAtLeastOneChangedField($user)) {
1054
                unset($recipients[$recipient]);
1055
            }
1056
        }
1057
    }
1058
1059
    public function removeRecipientsThatHaveUnsubscribedArtifactNotification(array &$recipients) {
1060
        $unsubscribers = $this->getArtifact()->getUnsubscribersIds();
1061
1062
        foreach ($recipients as $recipient => $check_perms) {
1063
            $user = $this->getUserFromRecipientName($recipient);
1064
1065
            if (! $user || in_array($user->getId(), $unsubscribers)) {
1066
                unset($recipients[$recipient]);
1067
            }
1068
        }
1069
    }
1070
1071
    private function userCanReadAtLeastOneChangedField(PFUser $user) {
1072
        $factory = $this->getFormElementFactory();
1073
1074
        foreach ($this->getValues() as $field_id => $current_changeset_value) {
1075
            $field = $factory->getFieldById($field_id);
1076
            $field_is_readable = $field && $field->userCanRead($user);
1077
            $field_has_changed = $current_changeset_value && $current_changeset_value->hasChanged();
1078
            if ($field_is_readable && $field_has_changed) {
1079
                return true;
1080
            }
1081
        }
1082
        return false;
1083
    }
1084
1085
    /**
1086
     * Get the recipients for notification
1087
     *
1088
     * @param bool $is_update It is an update, not a new artifact
1089
     *
1090
     * @return array of [$recipient => $checkPermissions] where $recipient is a usenrame or an email and $checkPermissions is bool.
1091
     */
1092
    public function getRecipients($is_update) {
1093
        $factory = $this->getFormElementFactory();
1094
1095
        // 0 Is update
1096
        $is_update = ! $this->getArtifact()->isFirstChangeset($this);
1097
1098
        // 1 Get from the fields
1099
        $recipients = array();
1100
        $this->forceFetchAllValues();
1101
        foreach ($this->getValues() as $field_id => $current_changeset_value) {
1102
            if ($field = $factory->getFieldById($field_id)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $factory->getFieldById($field_id) (which targets Tracker_FormElementFactory::getFieldById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1103
                if ($field->isNotificationsSupported() && $field->hasNotifications() && ($r = $field->getRecipients($current_changeset_value))) {
1104
                    $recipients = array_merge($recipients, $r);
1105
                }
1106
            }
1107
        }
1108
        // 2 Get from the commentators
1109
        $recipients = array_merge($recipients, $this->getArtifact()->getCommentators());
1110
        $recipients = array_values(array_unique($recipients));
1111
1112
1113
        //now force check perms for all this people
1114
        $tablo = array();
1115
        foreach($recipients as $r) {
1116
            $tablo[$r] = true;
1117
        }
1118
1119
        // 3 Get from the global notif
1120
        foreach ($this->getArtifact()->getTracker()->getRecipients() as $r) {
1121
            if ( $r['on_updates'] == 1 || !$is_update ) {
1122
                foreach($r['recipients'] as $recipient) {
1123
                    $tablo[$recipient] = $r['check_permissions'];
1124
                }
1125
            }
1126
        }
1127
        $this->removeRecipientsThatMayReceiveAnEmptyNotification($tablo);
1128
        $this->removeRecipientsThatHaveUnsubscribedArtifactNotification($tablo);
1129
1130
        return $tablo;
1131
    }
1132
1133
    /**
1134
     * Get the text body for notification
1135
     *
1136
     * @param Boolean $is_update    It is an update, not a new artifact
1137
     * @param String  $recipient    The recipient who will receive the notification
0 ignored issues
show
Documentation introduced by
There is no parameter named $recipient. Did you maybe mean $recipient_user?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1138
     * @param BaseLanguage $language The language of the message
1139
     * @param Boolean $ignore_perms indicates if permissions have to be ignored
1140
     *
1141
     * @return String
1142
     */
1143
    public function getBodyText($is_update, $recipient_user, BaseLanguage $language, $ignore_perms) {
1144
        $format = 'text';
1145
        $art = $this->getArtifact();
1146
        $um = $this->getUserManager();
1147
        $user = $um->getUserById($this->submitted_by);
1148
1149
        $output = '+============== '.'['.$art->getTracker()->getItemName() .' #'. $art->getId().'] '.$art->fetchMailTitle($recipient_user, $format, $ignore_perms).' ==============+';
1150
        $output .= PHP_EOL;
1151
        $output .= PHP_EOL;
1152
        $proto = ($GLOBALS['sys_force_ssl']) ? 'https' : 'http';
1153
        $output .= ' <'. $proto .'://'. $GLOBALS['sys_default_domain'] .TRACKER_BASE_URL.'/?aid='. $art->getId() .'>';
1154
        $output .= PHP_EOL;
1155
        $output .= $language->getText('plugin_tracker_include_artifact', 'last_edited');
1156
        $output .= ' '. $this->getUserHelper()->getDisplayNameFromUserId($this->submitted_by);
1157
        $output .= ' on '.DateHelper::formatForLanguage($language, $this->submitted_on);
1158
        if ( $comment = $this->getComment() ) {
1159
            $output .= PHP_EOL;
1160
            $output .= $comment->fetchMailFollowUp($format);
1161
        }
1162
        $output .= PHP_EOL;
1163
        $output .= ' -------------- ' . $language->getText('plugin_tracker_artifact_changeset', 'header_changeset') . ' ---------------- ' ;
1164
        $output .= PHP_EOL;
1165
        $output .= $this->diffToPrevious($format, $recipient_user, $ignore_perms);
1166
        $output .= PHP_EOL;
1167
        $output .= ' -------------- ' . $language->getText('plugin_tracker_artifact_changeset', 'header_artifact') . ' ---------------- ';
1168
        $output .= PHP_EOL;
1169
        $output .= $art->fetchMail($recipient_user, $format, $ignore_perms);
1170
        $output .= PHP_EOL;
1171
        return $output;
1172
    }
1173
    /**
1174
     * Get the html body for notification
1175
     *
1176
     * @param Boolean $is_update    It is an update, not a new artifact
1177
     * @param String  $recipient    The recipient who will receive the notification
0 ignored issues
show
Documentation introduced by
There is no parameter named $recipient. Did you maybe mean $recipient_user?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1178
     * @param BaseLanguage $language The language of the message
1179
     * @param Boolean $ignore_perms ???
1180
     *
1181
     * @return String
1182
     */
1183
    public function getBodyHtml($is_update, $recipient_user, BaseLanguage $language, $ignore_perms) {
1184
        $format = 'html';
1185
        $art = $this->getArtifact();
1186
        $hp = Codendi_HTMLPurifier::instance();
1187
        $followup = '';
1188
        $changes = $this->mailDiffToPrevious($format, $recipient_user, $ignore_perms);
1189
        // Display latest changes (diff)
1190
        if ($comment = $this->getComment()) {
1191
            $followup = $comment->fetchMailFollowUp($format);
1192
        }
1193
1194
        $output =
1195
        '<table style="width:100%">
1196
            <tr>
1197
                <td align="left" colspan="2">
1198
                    <h1>'.$hp->purify($art->fetchMailTitle($recipient_user, $format, $ignore_perms)).'
1199
                    </h1>
1200
                </td>
1201
            </tr>';
1202
1203
        if ($followup || $changes) {
1204
1205
            $output .=
1206
                '<tr>
1207
                    <td colspan="2" align="left">
1208
                        <h2>'.$language->getText('plugin_tracker_artifact_changeset', 'header_html_changeset').'
1209
                        </h2>
1210
                    </td>
1211
                </tr>';
1212
            // Last comment
1213
            if ($followup) {
1214
                $output .= $followup;
1215
            }
1216
            // Last changes
1217
            if ($changes) {
1218
                //TODO check that the following is PHP compliant (what if I made a changes without a comment? -- comment is null)
1219
                if (!empty($comment->body)) {
1220
                    $output .= '
1221
                        <tr>
1222
                            <td colspan="2">
1223
                                <hr size="1" />
1224
                            </td>
1225
                        </tr>';
1226
                }
1227
                $output .=
1228
                    '<tr>
1229
                        <td> </td>
1230
                        <td align="left">
1231
                            <ul>'.
1232
                                $changes.'
1233
                            </ul>
1234
                        </td>
1235
                    </tr>';
1236
            }
1237
1238
            $artifact_link = get_server_url().'/plugins/tracker/?aid='.(int)$art->getId();
1239
1240
            $output .=
1241
                '<tr>
1242
                    <td> </td>
1243
                    <td align="right">'.
1244
                        $this->fetchHtmlAnswerButton($artifact_link).
1245
                        '</span>
1246
                    </td>
1247
                </tr>';
1248
        }
1249
        $output .= '</table>';
1250
1251
        //Display of snapshot
1252
        $snapshot = $art->fetchMail($recipient_user, $format, $ignore_perms);
1253
        if ($snapshot) {
1254
            $output .= $snapshot;
1255
        }
1256
        return $output;
1257
    }
1258
1259
    /**
1260
     * @return string html call to action button to include in an html mail
1261
     */
1262
    private function fetchHtmlAnswerButton($artifact_link) {
1263
        return '<span class="cta">
1264
            <a href="'. $artifact_link .'" target="_blank">' .
1265
                $GLOBALS['Language']->getText('tracker_include_artifact','mail_answer_now') .
1266
            '</a>
1267
        </span>';
1268
    }
1269
1270
    /**
1271
     * @return string html call to action button to include in an html mail
1272
     */
1273
    private function getUnsubscribeLink() {
1274
        $link = get_server_url().'/plugins/tracker/?aid='.(int)$this->getArtifact()->getId().'&func=manage-subscription';
1275
1276
        return '<a href="'. $link .'" target="_blank">' .
1277
            $GLOBALS['Language']->getText('plugin_tracker_artifact','mail_unsubscribe') .
1278
        '</a>';
1279
    }
1280
1281
    /**
1282
     * Wrapper for UserHelper
1283
     *
1284
     * @return UserHelper
1285
     */
1286
    protected function getUserHelper() {
1287
        return UserHelper::instance();
1288
    }
1289
1290
    /**
1291
     * Get the subject for notification
1292
     *
1293
     * @return string
1294
     */
1295
    public function getSubject(PFUser $recipient, $ignore_perms=false) {
1296
        $subject  = '['. $this->getArtifact()->getTracker()->getItemName() .' #'. $this->getArtifact()->getId() .'] ';
1297
        $subject .= $this->getSubjectAssignedTo($recipient);
1298
        $subject .= $this->getArtifact()->fetchMailTitle($recipient, 'text' ,$ignore_perms);
1299
        return $subject;
1300
    }
1301
1302
    /**
1303
     * @return string
1304
     */
1305
    private function getSubjectAssignedTo(PFUser $recipient) {
1306
        if ($this->isNotificationAssignedToEnabled()) {
1307
            $users = $this->getArtifact()->getAssignedTo($recipient);
1308
            if (in_array($recipient, $users, true)) {
1309
                return '[Assigned to me] ';
1310
            }
1311
        }
1312
        return '';
1313
    }
1314
1315
    /**
1316
     * Return the Tracker_Artifact of this changeset
1317
     *
1318
     * @return Tracker_Artifact The artifact of this changeset
1319
     */
1320
    function getArtifact() {
1321
        return $this->artifact;
1322
    }
1323
1324
    /**
1325
     * Returns the Id of this changeset
1326
     *
1327
     * @return int The Id of this changeset
1328
     */
1329
    public function getId() {
1330
        return $this->id;
1331
    }
1332
1333
    public function exportCommentToSOAP() {
1334
        $comment = $this->getComment();
1335
        if ($comment) {
1336
            $soap = $this->getSoapMetadata();
1337
            return $comment->exportToSOAP($soap);
1338
        }
1339
    }
1340
1341
    private function getSoapMetadata() {
1342
        $soap = array(
1343
            'submitted_by' => $this->getSubmittedBy(),
1344
            'email'        => $this->getEmailForUndefinedSubmitter(),
1345
            'submitted_on' => $this->getSubmittedOn(),
1346
        );
1347
        return $soap;
1348
    }
1349
1350
    public function getSoapValue(PFUser $user) {
1351
        $soap    = $this->getSoapMetadata();
1352
        $comment = $this->getComment();
1353
        if (! $comment) {
1354
            $comment = new Tracker_Artifact_Changeset_CommentNull($this);
1355
        }
1356
        $soap['last_comment'] = $comment->getSoapValue();
1357
        $factory = $this->getFormElementFactory();
1358
        foreach ($this->getValueDao()->searchById($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getValueDao()->searchById($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1359
            $field = $factory->getFieldById($row['field_id']);
1360
            if ($field && $field->isCompatibleWithSoap()) {
1361
                $soap['fields'][] = $field->getSoapValue($user, $this);
1362
            }
1363
        }
1364
        return $soap;
1365
    }
1366
1367
    public function getRESTValue(PFUser $user, $fields) {
1368
        $comment = $this->getComment();
1369
        if (! $comment) {
1370
            $comment = new Tracker_Artifact_Changeset_CommentNull($this);
1371
        }
1372
        if ($fields == self::FIELDS_COMMENTS && $comment->hasEmptyBody()) {
1373
            return null;
1374
        }
1375
        $classname_with_namespace = 'Tuleap\Tracker\REST\ChangesetRepresentation';
1376
        $changeset_representation = new $classname_with_namespace;
1377
        $changeset_representation->build(
1378
            $this,
1379
            $comment,
1380
            $fields  == self::FIELDS_COMMENTS  ? array() : $this->getRESTFieldValues($user)
1381
        );
1382
        return $changeset_representation;
1383
    }
1384
1385
    private function getRESTFieldValues(PFUser $user) {
1386
        $values = array();
1387
        $factory = $this->getFormElementFactory();
1388
1389
        foreach ($factory->getUsedFieldsForREST($this->getTracker()) as $field) {
1390
            if ($field && $field->userCanRead($user)) {
1391
                $values[] = $field->getRESTValue($user, $this);
1392
            }
1393
        }
1394
        return array_filter($values);
1395
    }
1396
1397
    private function getEmailForUndefinedSubmitter() {
1398
        if (! $this->getSubmittedBy()) {
1399
            return $this->getEmail();
1400
        }
1401
    }
1402
1403
    /**
1404
     * Link to changeset in interface
1405
     *
1406
     * @return String
1407
     */
1408
    public function getUri() {
1409
        return  TRACKER_BASE_URL.'/?aid='.$this->getArtifact()->getId().'#followup_'.$this->getId();
1410
    }
1411
}
1412