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_FormElement_Field_ArtifactLink   F
last analyzed

Complexity

Total Complexity 230

Size/Duplication

Total Lines 1604
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 35
Metric Value
wmc 230
lcom 2
cbo 35
dl 0
loc 1604
rs 0.5217

90 Methods

Rating   Name   Duplication   Size   Complexity  
A fetchRawValue() 0 4 1
A getSoapAvailableValues() 0 3 1
A getFieldDataFromSoapValue() 0 3 1
A getFieldDataFromRESTValueByField() 0 3 1
A fetchSubmitForOverlay() 0 17 4
A getArtifactLinkId() 0 3 1
A getArrayOfIdsFromString() 0 3 1
A formatNewValuesLikeWebUI() 0 3 1
A cast() 0 3 1
A getCriteriaWhere() 0 3 1
A getCriteriaDao() 0 3 1
A fetchArtifactParentsOptions() 0 16 4
A fetchArtifactCopyMode() 0 3 1
A fetchArtifactValueWithEditionFormIfEditable() 0 3 1
A getHiddenArtifactValueForEdition() 0 3 1
A getValueDao() 0 3 1
A fetchFollowUp() 0 3 1
A fetchRawValueFromChangeset() 0 3 1
A hasChanges() 0 3 1
A getFactoryLabel() 0 3 1
A getFactoryDescription() 0 3 1
A getFactoryIconUseIt() 0 3 1
A getFactoryIconCreate() 0 3 1
A getFactoryUniqueField() 0 3 1
A allLastChangesetValuesRemoved() 0 4 2
A getRuleArtifactId() 0 3 1
A setArtifactFactory() 0 3 1
A getTrackerFactory() 0 3 1
A getTrackerChildrenFromHierarchy() 0 3 1
A getHierarchyFactory() 0 3 1
A getUpdateLinkingDirectionCommand() 0 3 1
A isSourceOfAssociation() 0 4 1
A saveNewChangeset() 0 4 1
A canLinkArtifacts() 0 3 2
A augmentDataFromRequest() 0 16 4
A accept() 0 3 1
A fetchAdminFormElement() 0 15 2
A fetchCriteriaValue() 0 9 2
A fetchChangesetValue() 0 9 2
A fetchCSVChangesetValue() 0 9 2
B getFieldDataFromRESTValue() 0 12 5
A getFieldData() 0 7 1
A fetchArtifactForOverlay() 0 16 3
A getArtifactLinkIdsOfLastChangeset() 0 6 2
A getDataLikeWebUI() 0 6 1
A formatRemovedValuesLikeWebUI() 0 7 2
A getCriteriaFrom() 0 16 3
B buildMatchExpression() 0 33 6
A getQuerySelect() 0 5 1
A getQueryFrom() 0 8 1
B fetchParentSelector() 0 24 4
D fetchHtmlWidget() 0 143 22
A getWidgetTitle() 0 8 2
D process() 0 114 16
B fetchHtmlWidgetMasschange() 0 25 5
A fetchArtifactValue() 0 6 1
C fetchLinks() 0 36 8
A fetchReverseLinks() 0 15 1
A fetchArtifactValueReadOnly() 0 6 1
A fetchLinksReadOnly() 0 23 2
B fetchSubmitValue() 0 23 4
A fetchSubmitValueMasschange() 0 9 1
A fetchTooltipValue() 0 12 3
C fetchMailArtifactValue() 0 33 8
A getChangesetValue() 0 11 2
A getReverseLinks() 0 5 1
A getArtifactLinkInfos() 0 8 2
B getChangesetValues() 0 29 3
A isValid() 0 5 1
B isValidRegardingRequiredProperty() 0 13 5
A getLastChangesetArtifactIds() 0 8 2
B isEmpty() 0 20 7
B validate() 0 20 5
A getArtifactFactory() 0 6 2
A postSaveNewChangeset() 0 11 1
A getProcessChildrenTriggersCommand() 0 6 1
A updateLinkingDirection() 0 13 3
A removeArtifactsFromSubmittedValue() 0 7 1
C getArtifactsFromChangesetValue() 0 25 8
A saveValue() 0 15 2
B getArtifactIdsToLink() 0 24 4
B updateCrossReferences() 0 12 5
A getAddedArtifactIds() 0 8 3
A getRemovedArtifactIds() 0 6 2
A insertCrossReference() 0 7 1
A removeCrossReference() 0 7 1
A getTrackerReferenceManager() 0 6 1
A getLinkedArtifacts() 0 10 3
A getSlicedLinkedArtifacts() 0 16 3
A addArtifactUserCanViewFromId() 0 6 3

How to fix   Complexity   

Complex Class

Complex classes like Tracker_FormElement_Field_ArtifactLink 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_FormElement_Field_ArtifactLink, 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
 * Copyright (c) Enalean, 2015. All Rights Reserved.
5
 *
6
 * This file is a part of Tuleap.
7
 *
8
 * Tuleap is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * Tuleap is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
20
*/
21
22
class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field {
23
24
    const CREATE_NEW_PARENT_VALUE = -1;
25
    const NEW_VALUES_KEY          = 'new_values';
26
27
    /**
28
     * @var Tracker_ArtifactFactory
29
     */
30
    private $artifact_factory;
31
32
    /**
33
     * @var Tracker_Artifact|null
34
     */
35
    private $source_of_association = array();
36
37
    /**
38
     * Display the html form in the admin ui
39
     *
40
     * @return string html
41
     */
42
    protected function fetchAdminFormElement() {
43
        $hp = Codendi_HTMLPurifier::instance();
44
        $html = '';
45
        $value = '';
46
        if ($this->hasDefaultValue()) {
47
            $value = $this->getDefaultValue();
48
        }
49
        $html .= '<input type="text"
50
                         value="'.  $hp->purify($value, CODENDI_PURIFIER_CONVERT_HTML) .'" autocomplete="off" />';
51
        $html .= '<br />';
52
        $html .= '<a href="#">bug #123</a><br />';
53
        $html .= '<a href="#">bug #321</a><br />';
54
        $html .= '<a href="#">story #10234</a>';
55
        return $html;
56
    }
57
58
    /**
59
     * Display the field value as a criteria
60
     *
61
     * @param Tracker_ReportCriteria $criteria
62
     *
63
     * @return string
64
     */
65
    public function fetchCriteriaValue($criteria) {
66
        $html = '<input type="text" name="criteria['. $this->id .']" id="tracker_report_criteria_'. $this->id .'" value="';
67
        if ($criteria_value = $this->getCriteriaValue($criteria)) {
68
            $hp = Codendi_HTMLPurifier::instance();
69
            $html .= $hp->purify($criteria_value, CODENDI_PURIFIER_CONVERT_HTML);
70
        }
71
        $html .= '" />';
72
        return $html;
73
    }
74
75
    /**
76
     * Display the field as a Changeset value.
77
     * Used in report table
78
     *
79
     * @param int $artifact_id the corresponding artifact id
80
     * @param int $changeset_id the corresponding changeset
81
     * @param mixed $value the value of the field
82
     *
83
     * @return string
84
     */
85
    public function fetchChangesetValue($artifact_id, $changeset_id, $value, $report=null, $from_aid = null) {
86
        $arr = array();
87
        $values = $this->getChangesetValues($changeset_id);
88
        foreach ($values as $artifact_link_info) {
89
            $arr[] = $artifact_link_info->getUrl();
90
        }
91
        $html = implode(', ', $arr);
92
        return $html;
93
    }
94
95
    /**
96
     * Display the field as a Changeset value.
97
     * Used in CSV data export.
98
     *
99
     * @param int $artifact_id the corresponding artifact id
100
     * @param int $changeset_id the corresponding changeset
101
     * @param mixed $value the value of the field
102
     *
103
     * @return string
104
     */
105
    public function fetchCSVChangesetValue($artifact_id, $changeset_id, $value, $report) {
106
        $arr = array();
107
        $values = $this->getChangesetValues($changeset_id);
108
        foreach ($values as $artifact_link_info) {
109
            $arr[] = $artifact_link_info->getArtifactId();
110
        }
111
        $html = implode(',', $arr);
112
        return $html;
113
    }
114
115
    /**
116
     * Fetch the value
117
     * @param mixed $value the value of the field
118
     * @return string
119
     */
120
    public function fetchRawValue($value) {
121
        $artifact_id_array = $value->getArtifactIds();
122
        return implode(", ", $artifact_id_array);
123
    }
124
125
    /**
126
     * Get available values of this field for SOAP usage
127
     * Fields like int, float, date, string don't have available values
128
     *
129
     * @return mixed The values or null if there are no specific available values
130
     */
131
    public function getSoapAvailableValues() {
132
        return null;
133
    }
134
135
    /**
136
     * Return data that can be proceced by createArtifact or updateArtifact based on SOAP request
137
     *
138
     * @param stdClass         $soap_value
139
     * @param Tracker_Artifact $artifact
140
     *
141
     * @return array
142
     */
143
    public function getFieldDataFromSoapValue(stdClass $soap_value, Tracker_Artifact $artifact = null) {
144
        return $this->getFieldData($soap_value->field_value->value, $artifact);
145
    }
146
147
148
    /**
149
     * @see Tracker_FormElement_Field::getFieldDataFromRESTValue()
150
     * @param array $value
151
     * @param Tracker_Artifact $artifact
152
     * @return array
153
     * @throws Exception
154
     */
155
    public function getFieldDataFromRESTValue(array $value, Tracker_Artifact $artifact = null) {
156
        if (array_key_exists('links', $value) && is_array($value['links'])){
157
            $link_ids = array();
158
            foreach ($value['links'] as $link) {
159
                if (array_key_exists('id', $link)) {
160
                    $link_ids[] = $link['id'];
161
                }
162
            }
163
            return $this->getFieldData(implode(',', $link_ids), $artifact);
164
        }
165
        throw new Tracker_FormElement_InvalidFieldValueException('Value should be \'links\' and an array of {"id": integer}');
166
    }
167
168
    public function getFieldDataFromRESTValueByField($value, Tracker_Artifact $artifact = null) {
169
        throw new Tracker_FormElement_RESTValueByField_NotImplementedException();
170
    }
171
172
    /**
173
     * Get the field data (SOAP or CSV) for artifact submission
174
     *
175
     * @param string           $string_value The soap field value
176
     * @param Tracker_Artifact $artifact     The artifact the value is to be added/removed
177
     *
178
     * @return array
179
     */
180
    public function getFieldData($string_value, Tracker_Artifact $artifact = null) {
181
        $existing_links   = $this->getArtifactLinkIdsOfLastChangeset($artifact);
182
        $submitted_values = $this->getArrayOfIdsFromString($string_value);
183
        $new_values       = array_diff($submitted_values, $existing_links);
184
        $removed_values   = array_diff($existing_links, $submitted_values);
185
        return $this->getDataLikeWebUI($new_values, $removed_values);
186
    }
187
188
    public function fetchArtifactForOverlay(Tracker_Artifact $artifact) {
189
        $user_manager   = UserManager::instance();
190
        $user           = $user_manager->getCurrentUser();
191
        $parent_tracker = $this->getTracker()->getParent();
192
193
        if ($artifact->getParent($user) || ! $parent_tracker) {
194
            return '';
195
        }
196
197
        $prefill_parent = '';
198
        $name           = 'artifact['. $this->id .']';
199
        $current_user   = $this->getCurrentUser();
200
        $can_create     = false;
201
202
        return $this->fetchParentSelector($prefill_parent, $name, $parent_tracker, $current_user, $can_create);
203
    }
204
205
    public function fetchSubmitForOverlay($submitted_values) {
206
        $prefill_parent = '';
207
        $name           = 'artifact['. $this->id .']';
208
        $parent_tracker = $this->getTracker()->getParent();
209
        $current_user   = $this->getCurrentUser();
210
        $can_create     = false;
211
212
        if (! $parent_tracker) {
213
            return '';
214
        }
215
216
        if (isset($submitted_values['disable_artifact_link_field']) && $submitted_values['disable_artifact_link_field']) {
217
            return '';
218
        }
219
220
        return $this->fetchParentSelector($prefill_parent, $name, $parent_tracker, $current_user, $can_create);
221
    }
222
223
    private function getArtifactLinkIdsOfLastChangeset(Tracker_Artifact $artifact = null) {
224
        if ($artifact) {
225
            return array_map(array($this, 'getArtifactLinkId'), $this->getChangesetValues($artifact->getLastChangeset()->getId()));
226
        }
227
        return array();
228
    }
229
230
    private function getArtifactLinkId(Tracker_ArtifactLinkInfo $link_info) {
231
        return $link_info->getArtifactId();
232
    }
233
234
    private function getArrayOfIdsFromString($value) {
235
        return array_filter(array_map('intval', explode(',', $value)));
236
    }
237
238
    private function getDataLikeWebUI(array $new_values, array $removed_values) {
239
        return array(
240
            'new_values'     => $this->formatNewValuesLikeWebUI($new_values),
241
            'removed_values' => $this->formatRemovedValuesLikeWebUI($removed_values)
242
        );
243
    }
244
245
    private function formatNewValuesLikeWebUI(array $new_values) {
246
        return implode(',', $new_values);
247
    }
248
249
    private function formatRemovedValuesLikeWebUI(array $removed_values) {
250
        $values = array();
251
        foreach ($removed_values as $value) {
252
            $values[$value] = array($value);
253
        }
254
        return $values;
255
    }
256
257
    /**
258
     * Get the "from" statement to allow search with this field
259
     * You can join on 'c' which is a pseudo table used to retrieve
260
     * the last changeset of all artifacts.
261
     *
262
     * @param Tracker_ReportCriteria $criteria
263
     *
264
     * @return string
265
     */
266
    public function getCriteriaFrom($criteria) {
267
        //Only filter query if field is used
268
        if($this->isUsed()) {
269
            //Only filter query if criteria is valuated
270
            if ($criteria_value = $this->getCriteriaValue($criteria)) {
271
                $a = 'A_'. $this->id;
272
                $b = 'B_'. $this->id;
273
                return " INNER JOIN tracker_changeset_value AS $a ON ($a.changeset_id = c.id AND $a.field_id = $this->id )
274
                         INNER JOIN tracker_changeset_value_artifactlink AS $b ON (
275
                            $b.changeset_value_id = $a.id
276
                            AND ". $this->buildMatchExpression("$b.artifact_id", $criteria_value) ."
277
                         ) ";
278
            }
279
        }
280
        return '';
281
    }
282
    protected $pattern = '[+\-]*[0-9]+';
283
    protected function cast($value) {
284
        return (int)$value;
285
    }
286
    protected function buildMatchExpression($field_name, $criteria_value) {
287
        $expr = '';
288
        $matches = array();
289
        if (preg_match('/\/(.*)\//', $criteria_value, $matches)) {
290
291
            // If it is sourrounded by /.../ then assume a regexp
292
            $expr = $field_name." RLIKE ".$this->getCriteriaDao()->da->quoteSmart($matches[1]);
293
        }
294
        if (!$expr) {
295
            $matches = array();
296
            if (preg_match("/^(<|>|>=|<=)\s*($this->pattern)\$/", $criteria_value, $matches)) {
297
                // It's < or >,  = and a number then use as is
298
                $matches[2] = (string)($this->cast($matches[2]));
299
                $expr = $field_name.' '.$matches[1].' '.$matches[2];
300
301
            } else if (preg_match("/^($this->pattern)\$/", $criteria_value, $matches)) {
302
                // It's a number so use  equality
303
                $matches[1] = $this->cast($matches[1]);
304
                $expr = $field_name.' = '.$matches[1];
305
306
            } else if (preg_match("/^($this->pattern)\s*-\s*($this->pattern)\$/", $criteria_value, $matches)) {
307
                // it's a range number1-number2
308
                $matches[1] = (string)($this->cast($matches[1]));
309
                $matches[2] = (string)($this->cast($matches[2]));
310
                $expr = $field_name.' >= '.$matches[1].' AND '.$field_name.' <= '. $matches[2];
311
312
            } else {
313
                // Invalid syntax - no condition
314
                $expr = '1';
315
            }
316
        }
317
        return $expr;
318
    }
319
320
    /**
321
     * Get the "where" statement to allow search with this field
322
     *
323
     * @param Tracker_ReportCriteria $criteria
324
     *
325
     * @return string
326
     */
327
    public function getCriteriaWhere($criteria) {
328
        return '';
329
    }
330
331
    public function getQuerySelect() {
332
        $R1 = 'R1_'. $this->id;
333
        $R2 = 'R2_'. $this->id;
334
        return "$R2.artifact_id AS `". $this->name . "`";
335
    }
336
337
    public function getQueryFrom() {
338
        $R1 = 'R1_'. $this->id;
339
        $R2 = 'R2_'. $this->id;
340
341
        return "LEFT JOIN ( tracker_changeset_value AS $R1
342
                    INNER JOIN tracker_changeset_value_artifactlink AS $R2 ON ($R2.changeset_value_id = $R1.id)
343
                ) ON ($R1.changeset_id = c.id AND $R1.field_id = ". $this->id ." )";
344
    }
345
346
    /**
347
     * Return the dao of the criteria value used with this field.
348
     * @return DataAccessObject
349
     */
350
    protected function getCriteriaDao() {
351
        return new Tracker_Report_Criteria_ArtifactLink_ValueDao();
352
    }
353
354
    private function fetchParentSelector($prefill_parent, $name, Tracker $parent_tracker, PFUser $user, $can_create) {
355
        $purifier = Codendi_HTMLPurifier::instance();
356
        $possible_parents_getr = new Tracker_Artifact_PossibleParentsRetriever($this->getArtifactFactory());
357
        $html     = '';
358
        $html    .= '<p>';
359
        list($label, $paginated_possible_parents, $display_selector) = $possible_parents_getr->getPossibleArtifactParents($parent_tracker, $user, 0, 0);
360
        $possible_parents = $paginated_possible_parents->getArtifacts();
361
        if ($display_selector) {
362
            $html .= '<label>';
363
            $html .= $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_choose_parent', $purifier->purify($parent_tracker->getItemName()));
364
            $html .= '<select name="'. $purifier->purify($name) .'[parent]">';
365
            $html .= '<option value="">'. $GLOBALS['Language']->getText('global', 'please_choose_dashed') .'</option>';
366
            if ($can_create) {
367
                $html .= '<option value="'.self::CREATE_NEW_PARENT_VALUE.'">'. $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_create_new_parent') .'</option>';
368
            }
369
            $html .= $this->fetchArtifactParentsOptions($prefill_parent, $label, $possible_parents);
370
            $html .= '</select>';
371
            $html .= '</label>';
372
        } elseif (count($possible_parents) > 0) {
373
            $html .= $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_will_have_as_parent', array($possible_parents[0]->fetchDirectLinkToArtifactWithTitle()));
374
        }
375
        $html .= '</p>';
376
        return $html;
377
    }
378
379
    private function fetchArtifactParentsOptions($prefill_parent, $label, array $possible_parents) {
380
        $purifier = Codendi_HTMLPurifier::instance();
381
        $html     = '';
382
        if ($possible_parents) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $possible_parents 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...
383
            $html .= '<optgroup label="'. $purifier->purify($label) .'">';
384
            foreach ($possible_parents as $possible_parent) {
385
                $selected = '';
386
                if ($possible_parent->getId() == $prefill_parent) {
387
                    $selected = ' selected="selected"';
388
                }
389
                $html .= '<option value="'. $possible_parent->getId() .'"'.$selected.'>'. $possible_parent->getXRefAndTitle() .'</option>';
390
            }
391
            $html .= '</optgroup>';
392
        }
393
        return $html;
394
    }
395
396
    /**
397
     * Fetch the html widget for the field
398
     *
399
     * @param Tracker_Artifact $artifact               Artifact on which we operate
400
     * @param string           $name                   The name, if any
401
     * @param array            $artifact_links         The current artifact links
402
     * @param string           $prefill_new_values     Prefill new values field (what the user has submitted, if any)
403
     * @param array            $prefill_removed_values Pre-remove values (what the user has submitted, if any)
404
     * @param string           $prefill_parent         Prefilled parent (what the user has submitted, if any) - Only valid on submit
405
     * @param bool             $read_only              True if the user can't add or remove links
406
     *
407
     * @return string html
408
     */
409
    protected function fetchHtmlWidget(
410
        Tracker_Artifact $artifact,
411
        $name,
412
        $artifact_links,
413
        $prefill_new_values,
414
        $prefill_removed_values,
415
        $prefill_parent,
416
        $read_only,
417
        $from_aid = null,
418
        $reverse_artifact_links = false
419
    ) {
420
        $current_user = $this->getCurrentUser();
421
        $html = '';
422
423
        if ($reverse_artifact_links) {
424
            $html .= '<div class="artifact-link-value-reverse">';
425
            $html .= '<a href="" class="btn" id="display-tracker-form-element-artifactlink-reverse">' . $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_display_reverse') . '</a>';
426
            $html .= '<div id="tracker-form-element-artifactlink-reverse" style="display: none">';
427
        } else {
428
            $html .= '<div class="artifact-link-value">';
429
        }
430
431
        $html .= '<h5 class="artifack_link_subtitle">'.$this->getWidgetTitle($reverse_artifact_links).'</h5>';
432
433
        $html_name_new = '';
434
        $html_name_del = '';
435
436
        if ($name) {
437
            $html_name_new = 'name="'. $name .'[new_values]"';
438
            $html_name_del = 'name="'. $name .'[removed_values]';
439
        }
440
441
        $hp              = Codendi_HTMLPurifier::instance();
442
        $read_only_class = 'read-only';
443
444
        if (! $read_only) {
445
            $read_only_class = '';
446
            $html .= '<div><span class="input-append" style="display:inline;"><input type="text"
447
                             '. $html_name_new .'
448
                             class="tracker-form-element-artifactlink-new"
449
                             size="40"
450
                             value="'.  $hp->purify($prefill_new_values, CODENDI_PURIFIER_CONVERT_HTML)  .'"
451
                             title="' . $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_help') . '" />';
452
            $html .= '</span></div>';
453
454
            $parent_tracker = $this->getTracker()->getParent();
455
            $is_submit      = $artifact->getId() == -1;
456
            if ($parent_tracker && $is_submit) {
457
                $can_create   = true;
458
                $html .= $this->fetchParentSelector($prefill_parent, $name, $parent_tracker, $current_user, $can_create);
459
            }
460
        }
461
462
        $html .= '<div class="tracker-form-element-artifactlink-list '.$read_only_class.'">';
463
        if ($artifact_links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $artifact_links 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...
464
            $ids = array();
465
            // build an array of artifact_id / last_changeset_id for fetch renderer method
466
            foreach ($artifact_links as $artifact_link) {
467
                if ($artifact_link->getTracker()->isActive() && $artifact_link->userCanView($current_user)) {
468
                    if (!isset($ids[$artifact_link->getTrackerId()])) {
469
                        $ids[$artifact_link->getTrackerId()] = array(
470
                        'id'                => '',
471
                        'last_changeset_id' => '',
472
                        );
473
                    }
474
                    $ids[$artifact_link->getTrackerId()]['id'] .= $artifact_link->getArtifactId() .',';
475
                    $ids[$artifact_link->getTrackerId()]['last_changeset_id'] .= $artifact_link->getLastChangesetId() .',';
476
                }
477
            }
478
479
            $projects = array();
480
            $this_project_id = $this->getTracker()->getProject()->getGroupId();
481
            foreach ($ids as $tracker_id => $matching_ids) {
482
                //remove last coma
483
                $matching_ids['id'] = substr($matching_ids['id'], 0, -1);
484
                $matching_ids['last_changeset_id'] = substr($matching_ids['last_changeset_id'], 0, -1);
485
486
                $tracker = $this->getTrackerFactory()->getTrackerById($tracker_id);
487
                $project = $tracker->getProject();
488
                if ($tracker->userCanView()) {
489
                    $trf = Tracker_ReportFactory::instance();
490
                    $report = $trf->getDefaultReportsByTrackerId($tracker->getId());
491
                    if ($report) {
492
                        $renderers = $report->getRenderers();
493
                        $renderer_table_found = false;
494
                        // looking for the first table renderer
495
                        foreach ($renderers as $renderer) {
496
                            if ($renderer->getType() === Tracker_Report_Renderer::TABLE) {
497
                                $projects[$project->getGroupId()][$tracker_id] = array(
498
                                    'project'      => $project,
499
                                    'tracker'      => $tracker,
500
                                    'report'       => $report,
501
                                    'renderer'     => $renderer,
502
                                    'matching_ids' => $matching_ids,
503
                                );
504
                                $renderer_table_found = true;
505
                                break;
506
                            }
507
                        }
508
                        if ( ! $renderer_table_found) {
509
                            $html .= $GLOBALS['Language']->getText('plugin_tracker', 'no_reports_available');
510
                        }
511
                    } else {
512
                        $html .= $GLOBALS['Language']->getText('plugin_tracker', 'no_reports_available');
513
                    }
514
                }
515
            }
516
517
            foreach ($projects as $trackers) {
518
                foreach ($trackers as $t) {
519
                    extract($t);
520
521
                    $html .= '<div class="tracker-form-element-artifactlink-trackerpanel">';
522
523
                    $project_name = '';
524
                    if ($project->getGroupId() != $this_project_id) {
525
                        $project_name = ' (<abbr title="'. $hp->purify($project->getPublicName(), CODENDI_PURIFIER_CONVERT_HTML) .'">';
526
                        $project_name .= $hp->purify($project->getUnixName(), CODENDI_PURIFIER_CONVERT_HTML);
527
                        $project_name .= '</abbr>)';
528
                    }
529
                    $html .= '<h2 class="tracker-form-element-artifactlink-tracker_'. $tracker->getId() .'">';
530
                    $html .= $hp->purify($tracker->getName(), CODENDI_PURIFIER_CONVERT_HTML) . $project_name;
531
                    $html .= '</h2>';
532
                    if ($from_aid == null) {
533
                        $html .= $renderer->fetchAsArtifactLink($matching_ids, $this->getId(), $read_only, $prefill_removed_values, false);
534
                    } else {
535
                        $html .= $renderer->fetchAsArtifactLink($matching_ids, $this->getId(), $read_only, $prefill_removed_values, false, $from_aid);
536
                    }
537
                    $html .= '</div>';
538
                }
539
            }
540
        } else {
541
            $html .= $this->getNoValueLabel();
542
        }
543
        $html .= '</div>';
544
545
        if ($reverse_artifact_links) {
546
            $html .= '</div>';
547
        }
548
        $html .= '</div>';
549
550
        return $html;
551
    }
552
553
    /**
554
     *
555
     * @param boolean $reverse_artifact_links
556
     */
557
    private function getWidgetTitle($reverse_artifact_links) {
558
        if ($reverse_artifact_links) {
559
            return $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_reverse_title');
560
        }
561
562
        return $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_title');
563
564
    }
565
566
    /**
567
     * Process the request
568
     *
569
     * @param Tracker_IDisplayTrackerLayout  $layout          Displays the page header and footer
570
     * @param Codendi_Request                $request         The data coming from the user
571
     * @param PFUser                           $current_user    The user who mades the request
572
     *
573
     * @return void
574
     */
575
    public function process(Tracker_IDisplayTrackerLayout $layout, $request, $current_user) {
576
        switch ($request->get('func')) {
577
            case 'fetch-artifacts':
578
                $read_only              = false;
579
                $prefill_removed_values = array();
580
                $only_rows              = true;
581
582
                $this_project_id = $this->getTracker()->getProject()->getGroupId();
583
                $hp = Codendi_HTMLPurifier::instance();
584
585
                $ugroups = $current_user->getUgroups($this_project_id, array());
586
587
                $ids     = $request->get('ids'); //2, 14, 15
588
                $tracker = array();
589
                $result  = array();
590
                //We must retrieve the last changeset ids of each artifact id.
591
                $dao = new Tracker_ArtifactDao();
592
                foreach($dao->searchLastChangesetIds($ids, $ugroups, $current_user->isSuperUser()) as $matching_ids) {
0 ignored issues
show
Bug introduced by
The expression $dao->searchLastChangese...nt_user->isSuperUser()) 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...
593
                    $tracker_id = $matching_ids['tracker_id'];
594
                    $tracker = $this->getTrackerFactory()->getTrackerById($tracker_id);
595
                    $project = $tracker->getProject();
596
597
                    if ($tracker->userCanView()) {
598
                        $trf = Tracker_ReportFactory::instance();
599
                        $report = $trf->getDefaultReportsByTrackerId($tracker->getId());
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $report is correct as $trf->getDefaultReportsB...erId($tracker->getId()) (which targets Tracker_ReportFactory::g...ultReportsByTrackerId()) 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...
600
                        if ($report) {
601
                            $renderers = $report->getRenderers();
602
                            // looking for the first table renderer
603
                            foreach ($renderers as $renderer) {
604
                                if ($renderer->getType() === Tracker_Report_Renderer::TABLE) {
605
                                    $key = $this->id .'_'. $report->id .'_'. $renderer->getId();
606
                                    $result[$key] = $renderer->fetchAsArtifactLink($matching_ids, $this->getId(), $read_only, $prefill_removed_values, $only_rows);
607
                                    $head = '<div class="tracker-form-element-artifactlink-trackerpanel">';
608
609
                                    $project_name = '';
610
                                    if ($project->getGroupId() != $this_project_id) {
611
                                        $project_name = ' (<abbr title="'. $hp->purify($project->getPublicName(), CODENDI_PURIFIER_CONVERT_HTML) .'">';
612
                                        $project_name .= $hp->purify($project->getUnixName(), CODENDI_PURIFIER_CONVERT_HTML);
613
                                        $project_name .= '</abbr>)';
614
                                    }
615
                                    $head .= '<h2 class="tracker-form-element-artifactlink-tracker_'. $tracker->getId() .'">';
616
                                    $head .= $hp->purify($tracker->getName(), CODENDI_PURIFIER_CONVERT_HTML) . $project_name;
617
                                    $head .= '</h2>';
618
                                    //if ($artifact) {
619
                                    //    $title = $hp->purify('link a '. $tracker->getItemName(), CODENDI_PURIFIER_CONVERT_HTML);
620
                                    //    $head .= '<a href="'.TRACKER_BASE_URL.'/?tracker='.$tracker_id.'&func=new-artifact-link&id='.$artifact->getId().'" class="tracker-form-element-artifactlink-link-new-artifact">'. 'create a new '.$hp->purify($tracker->getItemName(), CODENDI_PURIFIER_CONVERT_HTML)  .'</a>';
621
                                    //}
622
                                    $result[$key]['head'] = $head . $result[$key]['head'];
623
                                    break;
624
                                }
625
                            }
626
                        }
627
                    }
628
                }
629
                if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result 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...
630
                    $head = array();
631
                    $rows = array();
632
                    foreach($result as $key => $value) {
633
                        $head[$key] = $value["head"];
634
                        $rows[$key] = $value["rows"];
635
                    }
636
                    header('Content-type: application/json');
637
                    echo json_encode(array('head' => $head, 'rows' => $rows));
638
                }
639
                exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method process() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
640
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
641
            case 'fetch-aggregates':
642
                $read_only              = false;
643
                $prefill_removed_values = array();
644
                $only_rows              = true;
645
                $only_one_column        = false;
646
                $extracolumn            = Tracker_Report_Renderer_Table::EXTRACOLUMN_UNLINK;
647
                $read_only              = true;
648
                $use_data_from_db       = false;
649
650
                $ugroups = $current_user->getUgroups($this->getTracker()->getGroupId(), array());
651
                $ids     = $request->get('ids'); //2, 14, 15
652
                $tracker = array();
653
                $json = array('tabs' => array());
654
                $dao = new Tracker_ArtifactDao();
655
                foreach ($dao->searchLastChangesetIds($ids, $ugroups, $current_user->isSuperUser()) as $matching_ids) {
0 ignored issues
show
Bug introduced by
The expression $dao->searchLastChangese...nt_user->isSuperUser()) 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...
656
                    $tracker_id = $matching_ids['tracker_id'];
657
                    $tracker = $this->getTrackerFactory()->getTrackerById($tracker_id);
658
                    $project = $tracker->getProject();
659
                    if ($tracker->userCanView()) {
660
                        $trf = Tracker_ReportFactory::instance();
661
                        $report = $trf->getDefaultReportsByTrackerId($tracker->getId());
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $report is correct as $trf->getDefaultReportsB...erId($tracker->getId()) (which targets Tracker_ReportFactory::g...ultReportsByTrackerId()) 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...
662
                        if ($report) {
663
                            $renderers = $report->getRenderers();
664
                            // looking for the first table renderer
665
                            foreach ($renderers as $renderer) {
666
                                if ($renderer->getType() === Tracker_Report_Renderer::TABLE) {
667
                                    $key = $this->id . '_' . $report->id . '_' . $renderer->getId();
668
                                    $columns          = $renderer->getTableColumns($only_one_column, $use_data_from_db);
669
                                    $extracted_fields = $renderer->extractFieldsFromColumns($columns);
670
                                    $json['tabs'][] = array(
671
                                        'key' => $key,
672
                                        'src' => $renderer->fetchAggregates($matching_ids, $extracolumn, $only_one_column,$columns, $extracted_fields, $use_data_from_db, $read_only),
673
                                    );
674
                                    break;
675
                                }
676
                            }
677
                        }
678
                    }
679
                }
680
                header('Content-type: application/json');
681
                echo json_encode($json);
682
                exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method process() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
683
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
684
            default:
685
                parent::process($layout, $request, $current_user);
686
                break;
687
        }
688
    }
689
690
    /**
691
     * Fetch the html widget for the field
692
     *
693
     * @param string $name                   The name, if any
694
     * @param array  $artifact_links         The current artifact links
695
     * @param string $prefill_new_values     Prefill new values field (what the user has submitted, if any)
696
     * @param bool   $read_only              True if the user can't add or remove links
697
     *
698
     * @return string html
699
     */
700
    protected function fetchHtmlWidgetMasschange($name, $artifact_links, $prefill_new_values, $read_only) {
701
        $html = '';
702
        $html_name_new = '';
703
        if ($name) {
704
            $html_name_new = 'name="'. $name .'[new_values]"';
705
        }
706
        $hp = Codendi_HTMLPurifier::instance();
707
        if (!$read_only) {
708
            $html .= '<input type="text"
709
                             '. $html_name_new .'
710
                             value="'.  $hp->purify($prefill_new_values, CODENDI_PURIFIER_CONVERT_HTML)  .'"
711
                             title="' . $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_artifactlink_help') . '" />';
712
            $html .= '<br />';
713
        }
714
        if ($artifact_links) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $artifact_links 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...
715
            $html .= '<ul class="tracker-form-element-artifactlink-list">';
716
            foreach ($artifact_links as $artifact_link_info) {
717
                $html .= '<li>';
718
                $html .= $artifact_link_info->getUrl();
719
                $html .= '</li>';
720
            }
721
            $html .= '</ul>';
722
        }
723
        return $html;
724
    }
725
726
    /**
727
     * Fetch the html code to display the field value in artifact
728
     *
729
     * @param Tracker_Artifact                $artifact         The artifact
730
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
731
     * @param array                           $submitted_values The value already submitted by the user
732
     *
733
     * @return string
734
     */
735
    protected function fetchArtifactValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
736
        $links_tab         = $this->fetchLinks($artifact, $value, $submitted_values);
737
        $reverse_links_tab = $this->fetchReverseLinks($artifact);
738
739
        return $links_tab . $reverse_links_tab;
740
    }
741
742
    private function fetchLinks(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
743
        $artifact_links = array();
744
        if ($value != null) {
745
            $artifact_links = $value->getValue();
746
        }
747
748
        if (! empty($submitted_values) && isset($submitted_values[0]) && is_array($submitted_values[0]) && isset($submitted_values[0][$this->getId()])) {
749
            $submitted_value = $submitted_values[0][$this->getId()];
750
        }
751
752
        $prefill_new_values = '';
753
        if (isset($submitted_value['new_values'])) {
754
            $prefill_new_values = $submitted_value['new_values'];
755
        }
756
757
        $prefill_removed_values = array();
758
        if (isset($submitted_value['removed_values'])) {
759
            $prefill_removed_values = $submitted_value['removed_values'];
760
        }
761
762
        $read_only      = false;
763
        $name           = 'artifact['. $this->id .']';
764
        $from_aid       = $artifact->getId();
765
        $prefill_parent = '';
766
767
        return $this->fetchHtmlWidget(
768
            $artifact,
769
            $name,
770
            $artifact_links,
0 ignored issues
show
Bug introduced by
It seems like $artifact_links defined by $value->getValue() on line 745 can also be of type string; however, Tracker_FormElement_Fiel...Link::fetchHtmlWidget() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
771
            $prefill_new_values,
772
            $prefill_removed_values,
773
            $read_only,
774
            $prefill_parent,
775
            $from_aid
776
        );
777
    }
778
779
    private function fetchReverseLinks(Tracker_Artifact $artifact) {
780
        $reverse_links = $this->getReverseLinks($artifact->getId());
781
782
        return $this->fetchHtmlWidget(
783
            $artifact,
784
            '',
785
            $reverse_links,
786
            '',
787
            '',
788
            '',
789
            true,
790
            null,
791
            true
792
        );
793
    }
794
795
    /**
796
     * Fetch the html code to display the field value in artifact in read only mode
797
     *
798
     * @param Tracker_Artifact                $artifact The artifact
799
     * @param Tracker_Artifact_ChangesetValue $value    The actual value of the field
800
     *
801
     * @return string
802
     */
803
    public function fetchArtifactValueReadOnly(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
804
        $links_tab_read_only = $this->fetchLinksReadOnly($artifact, $value);
805
        $reverse_links_tab   = $this->fetchReverseLinks($artifact);
806
807
        return $links_tab_read_only . $reverse_links_tab;
808
    }
809
810
    public function fetchArtifactCopyMode(Tracker_Artifact $artifact, $submitted_values = array()) {
811
        return '';
812
    }
813
814
    public function fetchArtifactValueWithEditionFormIfEditable(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
815
        return $this->getHiddenArtifactValueForEdition($artifact, $value, $submitted_values) . $this->fetchArtifactValueReadOnly($artifact, $value) ;
816
    }
817
818
    public function getHiddenArtifactValueForEdition(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
819
        return "<div class='tracker_hidden_edition_field' data-field-id=" . $this->getId() . ">" . $this->fetchLinks($artifact, $value) . "</div>";
820
    }
821
822
    private function fetchLinksReadOnly(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
823
        $artifact_links = array();
824
825
        if ($value != null) {
826
            $artifact_links = $value->getValue();
827
        }
828
829
        $read_only              = true;
830
        $name                   = '';
831
        $prefill_new_values     = '';
832
        $prefill_removed_values = array();
833
        $prefill_parent         = '';
834
835
        return $this->fetchHtmlWidget(
836
            $artifact,
837
            $name,
838
            $artifact_links,
0 ignored issues
show
Bug introduced by
It seems like $artifact_links defined by $value->getValue() on line 826 can also be of type string; however, Tracker_FormElement_Fiel...Link::fetchHtmlWidget() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
839
            $prefill_new_values,
840
            $prefill_removed_values,
841
            $prefill_parent,
842
            $read_only
843
        );
844
    }
845
846
    /**
847
     * Fetch the html code to display the field value in new artifact submission form
848
     *
849
     * @param array $submitted_values the values already submitted
850
     *
851
     * @return string html
852
     */
853
    protected function fetchSubmitValue($submitted_values = array()) {
854
        $html = '';
855
        $prefill_new_values = '';
856
        if (isset($submitted_values[$this->getId()]['new_values'])) {
857
            $prefill_new_values = $submitted_values[$this->getId()]['new_values'];
858
        } else if ($this->hasDefaultValue()) {
859
            $prefill_new_values = $this->getDefaultValue();
860
        }
861
        $prefill_parent = '';
862
        if (isset($submitted_values[$this->getId()]['parent'])) {
863
            $prefill_parent = $submitted_values[$this->getId()]['parent'];
864
        }
865
        $read_only              = false;
866
        $name                   = 'artifact['. $this->id .']';
867
        $prefill_removed_values = array();
868
        $artifact_links         = array();
869
870
        // Well, shouldn't be here but API doesn't provide a Null Artifact on creation yet
871
        // Here to avoid having to pass null arg for fetchHtmlWidget
872
        $artifact = new Tracker_Artifact(-1, $this->tracker_id, $this->getCurrentUser()->getId(), 0, false);
873
874
        return $this->fetchHtmlWidget($artifact, $name, $artifact_links, $prefill_new_values, $prefill_removed_values, $prefill_parent, $read_only);
875
    }
876
877
    /**
878
     * Fetch the html code to display the field value in masschange submission form
879
     *
880
     * @param array $submitted_values the values already submitted
0 ignored issues
show
Bug introduced by
There is no parameter named $submitted_values. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

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

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

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

Loading history...
881
     *
882
     * @return string html
883
     */
884
    protected function fetchSubmitValueMasschange() {
885
        $html = '';
886
        $prefill_new_values     = $GLOBALS['Language']->getText('global','unchanged');
887
        $read_only              = false;
888
        $name                   = 'artifact['. $this->id .']';
889
        $artifact_links         = array();
890
891
        return $this->fetchHtmlWidgetMasschange($name, $artifact_links, $prefill_new_values, $read_only);
892
    }
893
894
895
    /**
896
     * Fetch the html code to display the field value in tooltip
897
     *
898
     * @param Tracker_Artifact $artifact
899
     * @param Tracker_Artifact_ChangesetValue $value The changeset value of the field
900
     *
901
     * @return string
902
     */
903
    protected function fetchTooltipValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
904
        $html = '';
905
        if ($value != null) {
906
            $html = '<ul>';
907
            $artifact_links = $value->getValue();
908
            foreach($artifact_links as $artifact_link_info) {
0 ignored issues
show
Bug introduced by
The expression $artifact_links of type string|array 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...
909
                $html .= '<li>' . $artifact_link_info->getLabel() . '</li>';
910
            }
911
            $html .= '</ul>';
912
        }
913
        return $html;
914
    }
915
916
    /**
917
     * @return Tracker_FormElement_Field_Value_ArtifactLinkDao
918
     */
919
    protected function getValueDao() {
920
        return new Tracker_FormElement_Field_Value_ArtifactLinkDao();
921
    }
922
923
    /**
924
     * Fetch the html code to display the field value in artifact
925
     *
926
     * @param Tracker_Artifact                $artifact         The artifact
927
     * @param PFUser                          $user             The user who will receive the email
928
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
929
     * @param array                           $submitted_values The value already submitted by the user
0 ignored issues
show
Bug introduced by
There is no parameter named $submitted_values. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

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

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

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

Loading history...
930
     *
931
     * @return string
932
     */
933
    public function fetchMailArtifactValue(
934
        Tracker_Artifact $artifact,
935
        PFUser $user,
936
        Tracker_Artifact_ChangesetValue $value = null,
937
        $format='text'
938
    ) {
939
        if ( empty($value) || !$value->getValue()) {
940
            return '-';
941
        }
942
        $output = '';
943
        switch($format) {
944
            case 'html':
945
                $artifactlink_infos = $value->getValue();
946
                $url = array();
947
                foreach ($artifactlink_infos as $artifactlink_info) {
0 ignored issues
show
Bug introduced by
The expression $artifactlink_infos of type string|array 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...
948
                    if ($artifactlink_info->userCanView($user)) {
949
                        $url[] = $artifactlink_info->getUrl();
950
                    }
951
                }
952
                return implode(' , ', $url);
953
            default:
954
                $output = PHP_EOL;
955
                $artifactlink_infos = $value->getValue();
956
                foreach ($artifactlink_infos as $artifactlink_info) {
0 ignored issues
show
Bug introduced by
The expression $artifactlink_infos of type string|array 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...
957
                    if ($artifactlink_info->userCanView($user)) {
958
                        $output .= $artifactlink_info->getLabel();
959
                        $output .= PHP_EOL;
960
                    }
961
                }
962
                break;
963
        }
964
        return $output;
965
    }
966
967
    /**
968
     * Fetch the value to display changes in followups
969
     *
970
     * @param Tracker_ $artifact
971
     * @param array $from the value(s) *before*
972
     * @param array $to   the value(s) *after*
973
     *
974
     * @return string
975
     */
976
    public function fetchFollowUp($artifact, $from, $to) {
977
        // never used...
978
    }
979
980
    /**
981
     * Fetch the value in a specific changeset
982
     *
983
     * @param Tracker_Artifact_Changeset $changeset
984
     *
985
     * @return string
986
     */
987
    public function fetchRawValueFromChangeset($changeset) {
988
        // never used...
989
    }
990
991
    /**
992
     * Get the value of this field
993
     *
994
     * @param Tracker_Artifact_Changeset $changeset   The changeset (needed in only few cases like 'lud' field)
995
     * @param int                        $value_id    The id of the value
996
     * @param boolean                    $has_changed If the changeset value has changed from the rpevious one
997
     *
998
     * @return Tracker_Artifact_ChangesetValue or null if not found
999
     */
1000
    public function getChangesetValue($changeset, $value_id, $has_changed) {
1001
        $rows                   = $this->getValueDao()->searchById($value_id, $this->id);
1002
        $artifact_links         = $this->getArtifactLinkInfos($rows);
1003
        $reverse_artifact_links = array();
1004
1005
        if ($changeset) {
1006
            $reverse_artifact_links = $this->getReverseLinks($changeset->getArtifact()->getId());
1007
        }
1008
1009
        return new Tracker_Artifact_ChangesetValue_ArtifactLink($value_id, $this, $has_changed, $artifact_links, $reverse_artifact_links);
1010
    }
1011
1012
1013
    private function getReverseLinks($artifact_id) {
1014
        $links_data = $this->getValueDao()->searchReverseLinksById($artifact_id);
1015
1016
        return $this->getArtifactLinkInfos($links_data);
1017
    }
1018
1019
    private function getArtifactLinkInfos($data) {
1020
        $artifact_links = array();
1021
        while ($row = $data->getRow()) {
1022
            $artifact_links[$row['artifact_id']] = new Tracker_ArtifactLinkInfo($row['artifact_id'], $row['keyword'], $row['group_id'], $row['tracker_id'], $row['last_changeset_id']);
1023
        }
1024
1025
        return $artifact_links;
1026
    }
1027
1028
    /**
1029
     * @return array
1030
     */
1031
    protected $artifact_links_by_changeset = array();
1032
1033
    /**
1034
     *
1035
     * @param Integer $changeset_id
1036
     *
1037
     * @return Tracker_ArtifactLinkInfo[]
1038
     */
1039
    protected function getChangesetValues($changeset_id) {
1040
        if (!isset($this->artifact_links_by_changeset[$changeset_id])) {
1041
            $this->artifact_links_by_changeset[$changeset_id] = array();
1042
1043
            $da = CodendiDataAccess::instance();
1044
            $field_id     = $da->escapeInt($this->id);
1045
            $changeset_id = $da->escapeInt($changeset_id);
1046
            $sql = "SELECT cv.changeset_id, cv.has_changed, val.*, a.tracker_id, a.last_changeset_id
1047
                    FROM tracker_changeset_value_artifactlink AS val
1048
                         INNER JOIN tracker_artifact AS a ON(a.id = val.artifact_id)
1049
                         INNER JOIN tracker_changeset_value AS cv
1050
                         ON ( val.changeset_value_id = cv.id
1051
                          AND cv.field_id = $field_id
1052
                          AND cv.changeset_id = $changeset_id
1053
                         )
1054
                    ORDER BY val.artifact_id";
1055
            $dao = new DataAccessObject();
1056
            foreach ($dao->retrieve($sql) as $row) {
0 ignored issues
show
Bug introduced by
The expression $dao->retrieve($sql) 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...
1057
                $this->artifact_links_by_changeset[$row['changeset_id']][] = new Tracker_ArtifactLinkInfo(
1058
                    $row['artifact_id'],
1059
                    $row['keyword'],
1060
                    $row['group_id'],
1061
                    $row['tracker_id'],
1062
                    $row['last_changeset_id']
1063
                );
1064
            }
1065
        }
1066
        return $this->artifact_links_by_changeset[$changeset_id];
1067
    }
1068
1069
    /**
1070
     * Check if there are changes between old and new value for this field
1071
     *
1072
     * @param Tracker_Artifact_ChangesetValue $old_value The data stored in the db
1073
     * @param array                           $new_value array of artifact ids
1074
     *
1075
     * @return bool true if there are differences
1076
     */
1077
    public function hasChanges(Tracker_Artifact_ChangesetValue_ArtifactLink $old_value, $new_value) {
1078
        return $old_value->hasChanges($new_value);
1079
    }
1080
1081
    /**
1082
     * @return the label of the field (mainly used in admin part)
1083
     */
1084
    public static function getFactoryLabel() {
1085
        return $GLOBALS['Language']->getText('plugin_tracker_formelement_admin', 'artifact_link_label');
1086
    }
1087
1088
    /**
1089
     * @return the description of the field (mainly used in admin part)
1090
     */
1091
    public static function getFactoryDescription() {
1092
        return $GLOBALS['Language']->getText('plugin_tracker_formelement_admin', 'artifact_link_description');
1093
    }
1094
1095
    /**
1096
     * @return the path to the icon
1097
     */
1098
    public static function getFactoryIconUseIt() {
1099
        return $GLOBALS['HTML']->getImagePath('ic/artifact-chain.png');
1100
    }
1101
1102
    /**
1103
     * @return the path to the icon
1104
     */
1105
    public static function getFactoryIconCreate() {
1106
        return $GLOBALS['HTML']->getImagePath('ic/artifact-chain--plus.png');
1107
    }
1108
1109
    /**
1110
     * @return bool say if the field is a unique one
1111
     */
1112
    public static function getFactoryUniqueField() {
1113
        return true;
1114
    }
1115
1116
    /**
1117
     * Say if the value is valid. If not valid set the internal has_error to true.
1118
     *
1119
     * @param Tracker_Artifact $artifact The artifact
1120
     * @param array            $value    data coming from the request.
1121
     *
1122
     * @return bool true if the value is considered ok
1123
     */
1124
    public function isValid(Tracker_Artifact $artifact, $value) {
1125
        $this->has_errors = ! $this->validate($artifact, $value);
1126
1127
        return ! $this->has_errors;
1128
    }
1129
1130
    /**
1131
     * Validate a required field
1132
     *
1133
     * @param Tracker_Artifact                $artifact             The artifact to check
1134
     * @param mixed                           $value      The submitted value
1135
     *
1136
     * @return boolean true on success or false on failure
1137
     */
1138
    public function isValidRegardingRequiredProperty(Tracker_Artifact $artifact, $value) {
1139
        if ( (! is_array($value) || empty($value['new_values'])) && $this->isRequired()) {
1140
            if ( ! $this->isEmpty($value, $artifact)) {
1141
                // Field is required but there are values, so field is valid
1142
                $this->has_errors = false;
1143
            } else {
1144
                $this->addRequiredError();
1145
                return false;
1146
            }
1147
        }
1148
1149
        return true;
1150
    }
1151
1152
    /**
1153
     * @return Array the ids
1154
     */
1155
    private function getLastChangesetArtifactIds(Tracker_Artifact $artifact) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
1156
        $lastChangeset = $artifact->getLastChangeset();
1157
        $ids = array();
1158
        if($lastChangeset) {
1159
            $ids = $lastChangeset->getValue($this)->getArtifactIds();
1160
        }
1161
        return $ids;
1162
    }
1163
1164
    /**
1165
     * Say if the submitted value is empty
1166
     * if no last changeset values and empty submitted values : empty
1167
     * if not empty last changeset values and empty submitted values : not empty
1168
     * if empty new values and not empty last changeset values and not empty removed values have the same size: empty
1169
     *
1170
     * @param array            $submitted_value
1171
     * @param Tracker_Artifact $artifact
1172
     *
1173
     * @return bool true if the submitted value is empty
1174
     */
1175
    public function isEmpty($submitted_value, Tracker_Artifact $artifact) {
1176
        $hasNoNewValues           = empty($submitted_value['new_values']);
1177
        $hasNoLastChangesetValues = true;
1178
        $last_changeset_values    = array();
1179
        $last_changeset           = $this->getLastChangesetValue($artifact);
1180
1181
        if ($last_changeset) {
1182
            $last_changeset_values    = $last_changeset->getArtifactIds();
1183
            $hasNoLastChangesetValues = empty($last_changeset_values);
1184
        }
1185
1186
        $hasLastChangesetValues   = !$hasNoLastChangesetValues;
1187
1188
        if (($hasNoLastChangesetValues && $hasNoNewValues) ||
1189
             ($hasLastChangesetValues && $hasNoNewValues
1190
                && $this->allLastChangesetValuesRemoved($last_changeset_values, $submitted_value))) {
1191
            return true;
1192
        }
1193
        return false;
1194
    }
1195
1196
    /**
1197
     * Say if all values of the changeset have been removed
1198
     *
1199
     * @param array $last_changeset_values
1200
     * @param array $submitted_value
1201
     *
1202
     * @return bool true if all values have been removed
1203
     */
1204
    private function allLastChangesetValuesRemoved($last_changeset_values, $submitted_value) {
1205
        return !empty($submitted_value['removed_values'])
1206
            && count($last_changeset_values) == count($submitted_value['removed_values']);
1207
    }
1208
1209
    /**
1210
     * Validate a value
1211
     *
1212
     * @param Tracker_Artifact $artifact The artifact
1213
     * @param string           $value    data coming from the request. Should be artifact id separated by comma
1214
     *
1215
     * @return bool true if the value is considered ok
1216
     */
1217
    protected function validate(Tracker_Artifact $artifact, $value) {
1218
        $is_valid = true;
1219
        if (! isset($value['new_values'])) {
1220
            return $is_valid;
1221
        }
1222
        $new_values = $value['new_values'];
1223
        if (trim($new_values) != '') {
1224
            $r = $this->getRuleArtifactId();
1225
            $art_id_array = explode(',', $new_values);
1226
            foreach ($art_id_array as $artifact_id) {
1227
                $artifact_id = trim ($artifact_id);
1228
                if ( ! $r->isValid($artifact_id)) {
1229
                    $is_valid = false;
1230
                    $GLOBALS['Response']->addFeedback('error', $GLOBALS['Language']->getText('plugin_tracker_common_artifact', 'error_artifactlink_value', array($this->getLabel(), $artifact_id)));
1231
                }
1232
            }
1233
        }
1234
1235
        return $is_valid;
1236
    }
1237
1238
    public function getRuleArtifactId() {
1239
        return new Tracker_Valid_Rule_ArtifactId();
1240
    }
1241
1242
    public function setArtifactFactory(Tracker_ArtifactFactory $artifact_factory) {
1243
        $this->artifact_factory = $artifact_factory;
1244
    }
1245
1246
    /**
1247
     * @return Tracker_ArtifactFactory
1248
     */
1249
    public function getArtifactFactory() {
1250
        if (!$this->artifact_factory) {
1251
            $this->artifact_factory = Tracker_ArtifactFactory::instance();
1252
        }
1253
        return $this->artifact_factory;
1254
    }
1255
1256
    public function getTrackerFactory() {
1257
        return TrackerFactory::instance();
1258
    }
1259
1260
    protected function getTrackerChildrenFromHierarchy(Tracker $tracker) {
1261
        return $this->getHierarchyFactory()->getChildren($tracker->getId());
1262
    }
1263
1264
    /**
1265
     * @return Tracker_HierarchyFactory
1266
     */
1267
    protected function getHierarchyFactory() {
1268
        return Tracker_HierarchyFactory::instance();
1269
    }
1270
1271
    /**
1272
     * @see Tracker_FormElement_Field::postSaveNewChangeset()
1273
     */
1274
    public function postSaveNewChangeset(
1275
        Tracker_Artifact $artifact,
1276
        PFUser $submitter,
1277
        Tracker_Artifact_Changeset $new_changeset,
1278
        Tracker_Artifact_Changeset $previous_changeset = null
1279
    ) {
1280
        $queue = new Tracker_FormElement_Field_ArtifactLink_PostSaveNewChangesetQueue();
1281
        $queue->add($this->getUpdateLinkingDirectionCommand());
1282
        $queue->add($this->getProcessChildrenTriggersCommand());
1283
        $queue->execute($artifact, $submitter, $new_changeset, $previous_changeset);
1284
    }
1285
1286
    /**
1287
     * @protected for testing purpose
1288
     */
1289
    protected function getProcessChildrenTriggersCommand() {
1290
        return new Tracker_FormElement_Field_ArtifactLink_ProcessChildrenTriggersCommand(
1291
            $this,
1292
            $this->getWorkflowFactory()->getTriggerRulesManager()
1293
        );
1294
    }
1295
1296
    private function getUpdateLinkingDirectionCommand() {
1297
        return new Tracker_FormElement_Field_ArtifactLink_UpdateLinkingDirectionCommand($this->source_of_association);
1298
    }
1299
1300
    /**
1301
     * Return true if $artifact_to_check is "parent of" $artifact_reference
1302
     *
1303
     * @todo: take planning into account
1304
     *
1305
     * When $artifact_to_check is a Release
1306
     * And  $artifact_reference is a Sprint
1307
     * And Release -> Sprint (in tracker hierarchy)
1308
     * Then return True
1309
     *
1310
     * @param Tracker_Artifact $artifact_to_check
1311
     * @param Tracker_Artifact $artifact_reference
1312
     *
1313
     * @return Boolean
1314
     */
1315
    protected function isSourceOfAssociation(Tracker_Artifact $artifact_to_check, Tracker_Artifact $artifact_reference) {
1316
        $children = $this->getTrackerChildrenFromHierarchy($artifact_to_check->getTracker());
1317
        return in_array($artifact_reference->getTracker(), $children);
1318
    }
1319
1320
    /**
1321
     * Save the value submitted by the user in the new changeset
1322
     *
1323
     * @param Tracker_Artifact           $artifact         The artifact
1324
     * @param Tracker_Artifact_Changeset $old_changeset    The old changeset. null if it is the first one
1325
     * @param int                        $new_changeset_id The id of the new changeset
1326
     * @param mixed                      $submitted_value  The value submitted by the user
1327
     * @param boolean $is_submission true if artifact submission, false if artifact update
1328
     *
1329
     * @return bool true if success
1330
     */
1331
    public function saveNewChangeset(Tracker_Artifact $artifact, $old_changeset, $new_changeset_id, $submitted_value, PFUser $submitter, $is_submission = false, $bypass_permissions = false) {
1332
        $submitted_value = $this->updateLinkingDirection($artifact, $old_changeset, $submitted_value, $submitter);
1333
        return parent::saveNewChangeset($artifact, $old_changeset, $new_changeset_id, $submitted_value, $submitter, $is_submission, $bypass_permissions);
1334
    }
1335
1336
    /**
1337
     * Verify (and update if needed) that the link between what submitted the user ($submitted_values) and
1338
     * the current artifact is correct resp. the association definition.
1339
     *
1340
     * Given I defined following hierarchy:
1341
     * Release
1342
     * `-- Sprint
1343
     *
1344
     * If $artifact is a Sprint and I try to link a Release, this method detect
1345
     * it and update the corresponding Release with a link toward current sprint
1346
     *
1347
     * @param Tracker_Artifact           $artifact
1348
     * @param Tracker_Artifact_Changeset $old_changeset
1349
     * @param mixed                      $submitted_value
1350
     * @param PFUser                       $submitter
1351
     *
1352
     * @return mixed The submitted value expurged from updated links
1353
     */
1354
    protected function updateLinkingDirection(Tracker_Artifact $artifact, $old_changeset, $submitted_value, PFUser $submitter) {
1355
        $previous_changesetvalue = $this->getPreviousChangesetValue($old_changeset);
1356
        $artifacts               = $this->getArtifactsFromChangesetValue($submitted_value, $previous_changesetvalue);
1357
        $artifact_id_already_linked = array();
1358
        foreach ($artifacts as $artifact_to_add) {
1359
            if ($this->isSourceOfAssociation($artifact_to_add, $artifact)) {
1360
                $this->source_of_association[] = $artifact_to_add;
1361
                $artifact_id_already_linked[] = $artifact_to_add->getId();
1362
            }
1363
        }
1364
1365
        return $this->removeArtifactsFromSubmittedValue($submitted_value, $artifact_id_already_linked);
1366
    }
1367
1368
    /**
1369
     * Remove from user submitted artifact links the artifact ids that where already
1370
     * linked after the direction checking
1371
     *
1372
     * Should be private to the class but almost impossible to test in the context
1373
     * of saveNewChangeset.
1374
     *
1375
     * @param Array $submitted_value
1376
     * @param Array $artifact_id_already_linked
1377
     *
1378
     * @return Array
1379
     */
1380
    public function removeArtifactsFromSubmittedValue($submitted_value, array $artifact_id_already_linked) {
1381
        $new_values = explode(',', $submitted_value['new_values']);
1382
        $new_values = array_map('trim', $new_values);
1383
        $new_values = array_diff($new_values, $artifact_id_already_linked);
1384
        $submitted_value['new_values'] = implode(',', $new_values);
1385
        return $submitted_value;
1386
    }
1387
1388
    protected function getArtifactsFromChangesetValue($value, $previous_changesetvalue = null) {
1389
        $new_values     = (string)$value['new_values'];
1390
        $removed_values = isset($value['removed_values']) ? $value['removed_values'] : array();
1391
        // this array will be the one to save in the new changeset
1392
        $artifact_ids = array();
1393
        if ($previous_changesetvalue != null) {
1394
            $artifact_ids = $previous_changesetvalue->getArtifactIds();
1395
            // We remove artifact links that user wants to remove
1396
            if (is_array($removed_values) && ! empty($removed_values)) {
1397
                $artifact_ids = array_diff($artifact_ids, array_keys($removed_values));
1398
            }
1399
        }
1400
1401
        if (trim($new_values) != '') {
1402
            $new_artifact_ids = array_diff(explode(',', $new_values), array_keys($removed_values));
1403
            // We add new links to existing ones
1404
            foreach ($new_artifact_ids as $new_artifact_id) {
1405
                if ( ! in_array($new_artifact_id, $artifact_ids)) {
1406
                    $artifact_ids[] = (int)$new_artifact_id;
1407
                }
1408
            }
1409
        }
1410
1411
        return $this->getArtifactFactory()->getArtifactsByArtifactIdList($artifact_ids);
1412
    }
1413
1414
    /**
1415
     * Save the value and return the id
1416
     *
1417
     * @param Tracker_Artifact                $artifact                The artifact
1418
     * @param int                             $changeset_value_id      The id of the changeset_value
1419
     * @param mixed                           $value                   The value submitted by the user
1420
     * @param Tracker_Artifact_ChangesetValue $previous_changesetvalue The data previously stored in the db
1421
     *
1422
     */
1423
    protected function saveValue($artifact, $changeset_value_id, $value, Tracker_Artifact_ChangesetValue $previous_changesetvalue = null) {
1424
        $dao = $this->getValueDao();
1425
1426
        foreach ($this->getArtifactIdsToLink($artifact, $value, $previous_changesetvalue) as $artifact_to_be_linked_by_tracker) {
1427
            $tracker = $artifact_to_be_linked_by_tracker['tracker'];
1428
            $dao->create(
1429
                $changeset_value_id,
1430
                $artifact_to_be_linked_by_tracker['ids'],
1431
                $tracker->getItemName(),
1432
                $tracker->getGroupId()
1433
            );
1434
        }
1435
1436
        return $this->updateCrossReferences($artifact, $value);
1437
    }
1438
1439
    /** @return array */
1440
    private function getArtifactIdsToLink(Tracker_Artifact $artifact, $value, Tracker_Artifact_ChangesetValue $previous_changesetvalue = null) {
1441
        $all_artifacts_to_link = $this->getArtifactsFromChangesetValue(
1442
            $value,
1443
            $previous_changesetvalue
1444
        );
1445
1446
        $all_artifact_to_be_linked = array();
1447
        foreach ($all_artifacts_to_link as $artifact_to_link) {
1448
            if ($this->canLinkArtifacts($artifact, $artifact_to_link)) {
1449
                $tracker = $artifact_to_link->getTracker();
1450
1451
                if (! isset($all_artifact_to_be_linked[$tracker->getId()])) {
1452
                    $all_artifact_to_be_linked[$tracker->getId()] = array(
1453
                        'tracker' => $tracker,
1454
                        'ids'     => array()
1455
                    );
1456
                }
1457
1458
                $all_artifact_to_be_linked[$tracker->getId()]['ids'][] = $artifact_to_link->getId();
1459
            }
1460
        }
1461
1462
        return $all_artifact_to_be_linked;
1463
    }
1464
1465
    private function canLinkArtifacts(Tracker_Artifact $src_artifact, Tracker_Artifact $artifact_to_link) {
1466
        return ($src_artifact->getId() != $artifact_to_link->getId()) && $artifact_to_link->getTracker();
1467
    }
1468
1469
    /**
1470
     * Update cross references of this field
1471
     *
1472
     * @param Tracker_Artifact $artifact the artifact that is currently updated
1473
     * @param array            $values   the array of added and removed artifact links ($values['added_values'] is a string and $values['removed_values'] is an array of artifact ids
1474
     *
1475
     * @return boolean
1476
     */
1477
    protected function updateCrossReferences(Tracker_Artifact $artifact, $values) {
1478
        $update_ok = true;
1479
1480
        foreach ($this->getAddedArtifactIds($values) as $added_artifact_id) {
1481
            $update_ok = $update_ok && $this->insertCrossReference($artifact, $added_artifact_id);
1482
        }
1483
        foreach ($this->getRemovedArtifactIds($values) as $removed_artifact_id) {
1484
            $update_ok = $update_ok && $this->removeCrossReference($artifact, $removed_artifact_id);
1485
        }
1486
1487
        return $update_ok;
1488
    }
1489
1490
    private function getAddedArtifactIds(array $values) {
1491
        if (array_key_exists('new_values', $values)) {
1492
            if (trim($values['new_values']) != '') {
1493
                return array_map('intval', explode(',', $values['new_values']));
1494
            }
1495
        }
1496
        return array();
1497
    }
1498
1499
    private function getRemovedArtifactIds(array $values) {
1500
        if (array_key_exists('removed_values', $values)) {
1501
            return array_map('intval', array_keys($values['removed_values']));
1502
        }
1503
        return array();
1504
    }
1505
1506
    private function insertCrossReference(Tracker_Artifact $source_artifact, $target_artifact_id) {
1507
        return $this->getTrackerReferenceManager()->insertBetweenTwoArtifacts(
1508
            $source_artifact,
1509
            $this->getArtifactFactory()->getArtifactById($target_artifact_id),
1510
            $this->getCurrentUser()
1511
        );
1512
    }
1513
1514
    private function removeCrossReference(Tracker_Artifact $source_artifact, $target_artifact_id) {
1515
        return $this->getTrackerReferenceManager()->removeBetweenTwoArtifacts(
1516
            $source_artifact,
1517
            $this->getArtifactFactory()->getArtifactById($target_artifact_id),
1518
            $this->getCurrentUser()
1519
        );
1520
    }
1521
1522
    protected function getTrackerReferenceManager() {
1523
        return new Tracker_ReferenceManager(
1524
            ReferenceManager::instance(),
1525
            Tracker_ArtifactFactory::instance()
1526
        );
1527
    }
1528
1529
    /**
1530
     * Retrieve linked artifacts according to user's permissions
1531
     *
1532
     * @param Tracker_Artifact_Changeset $changeset The changeset you want to retrieve artifact from
1533
     * @param PFUser                       $user      The user who will see the artifacts
1534
     *
1535
     * @return Tracker_Artifact[]
1536
     */
1537
    public function getLinkedArtifacts(Tracker_Artifact_Changeset $changeset, PFUser $user) {
1538
        $artifacts = array();
1539
        $changeset_value = $changeset->getValue($this);
1540
        if ($changeset_value) {
1541
            foreach ($changeset_value->getArtifactIds() as $id) {
1542
                $this->addArtifactUserCanViewFromId($artifacts, $id, $user);
1543
            }
1544
        }
1545
        return $artifacts;
1546
    }
1547
1548
1549
1550
1551
    /**
1552
     * Retrieve sliced linked artifacts according to user's permissions
1553
     *
1554
     * This is nearly the same as a paginated list however, for performance
1555
     * reasons, the total size may be different than the sum of total paginated
1556
     * artifacts.
1557
     *
1558
     * Example to illustrate the difference between paginated and sliced:
1559
     *
1560
     * Given that artifact links are [12, 13, 24, 39, 65, 69]
1561
     * And that the user cannot see artifact #39
1562
     * When I request linked artifacts by bunchs of 2
1563
     * Then I get [[12, 13], [24], [65, 69]]  # instead of [[12, 13], [24, 65], [69]]
1564
     * And total size will be 6               # instead of 5
1565
     *
1566
     * @param Tracker_Artifact_Changeset $changeset The changeset you want to retrieve artifact from
1567
     * @param PFUser                     $user      The user who will see the artifacts
1568
     * @param int                        $limit     The number of artifact to fetch
1569
     * @param int                        $offset    The offset
1570
     *
1571
     * @return Tracker_Artifact_PaginatedArtifacts
1572
     */
1573
    public function getSlicedLinkedArtifacts(Tracker_Artifact_Changeset $changeset, PFUser $user, $limit, $offset) {
1574
        $changeset_value = $changeset->getValue($this);
1575
        if (! $changeset_value) {
1576
            return new Tracker_Artifact_PaginatedArtifacts(array(), 0);
1577
        }
1578
1579
        $artifact_ids = $changeset_value->getArtifactIds();
1580
        $size = count($artifact_ids);
1581
1582
        $artifacts = array();
1583
        foreach (array_slice($artifact_ids, $offset, $limit) as $id) {
1584
            $this->addArtifactUserCanViewFromId($artifacts, $id, $user);
1585
        }
1586
1587
        return new Tracker_Artifact_PaginatedArtifacts($artifacts, $size);
1588
    }
1589
1590
    /** @return Tracker_Artifact|null */
1591
    private function addArtifactUserCanViewFromId(array &$artifacts, $id, PFUser $user) {
1592
        $artifact = $this->getArtifactFactory()->getArtifactById($id);
1593
        if ($artifact && $artifact->userCanView($user)) {
1594
            $artifacts[] = $artifact;
1595
        }
1596
    }
1597
1598
    /**
1599
     * If request come with a 'parent', it should be automagically transformed as
1600
     * 'new_values'.
1601
     * Please note that it only work on artifact creation.
1602
     *
1603
     * @param type $fields_data
1604
     */
1605
    public function augmentDataFromRequest(&$fields_data) {
1606
        $new_values = array();
1607
1608
        if (empty($fields_data[$this->getId()]['parent'])) {
1609
            return;
1610
        }
1611
1612
        $parent = intval($fields_data[$this->getId()]['parent']);
1613
        if ($parent > 0) {
1614
            if (isset($fields_data[$this->getId()]['new_values'])) {
1615
                $new_values   = array_filter(explode(',', $fields_data[$this->getId()]['new_values']));
1616
            }
1617
            $new_values[] = $parent;
1618
            $fields_data[$this->getId()]['new_values'] = implode(',', $new_values);
1619
        }
1620
    }
1621
1622
    public function accept(Tracker_FormElement_FieldVisitor $visitor) {
1623
        return $visitor->visitArtifactLink($this);
1624
    }
1625
}
1626