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

Complexity

Total Complexity 240

Size/Duplication

Total Lines 1383
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 24
Metric Value
wmc 240
lcom 1
cbo 24
dl 0
loc 1383
rs 0.5665

93 Methods

Rating   Name   Duplication   Size   Complexity  
isNone() 0 1 ?
A getDecorators() 0 3 1
A setBind() 0 3 1
A isMultiple() 0 3 1
A getCriteriaWhere() 0 3 1
A getQuerySelect() 0 3 1
A getQuerySelectWithDecorator() 0 3 1
A getQueryFrom() 0 3 1
A getQueryFromWithDecorator() 0 3 1
A getQueryOrderby() 0 3 1
A getQueryGroupby() 0 3 1
A getQuerySelectAggregate() 0 3 1
A getAggregateFunctions() 0 3 1
A getCriteriaDao() 0 3 1
A fetchCriteriaAdditionnalInfo() 0 3 1
A fetchArtifactAdditionnalInfo() 0 3 1
A fetchSubmitAdditionnalInfo() 0 3 1
A criteriaCanBeAdvanced() 0 3 1
A fetchRawValue() 0 3 1
A fetchRawValueFromChangeset() 0 3 1
A getValueDao() 0 3 1
A fetchSubmitValueMasschange() 0 3 1
A fetchArtifactValue() 0 10 4
A fetchArtifactValueReadOnly() 0 15 4
A fetchArtifactValueWithEditionFormIfEditable() 0 3 1
A getWorkflow() 0 3 1
A getSelectedValue() 0 11 4
A getAllValues() 0 3 1
A fetchFieldContainerEnd() 0 3 1
A getMaxSize() 0 3 1
A afterCreate() 0 11 4
A shouldBeBindXML() 0 3 1
A getListDao() 0 3 1
A getBindFactory() 0 3 1
A saveValue() 0 3 1
A getSoapBindingProperties() 0 4 1
A getFieldDataFromRESTValueByField() 0 3 1
A getFieldDataFromSoapValue() 0 15 4
A getFieldData() 0 17 4
A getRecipients() 0 3 1
A getTransitionId() 0 3 1
A getDefaultRESTValue() 0 3 1
A isPossibleValue() 0 14 4
A isEmpty() 0 3 1
A fixOriginalValueIds() 0 3 1
A addBindValue() 0 3 1
A getFormElementDataForCreation() 0 9 2
A getBind() 0 13 3
A duplicate() 0 8 2
A getCriteriaFrom() 0 6 2
A fetchChangesetValue() 0 14 3
A fetchCSVChangesetValue() 0 7 2
B getCriteriaValue() 0 20 7
B setCriteriaValueFromSOAP() 0 23 5
C setCriteriaValueFromREST() 0 39 8
A getFormattedCriteriaValue() 0 6 2
D fetchCriteriaValue() 0 41 9
A fetchSubmitValue() 0 11 2
A getSubmitDefaultValues() 0 7 2
B fetchMailArtifactValue() 0 20 6
A fieldHasEnableWorkflow() 0 7 3
A fieldHasDefineWorkflow() 0 7 2
D validate() 0 67 17
A isTransitionValid() 0 10 3
B getVisibleValuesPlusNoneIfAny() 0 15 5
A getListValueById() 0 7 3
B getFirstValueFor() 0 11 5
D _fetchField() 0 54 14
A fetchFieldContainerStart() 0 11 2
A fetchFieldValue() 0 11 3
C _fetchFieldMasschange() 0 37 7
C fetchFollowUp() 0 25 7
A fetchAdminFormElement() 0 5 1
A fetchTooltipValue() 0 7 2
A fetchCardValue() 0 15 3
A processUpdate() 0 7 2
A exportToXml() 0 14 3
A continueGetInstanceFromXML() 0 12 2
A afterSaveObject() 0 5 1
A getChangesetValue() 0 14 3
A getSoapAvailableValues() 0 8 2
A getFieldDataFromRESTValue() 0 7 3
B hasChanges() 0 17 5
A isNotificationsSupported() 0 6 2
A permission_is_authorized() 0 5 1
A userCanMakeTransition() 0 11 3
A getDefaultValue() 0 7 2
A isValid() 0 5 2
A checkValueExists() 0 5 3
A isValidRegardingRequiredProperty() 0 9 3
A process() 0 6 2
A fetchFormattedForJson() 0 5 1
A getRESTAvailableValues() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Tracker_FormElement_Field_List 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_List, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) Enalean, 2014 - 2015. All Rights Reserved.
4
 * Copyright (c) Xerox Corporation, Codendi Team, 2001-2009. 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
23
abstract class Tracker_FormElement_Field_List extends Tracker_FormElement_Field implements Tracker_FormElement_Field_Shareable {
24
25
    const NONE_VALUE          = 100;
26
    const NOT_INDICATED_VALUE = 0;
27
28
    protected $bind;
29
30
    /**
31
     * @return array
32
     */
33
    public function getFormElementDataForCreation($parent_id) {
34
        $form_element_data = parent::getFormElementDataForCreation($parent_id);
35
36
        if ($this->getBind()) {
37
            $form_element_data['bind-type'] = $this->getBind()->getType();
38
        }
39
40
        return $form_element_data;
41
    }
42
43
    /**
44
     * Return true if submitted value is None
45
     */
46
    abstract public function isNone($value);
47
48
    /**
49
     * @return Tracker_FormElement_Field_List_Bind
50
     */
51
    public function getBind() {
52
        if (!$this->bind) {
53
            $this->bind = null;
54
            //retrieve the type of the bind first...
55
            $dao = new Tracker_FormElement_Field_ListDao();
56
            if ($row = $dao->searchByFieldId($this->id)->getRow()) {
57
                //...and build the bind
58
                $bf = new Tracker_FormElement_Field_List_BindFactory();
59
                $this->bind = $bf->getBind($this, $row['bind_type']);
60
            }
61
        }
62
        return $this->bind;
63
    }
64
65
    /**
66
     * @return array of Tracker_FormElement_Field_List_BindDecorator
67
     */
68
    public function getDecorators() {
69
        return $this->getBind()->getDecorators();
70
    }
71
72
    public function setBind($bind) {
73
        $this->bind = $bind;
74
    }
75
76
    /**
77
     * Duplicate a field. If the field has custom properties,
78
     * they should be propagated to the new one
79
     * @param int $from_field_id
80
     * @return array the mapping between old values and new ones
81
     */
82
    public function duplicate($from_field_id) {
83
        $dao = new Tracker_FormElement_Field_ListDao();
84
        if ($dao->duplicate($from_field_id, $this->id)) {
85
            $bf = new Tracker_FormElement_Field_List_BindFactory();
86
            return $bf->duplicate($from_field_id, $this->id);
87
        }
88
        return array();
89
    }
90
91
    /**
92
     * @return boolean
93
     */
94
    public function isMultiple() {
95
        return false;
96
    }
97
98
    /**
99
     * Get the "from" statement to allow search with this field
100
     * You can join on 'c' which is a pseudo table used to retrieve
101
     * the last changeset of all artifacts.
102
     *
103
     * @param Tracker_ReportCriteria $criteria
104
     *
105
     * @return string
106
     */
107
    public function getCriteriaFrom($criteria) {
108
        //Only filter query if field is used
109
        if($this->isUsed()) {
110
            return $this->getBind()->getCriteriaFrom($this->getCriteriaValue($criteria));
111
        }
112
    }
113
114
    /**
115
     * Get the "where" statement to allow search with this field
116
     *
117
     * @see getCriteriaFrom
118
     *
119
     * @param Tracker_ReportCriteria $criteria
120
     *
121
     * @return string
122
     */
123
    public function getCriteriaWhere($criteria) {
124
        return $this->getBind()->getCriteriaWhere($this->getCriteriaValue($criteria));
125
    }
126
127
    /**
128
     * Get the "select" statement to retrieve field values
129
     *
130
     * @see getQueryFrom
131
     *
132
     * @return string
133
     */
134
    public function getQuerySelect() {
135
        return $this->getBind()->getQuerySelect();
136
    }
137
138
    /**
139
     * Get the "select" statement to retrieve field values with the RGB values of their decorator
140
     * Has no sense for fields other than lists
141
     * @return string
142
     * @see getQueryFrom
143
     */
144
    public function getQuerySelectWithDecorator() {
145
        return $this->getBind()->getQuerySelectWithDecorator();
146
    }
147
148
    /**
149
     * Get the "from" statement to retrieve field values
150
     * You can join on artifact AS a, tracker_changeset AS c
151
     * which tables used to retrieve the last changeset of matching artifacts.
152
     * @return string
153
     */
154
    public function getQueryFrom() {
155
        return $this->getBind()->getQueryFrom();
156
    }
157
158
	/**
159
     * Get the "from" statement to retrieve field values
160
     * You can join on artifact AS a, tracker_changeset AS c
161
     * which tables used to retrieve the last changeset of matching artifacts.
162
     * @return string
163
     */
164
    public function getQueryFromWithDecorator() {
165
        return $this->getBind()->getQueryFromWithDecorator();
166
    }
167
168
    /**
169
     * Get the "order by" statement to retrieve field values
170
     */
171
    public function getQueryOrderby() {
172
        return $this->getBind()->getQueryOrderby();
173
    }
174
175
    /**
176
     * Get the "group by" statement to retrieve field values
177
     */
178
    public function getQueryGroupby() {
179
        return $this->getBind()->getQueryGroupby();
180
    }
181
182
    /**
183
     * Fetch sql snippets needed to compute aggregate functions on this field.
184
     *
185
     * @param array $functions The needed function. @see getAggregateFunctions
186
     *
187
     * @return array of the form array('same_query' => string(sql snippets), 'separate' => array(sql snippets))
188
     *               example:
189
     *               array(
190
     *                   'same_query'       => "AVG(R2_1234.value) AS velocity_AVG, STD(R2_1234.value) AS velocity_AVG",
191
     *                   'separate_queries' => array(
192
     *                       array(
193
     *                           'function' => 'COUNT_GRBY',
194
     *                           'select'   => "R2_1234.value AS label, count(*) AS value",
195
     *                           'group_by' => "R2_1234.value",
196
     *                       ),
197
     *                       //...
198
     *                   )
199
     *              )
200
     *
201
     *              Same query handle all queries that can be run concurrently in one query. Example:
202
     *               - numeric: avg, count, min, max, std, sum
203
     *               - selectbox: count
204
     *              Separate queries handle all queries that must be run spearately on their own. Example:
205
     *               - numeric: count group by
206
     *               - selectbox: count group by
207
     *               - multiselectbox: all (else it breaks other computations)
208
     */
209
    public function getQuerySelectAggregate($functions) {
210
        return $this->getBind()->getQuerySelectAggregate($functions);
211
    }
212
213
    /**
214
     * @return array the available aggreagate functions for this field. empty array if none or irrelevant.
215
     */
216
    public function getAggregateFunctions() {
217
        return array('COUNT', 'COUNT_GRBY');
218
    }
219
220
    /**
221
     * Return the dao of the criteria value used with this field.
222
     * @return Tracker_Report_Criteria_List_ValueDao
223
     */
224
    protected function getCriteriaDao() {
225
        return new Tracker_Report_Criteria_List_ValueDao();
226
    }
227
228
    /**
229
     * Display the field as a Changeset value.
230
     * Used in report table
231
     * @param int $artifact_id the corresponding artifact id
232
     * @param int $changeset_id the corresponding changeset
233
     * @param mixed $value the value of the field
234
     * @return string
235
     */
236
    public function fetchChangesetValue($artifact_id, $changeset_id, $value, $report=null, $from_aid = null) {
237
238
        //We have to fetch all values of the changeset as we are a list of value
239
        //This is the case only if we are multiple but an old changeset may
240
        //contain multiple values
241
        $values = array();
242
        foreach($this->getBind()->getChangesetValues($changeset_id) as $v) {
243
            $val = $this->getBind()->formatChangesetValue($v);
244
            if ($val != '') {
245
                $values[] = $val;
246
            }
247
        }
248
        return implode(', ', $values);
249
    }
250
251
    /**
252
     * Display the field as a Changeset value.
253
     * Used in CSV data export.
254
     *
255
     * @param int $artifact_id the corresponding artifact id
256
     * @param int $changeset_id the corresponding changeset
257
     * @param mixed $value the value of the field
258
     *
259
     * @return string
260
     */
261
    public function fetchCSVChangesetValue($artifact_id, $changeset_id, $value, $report) {
262
        $values = array();
263
        foreach($this->getBind()->getChangesetValues($changeset_id) as $v) {
264
            $values[] = $this->getBind()->formatChangesetValueForCSV($v);
265
        }
266
        return implode(',', $values);
267
    }
268
269
    /**
270
     * Search in the db the criteria value used to search against this field.
271
     * @param Tracker_ReportCriteria $criteria
272
     * @return mixed
273
     */
274
    public function getCriteriaValue($criteria) {
275
        if (empty($this->criteria_value) || empty($this->criteria_value[$criteria->report->id])) {
276
            $this->criteria_value = array();
277
278
            if (empty($this->criteria_value[$criteria->report->id])) {
279
                $this->criteria_value[$criteria->report->id] = array();
280
281
                if ($criteria->id > 0) {
282
                    foreach($this->getCriteriaDao()->searchByCriteriaId($criteria->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getCriteriaDao()-...iteriaId($criteria->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
283
                        $this->criteria_value[$criteria->report->id][] = $row['value'];
284
                    }
285
                }
286
            }
287
288
        } else if (in_array('', $this->criteria_value[$criteria->report->id])) {
289
            return '';
290
        }
291
292
        return $this->criteria_value[$criteria->report->id];
293
    }
294
295
    public function setCriteriaValueFromSOAP(Tracker_Report_Criteria $criteria, StdClass $soap_criteria_value) {
296
        $soap_criteria_values   = explode(',', $soap_criteria_value->value);
297
        $available_field_values = $this->getAllValues();
298
        $values                 = array();
299
        $criterias              = array();
300
301
        foreach ($available_field_values as $field_value_id => $field_value) {
302
            $values[$field_value->getLabel()] = $field_value_id;
303
        }
304
305
        foreach ($soap_criteria_values as $soap_criteria_value) {
306
            // Check if the SOAP string only contains digits
307
            if (ctype_digit($soap_criteria_value)) {
308
                $criterias[] = $soap_criteria_value;
309
            } else {
310
                $field_value_id = $values[$soap_criteria_value];
311
                if ($field_value_id) {
312
                    $criterias[] = $field_value_id;
313
                }
314
            }
315
        }
316
        $this->setCriteriaValue($criterias, $criteria->report->id);
317
    }
318
319
    /**
320
     * @throws Tracker_Report_InvalidRESTCriterionException
321
     */
322
    public function setCriteriaValueFromREST(Tracker_Report_Criteria $criteria, array $rest_criteria_value) {
323
        $searched_field_values = $rest_criteria_value[Tracker_Report_REST::VALUE_PROPERTY_NAME];
324
        $operator              = $rest_criteria_value[Tracker_Report_REST::OPERATOR_PROPERTY_NAME];
325
326
        if ($operator !== Tracker_Report_REST::OPERATOR_CONTAINS) {
327
            throw new Tracker_Report_InvalidRESTCriterionException("Unallowed operator for criterion field '$this->name' ($this->id). Allowed operators: [" . Tracker_Report_REST::OPERATOR_CONTAINS . "]");
328
        }
329
330
        if (is_numeric($searched_field_values)) {
331
            $values_to_match = array((int) $searched_field_values);
332
        } elseif(is_array($searched_field_values)) {
333
            $values_to_match = $searched_field_values;
334
        } else {
335
            throw new Tracker_Report_InvalidRESTCriterionException("Invalid format for criterion field '$this->name' ($this->id)");
336
        }
337
338
        $available_field_values = $this->getAllValues();
339
        $criterias              = array();
340
341
        foreach ($values_to_match as $value_to_match) {
342
            if (! is_numeric($value_to_match)) {
343
                throw new Tracker_Report_InvalidRESTCriterionException("Invalid format for criterion field '$this->name' ($this->id)");
344
            }
345
346
            if ($value_to_match == self::NONE_VALUE) {
347
                continue;
348
            }
349
350
            if (! isset($available_field_values[$value_to_match])) {
351
                continue;
352
            }
353
354
            $criterias[] = $value_to_match;
355
        }
356
357
        $this->setCriteriaValue($criterias, $criteria->report->id);
358
359
        return count($criterias) > 0;
360
    }
361
362
    /**
363
     * Format the criteria value submitted by the user for storage purpose (dao or session)
364
     *
365
     * @param mixed $value The criteria value submitted by the user
366
     *
367
     * @return mixed
368
     */
369
    public function getFormattedCriteriaValue($value) {
370
        if ( empty($value['values']) ) {
371
            $value['values'] = array('');
372
        }
373
        return $value['values'];
374
    }
375
376
    /**
377
     * Display the field value as a criteria
378
     * @param Tracker_ReportCriteria $criteria
379
     * @return string
380
     * @see fetchCriteria
381
     */
382
    public function fetchCriteriaValue($criteria) {
383
        $hp = Codendi_HTMLPurifier::instance();
384
        $html = '';
385
        $criteria_value = $this->getCriteriaValue($criteria);
386
        if ( ! is_array($criteria_value)) {
387
            $criteria_value = array($criteria_value);
388
        }
389
390
        $multiple = ' ';
391
        $size     = ' ';
392
        $prefix_name = "criteria[$this->id][values]";
393
        $name        = $prefix_name . '[]';
394
395
        if ($criteria->is_advanced) {
396
            $multiple = ' multiple="multiple" ';
397
            $size     = ' size="'. min(7, count($this->getBind()->getAllValues()) + 2) .'" ';
398
        }
399
400
        $html .= '<input type="hidden" name="'. $prefix_name .'" />';
401
        $html .= '<select id="tracker_report_criteria_'. ($criteria->is_advanced ? 'adv_' : '') . $this->id .'"
402
                          name="'. $name .'" '.
403
                          $size .
404
                          $multiple .'>';
405
        //Any value
406
        $selected = count($criteria_value) && !in_array('', $criteria_value) ? '' : 'selected="selected"';
407
        $html .= '<option value="" '. $selected .' title="'. $GLOBALS['Language']->getText('global','any') .'">'. $GLOBALS['Language']->getText('global','any') .'</option>';
408
        //None value
409
        $selected = in_array(Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID, $criteria_value) ? 'selected="selected"' : '';
410
        $html .= '<option value="'.Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID.'" '. $selected .' title="'. $GLOBALS['Language']->getText('global','none') .'">'. $GLOBALS['Language']->getText('global','none') .'</option>';
411
        //Field values
412
        foreach($this->getBind()->getAllValues() as $id => $value) {
413
            $selected = in_array($id, $criteria_value) ? 'selected="selected"' : '';
414
            $style = $this->getBind()->getSelectOptionInlineStyle($id);
415
            $html .= '<option value="'. $id .'"  title="'. $this->getBind()->formatCriteriaValue($id) .'" '. $selected .' style="'. $style .'">';
416
            $html .= $this->getBind()->formatCriteriaValue($id);
417
            $html .= '</option>';
418
        }
419
        $html .= '</select>';
420
421
        return $html;
422
    }
423
424
    /**
425
     * Add some additionnal information beside the criteria.
426
     * This is up to the field. It can be html or inline javascript
427
     * to enhance the user experience
428
     * @return string
429
     */
430
    public function fetchCriteriaAdditionnalInfo() {
431
        return ''; //$this->getBind()->fetchDecoratorsAsJavascript();
432
    }
433
    /**
434
     * Add some additionnal information beside the field in the artifact form.
435
     * This is up to the field. It can be html or inline javascript
436
     * to enhance the user experience
437
     * @return string
438
     */
439
    public function fetchArtifactAdditionnalInfo($value, $submitted_values = null) {
440
        return ''; //$this->getBind()->fetchDecoratorsAsJavascript();
441
    }
442
443
     /**
444
     * Add some additionnal information beside the field in the submit new artifact form.
445
     * This is up to the field. It can be html or inline javascript
446
     * to enhance the user experience
447
     * @return string
448
     */
449
    public function fetchSubmitAdditionnalInfo($submitted_values) {
450
        return '';
451
    }
452
453
    /**
454
     * @return bool
455
     */
456
    protected function criteriaCanBeAdvanced() {
457
        return true;
458
    }
459
460
    /**
461
     * Fetch the value
462
     * @param mixed $value the value of the field
463
     * @return string
464
     */
465
    public function fetchRawValue($value) {
466
        return $this->getBind()->fetchRawValue($value);
467
    }
468
469
    /**
470
     * Fetch the value in a specific changeset
471
     * @param Tracker_Artifact_Changeset $changeset
472
     * @return string
473
     */
474
    public function fetchRawValueFromChangeset($changeset) {
475
        return $this->getBind()->fetchRawValueFromChangeset($changeset);
476
    }
477
478
    /**
479
     * @return Tracker_FormElement_Field_Value_ListDao
480
     */
481
    protected function getValueDao() {
482
        return new Tracker_FormElement_Field_Value_ListDao();
483
    }
484
485
    /**
486
     * Fetch the html code to display the field value in new artifact submission form
487
     *
488
     * @return string html
489
     */
490
    protected function fetchSubmitValue($submitted_values = array()) {
491
        $selected_values = isset($submitted_values[$this->id]) ? $submitted_values[$this->id] : array();
492
        $default_values  = $this->getSubmitDefaultValues();
493
494
        return $this->_fetchField(
495
            'tracker_field_'. $this->id,
496
            'artifact['. $this->id .']',
497
            $default_values,
498
            $selected_values
499
        );
500
    }
501
502
    private function getSubmitDefaultValues() {
503
        if ($this->fieldHasEnableWorkflow()) {
504
            return array();
505
        }
506
507
        return $this->getBind()->getDefaultValues();
508
    }
509
510
     /**
511
     * Fetch the html code to display the field value in masschange submission form
512
     *
513
     * @return string html
514
     */
515
    protected function fetchSubmitValueMasschange() {
516
        return $this->_fetchFieldMasschange('tracker_field_'. $this->id, 'artifact['. $this->id .']', $this->getBind()->getDefaultValues());
517
    }
518
    /**
519
     * Fetch the html code to display the field value in artifact
520
     *
521
     * @param Tracker_Artifact                $artifact         The artifact
522
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
523
     * @param array                           $submitted_values The value already submitted by the user
524
     *
525
     * @return string
526
     */
527
    protected function fetchArtifactValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
528
        $values = array();
529
        if (! empty($submitted_values) && isset($submitted_values[0][$this->id])) {
530
            $values = $submitted_values[0][$this->id];
531
        }
532
        $selected_values  = $value ? $value->getListValues() : array();
533
        return $this->_fetchField('tracker_field_'. $this->id,
534
                'artifact['. $this->id .']',
535
                $selected_values, $values);
536
    }
537
538
     /**
539
     * Fetch the field value in artifact to be displayed in mail
540
     *
541
     * @param Tracker_Artifact                $artifact         The artifact
542
     * @param PFUser                          $user             The user who will receive the email
543
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
544
     * @param string                          $format           mail format
545
     *
546
     * @return string
547
     */
548
    public function fetchMailArtifactValue(Tracker_Artifact $artifact, PFUser $user, Tracker_Artifact_ChangesetValue $value = null, $format='text') {
549
        $output = '';
550
        switch($format) {
551
            case 'html':
552
                if ( empty($value) ||  !$value->getListValues()) {
553
                    return '-';
554
                }
555
                $output = $this->fetchArtifactValueReadOnly($artifact, $value);
556
                break;
557
            default:
558
                $tablo = array();
559
                $selected_values = !empty($value) ? $value->getListValues() : array();
560
                foreach ($selected_values as $value) {
561
                    $tablo[] = $this->getBind()->formatMailArtifactValue($value->getId());
562
                }
563
                $output = implode(', ', $tablo);
564
                break;
565
        }
566
        return $output;
567
    }
568
569
    /**
570
     * Fetch the html code to display the field value in artifact in read only mode
571
     *
572
     * @param Tracker_Artifact                $artifact The artifact
573
     * @param Tracker_Artifact_ChangesetValue $value    The actual value of the field
574
     *
575
     * @return string
576
     */
577
    public function fetchArtifactValueReadOnly(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
578
        $html = '';
579
        $selected_values = $value ? $value->getListValues() : array();
580
        $tablo = array();
581
582
        if (empty($selected_values)) {
583
            return $this->getNoValueLabel();
584
        }
585
586
        foreach ($selected_values as $id => $selected) {
587
            $tablo[] = $this->getBind()->formatArtifactValue($id);
588
        }
589
        $html .= implode(', ', $tablo);
590
        return $html;
591
    }
592
593
    public function fetchArtifactValueWithEditionFormIfEditable(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
594
        return $this->fetchArtifactValueReadOnly($artifact, $value) . $this->getHiddenArtifactValueForEdition($artifact, $value, $submitted_values);
595
    }
596
597
    /**
598
     * Indicate if a workflow is defined and enabled on a field_id.
599
     * @param $id the field_id
600
     * @return boolean, true if a workflow is defined and enabled on the field_id
0 ignored issues
show
Documentation introduced by
The doc-type boolean, could not be parsed: Expected "|" or "end of type", but got "," at position 7. (view supported doc-types)

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

Loading history...
601
     */
602
    public function fieldHasEnableWorkflow(){
603
        $workflow = $this->getWorkflow();
604
        if(!empty($workflow) && $workflow->is_used){
605
            return $workflow->field_id===$this->id;
606
        }
607
        return false;
608
    }
609
610
     /**
611
     * Indicate if a workflow is defined on a field_id.
612
     * @param $id the field_id
613
     * @return boolean, true if a workflow is defined on the field_id
0 ignored issues
show
Documentation introduced by
The doc-type boolean, could not be parsed: Expected "|" or "end of type", but got "," at position 7. (view supported doc-types)

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

Loading history...
614
     */
615
    public function fieldHasDefineWorkflow(){
616
        $workflow = $this->getWorkflow();
617
        if(!empty($workflow)){
618
            return $workflow->field_id===$this->id;
619
        }
620
        return false;
621
    }
622
623
    /**
624
     * Get the workflow of the tracker.
625
     * @return Workflow Object
626
     */
627
    public function getWorkflow(){
628
        return $this->getTracker()->getWorkflow();
629
    }
630
631
    /**
632
     * Validate a value
633
     * @param Tracker_Artifact $artifact
634
     * @param mixed $value data coming from the request. May be string or array.
635
     *
636
     * @return bool true if the value is considered ok
637
     */
638
    protected function validate(Tracker_Artifact $artifact, $value) {
639
        $valid          = true;
640
        $field_value_to = null;
641
642
        if ($this->fieldHasEnableWorkflow()) {
643
644
            $last_changeset = $artifact->getLastChangeset();
645
646
            try {
647
                $field_value_to = $this->getBind()->getValue($value);
648
                if (!$last_changeset) {
649
                    if (!$this->isTransitionValid(null, $field_value_to)) {
650
                           $this->has_errors = true;
651
                           $valid = false;
652
                    }
653
                } else {
654
                if ($last_changeset->getValue($this)!=null) {
655
                    foreach ($last_changeset->getValue($this)->getListValues() as $id => $value) {
656
                        if ($value != $field_value_to) {
657
                            if (!$this->isTransitionValid($value, $field_value_to)) {
658
                                $this->has_errors = true;
659
                                $valid = false;
660
                            }
661
                        }
662
                    }
663
                } else {
664
                    if (!$this->isTransitionValid(null, $field_value_to)) {
665
                        $this->has_errors = true;
666
                        $valid = false;
667
                    }
668
                }
669
            }
670
            } catch (Tracker_FormElement_InvalidFieldValueException $exexption) {
671
                $valid = false;
672
            }
673
674
            if ($valid) {
675
                //Check permissions on transition
676
                if (!$last_changeset || $last_changeset->getValue($this) == null) {
677
                    $from = null;
678
                    $to = $value;
679
                } else {
680
                    list(, $from) = each ($last_changeset->getValue($this)->getListValues());
0 ignored issues
show
Bug introduced by
$last_changeset->getValue($this)->getListValues() cannot be passed to each() as the parameter $array expects a reference.
Loading history...
681
                    if (!is_string($value)) {
682
                        $to = $value->getId();
683
                    }else {
684
                        $to = $value;
685
                    }
686
                }
687
                $transition_id = $this->getTransitionId($from, $to);
688
                if (!$this->userCanMakeTransition($transition_id)) {
689
                        $valid = false;
690
                 }
691
            }
692
        }
693
694
        if ($valid) {
695
            return true;
696
        } else {
697
            if ($field_value_to !== null) {
698
                $GLOBALS['Response']->addFeedback('error', $GLOBALS['Language']->getText('plugin_tracker_common_artifact', 'transition_not_valid', array($field_value_to->getLabel())));
699
            } else {
700
                $GLOBALS['Response']->addFeedback('error', $GLOBALS['Language']->getText('plugin_tracker_common_artifact', 'transition_to_none'));
701
            }
702
            return false;
703
        }
704
    }
705
706
707
    protected function isTransitionValid($field_value_from, $field_value_to){
708
        if (!$this->fieldHasEnableWorkflow()) {
709
            return true;
710
        }else {
711
            $workflow = $this->getWorkflow();
712
            if ($workflow->isTransitionExist($field_value_from, $field_value_to)) {
713
                return true;
714
            }else return false;
715
        }
716
    }
717
718
    protected function getSelectedValue($selected_values) {
719
        if ($this->getBind()) {
720
            foreach($this->getBind()->getAllValues() as $id => $value) {
721
                    if(isset($selected_values[$id])) {
722
                        $from = $value;
723
                        return $from;
724
                    }
725
            }
726
            return null;
727
        }
728
    }
729
730
    /**
731
     * @return array of BindValues
732
     */
733
    public function getAllValues() {
734
        return $this->getBind()->getAllValues();
735
    }
736
737
    /**
738
     * @return array of BindValues that are not hidden + none value if any
739
     */
740
    public function getVisibleValuesPlusNoneIfAny() {
741
        $values = $this->getAllValues();
742
        foreach ($values as $key => $value) {
743
            if ($value->isHidden()) {
744
                unset($values[$key]);
745
            }
746
        }
747
        if ($values) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type Tracker_FormElement_Field_List_BindValue[] 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...
748
            if (! $this->isRequired()) {
749
                $none = new Tracker_FormElement_Field_List_Bind_StaticValue_None();
750
                $values = array($none->getId() => $none) + $values;
751
            }
752
        }
753
        return $values;
754
    }
755
756
    /**
757
     * @return Tracker_FormElement_Field_List_Value or null if not found
758
     */
759
    public function getListValueById($value_id) {
760
        foreach ($this->getVisibleValuesPlusNoneIfAny() as $value) {
761
            if ($value->getId() == $value_id) {
762
                return $value;
763
            }
764
        }
765
    }
766
767
    /**
768
     *
769
     * @param Tracker_Artifact_Changeset $changeset
770
     * @return string
771
     */
772
    public function getFirstValueFor(Tracker_Artifact_Changeset $changeset) {
773
        if ($this->userCanRead()) {
774
            $value = $changeset->getValue($this);
775
            if ($value && ($last_values = $value->getListValues())) {
776
                // let's assume there is no more that one status
777
                if ($label = array_shift($last_values)->getLabel()) {
778
                    return $label;
779
                }
780
            }
781
        }
782
    }
783
784
    protected function _fetchField($id, $name, $selected_values, $submitted_values = array()) {
785
        $html     = '';
786
        $purifier = Codendi_HTMLPurifier::instance();
787
788
        if ($name) {
789
            if ($this->isMultiple()) {
790
                $name .= '[]';
791
            }
792
            $name = 'name="'. $purifier->purify($name) .'"';
793
        }
794
795
        if ($id) {
796
            $id = 'id="'. $id .'"';
797
        }
798
799
        $html .= $this->fetchFieldContainerStart($id, $name);
800
801
        $from = $this->getSelectedValue($selected_values);
802
        if ($from == null && !isset($submitted_values)) {
803
            $none_is_selected = isset($selected_values[Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID]);
804
        } else {
805
            $none_is_selected = ($submitted_values==Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID);
806
        }
807
808
        if (!$this->fieldHasEnableWorkflow()) {
809
            $none_value = new Tracker_FormElement_Field_List_Bind_StaticValue_None();
810
            $html .= $this->fetchFieldValue($none_value, $name, $none_is_selected);
811
        }
812
813
        if (($submitted_values) && !is_array($submitted_values)) {
814
            $submitted_values_array[] = $submitted_values;
815
            $submitted_values = $submitted_values_array;
816
        }
817
818
        foreach($this->getBind()->getAllValues() as $id => $value) {
819
            $transition_id = null;
820
            if ($this->isTransitionValid($from, $value)) {
821
                $transition_id = $this->getTransitionId($from, $value->getId());
822
                if (!empty($submitted_values)) {
823
                    $is_selected = in_array($id, array_values($submitted_values));
824
                } else {
825
                    $is_selected = isset($selected_values[$id]);
826
                }
827
                if ($this->userCanMakeTransition($transition_id)) {
828
                    if (!$value->isHidden()) {
829
                        $html .= $this->fetchFieldValue($value, $name, $is_selected);
830
                    }
831
                }
832
            }
833
        }
834
835
        $html .= $this->fetchFieldContainerEnd();
836
        return $html;
837
    }
838
839
    protected function fetchFieldContainerStart($id, $name) {
840
        $html     = '';
841
        $multiple = '';
842
        $size     = '';
843
        if ($this->isMultiple()) {
844
            $multiple = 'multiple="multiple"';
845
            $size     = 'size="'. min($this->getMaxSize(), count($this->getBind()->getAllValues()) + 2) .'"';
846
        }
847
        $html .= "<select $id $name $multiple $size>";
848
        return $html;
849
    }
850
851
    protected function fetchFieldValue(Tracker_FormElement_Field_List_Value $value, $name, $is_selected) {
852
        $id = $value->getId();
853
        if ($id == Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID) {
854
            $label = $value->getLabel();
855
        } else {
856
            $label = $this->getBind()->formatArtifactValue($id);
857
        }
858
        $style    = $this->getBind()->getSelectOptionInlineStyle($id);
859
        $selected = $is_selected ? 'selected="selected"' : '';
860
        return '<option value="'. $id .'" '. $selected .' title="'. $label .'" style="'. $style .'">'. $label .'</option>';
861
    }
862
863
    protected function fetchFieldContainerEnd() {
864
        return '</select>';
865
    }
866
867
868
    protected function _fetchFieldMasschange($id, $name, $selected_values) {
869
        $html = '';
870
        $multiple = ' ';
871
        $size     = ' ';
872
        if ($this->isMultiple()) {
873
            $multiple = ' multiple="multiple" ';
874
            $size     = ' size="'. min($this->getMaxSize(), count($this->getBind()->getAllValues()) + 2) .'" ';
875
            if ($name) {
876
                $name .= '[]';
877
            }
878
        }
879
        $html .= '<select ';
880
        if ($id) {
881
            $html .= 'id="'. $id .'" ';
882
        }
883
        if ($name) {
884
            $html .= 'name="'. $name .'" ';
885
        }
886
        $html .= $size . $multiple .'>';
887
888
        //if ( $this->fieldHasEnableWorkflow() ) {
889
        $html .= '<option value="'.$GLOBALS['Language']->getText('global','unchanged').'" selected="selected">'. $GLOBALS['Language']->getText('global','unchanged') .'</option>';
890
        $html .= '<option value="'.Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID.'">'. $GLOBALS['Language']->getText('global','none') .'</option>';
891
        //}
892
893
        foreach($this->getBind()->getAllValues() as $id => $value) {
894
                    if (!$value->isHidden()) {
895
                        $style = $this->getBind()->getSelectOptionInlineStyle($id);
896
                        $html .= '<option value="'. $id .'" title="'. $this->getBind()->formatArtifactValue($id) .'" style="'. $style .'">';
897
                        $html .= $this->getBind()->formatArtifactValue($id);
898
                        $html .= '</option>';
899
                    }
900
        }
901
902
        $html .= '</select>';
903
        return $html;
904
    }
905
906
907
    protected function getMaxSize() {
908
        return 7;
909
    }
910
911
    /**
912
     * Fetch the changes that has been made to this field in a followup
913
     * @param Tracker_ $artifact
914
     * @param array $from the value(s) *before*
915
     * @param array $to   the value(s) *after*
916
     */
917
    public function fetchFollowUp($artifact, $from, $to) {
918
        $html = '';
919
        $values = array();
920
        if ($from && isset($from['changeset_id'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $from 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...
921
            foreach($this->getBind()->getChangesetValues($from['changeset_id']) as $v) {
922
                if ($v['id'] != Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID) {
923
                    $values[] = $this->getBind()->formatChangesetValue($v);
924
                }
925
            }
926
            $from_value = implode(', ', $values);
927
        }
928
929
        if (!$from_value) {
930
            $html .= $GLOBALS['Language']->getText('plugin_tracker_artifact','set_to').' ';
931
        } else {
932
            $html .= ' '.$GLOBALS['Language']->getText('plugin_tracker_artifact','changed_from').' '. $from_value .'  '.$GLOBALS['Language']->getText('plugin_tracker_artifact','to').' ';
933
        }
934
935
        $values = array();
936
        foreach($this->getBind()->getfChangesetValues($to['changeset_id']) as $v) {
0 ignored issues
show
Bug introduced by
The method getfChangesetValues() does not exist on Tracker_FormElement_Field_List_Bind. Did you maybe mean getChangesetValues()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
937
            $values[] = $this->getBind()->formatChangesetValue($v);
938
        }
939
        $html .= implode(', ', $values);
940
        return $html;
941
    }
942
943
    /**
944
     * Display the html field in the admin ui
945
     * @return string html
946
     */
947
    protected function fetchAdminFormElement() {
948
        $html = '';
949
        $html .= $this->_fetchField('', '', $this->getBind()->getDefaultValues());
950
        return $html;
951
    }
952
953
    /**
954
     * Fetch the html code to display the field value in tooltip
955
     * @param Tracker_Artifact $artifact
956
     * @param Tracker_Artifact_ChangesetValue_List $value The changeset value of this field
957
     * @return string The html code to display the field value in tooltip
958
     */
959
    protected function fetchTooltipValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
960
        $html = '';
961
        if ($value) {
962
            $html .= $this->fetchChangesetValue($artifact->id, $artifact->getLastChangeset()->id, $value);
963
        }
964
        return $html;
965
    }
966
967
    /**
968
     * @see Tracker_FormElement_Field::fetchCardValue()
969
     */
970
    public function fetchCardValue(Tracker_Artifact $artifact, Tracker_CardDisplayPreferences $display_preferences) {
971
        $html = '';
972
        //We have to fetch all values of the changeset as we are a list of value
973
        //This is the case only if we are multiple but an old changeset may
974
        //contain multiple values
975
        $values = array();
976
        foreach($this->getBind()->getChangesetValues($artifact->getLastChangeset()->id) as $v) {
977
            $val = $this->getBind()->formatCardValue($v, $display_preferences);
978
            if ($val != '') {
979
                $values[] = $val;
980
            }
981
        }
982
        $html .= implode(' ', $values);
983
        return $html;
984
    }
985
986
    /**
987
     * Update the form element.
988
     * Override the parent function to handle binds
989
     *
990
     * @return void
991
     */
992
    protected function processUpdate(TrackerManager $tracker_manager, $request, $current_user) {
993
        $redirect = false;
994
        if ($request->exist('bind')) {
995
            $redirect = $this->getBind()->process($request->get('bind'), $no_redirect = true);
996
        }
997
        parent::processUpdate($tracker_manager, $request, $current_user, $redirect);
998
    }
999
1000
    /**
1001
     * Hook called after a creation of a field
1002
     *
1003
     * @param array $data The data used to create the field
0 ignored issues
show
Bug introduced by
There is no parameter named $data. 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...
1004
     *
1005
     * @return void
1006
     */
1007
    public function afterCreate($formElement_data) {
1008
        parent::afterCreate();
1009
        $type      = isset($formElement_data['bind-type']) ? $formElement_data['bind-type'] : '';
1010
        $bind_data = isset($formElement_data['bind'])      ? $formElement_data['bind']      : array();
1011
1012
        $bf = new Tracker_FormElement_Field_List_BindFactory();
1013
        if ($this->bind = $bf->createBind($this, $type, $bind_data)) {
1014
            $dao = new Tracker_FormElement_Field_ListDao();
1015
            $dao->save($this->getId(), $bf->getType($this->bind));
1016
        }
1017
    }
1018
1019
    /**
1020
     * Transforms FormElement_List into a SimpleXMLElement
1021
     */
1022
    public function exportToXml(
1023
        SimpleXMLElement $root,
1024
        &$xmlMapping,
1025
        $project_export_context,
1026
        UserXMLExporter $user_xml_exporter
1027
    ) {
1028
        parent::exportToXML($root, $xmlMapping, $project_export_context, $user_xml_exporter);
1029
        if ($this->getBind() && $this->shouldBeBindXML()) {
1030
            $child = $root->addChild('bind');
1031
            $bf = new Tracker_FormElement_Field_List_BindFactory();
1032
            $child->addAttribute('type', $bf->getType($this->getBind()));
1033
            $this->getBind()->exportToXML($child, $xmlMapping, $project_export_context, $user_xml_exporter);
1034
        }
1035
    }
1036
1037
    /**
1038
     * Say if we export the bind in the XML
1039
     *
1040
     * @return bool
1041
     */
1042
    public function shouldBeBindXML() {
1043
        return true;
1044
    }
1045
1046
    /**
1047
     * Continue the initialisation from an xml (FormElementFactory is not smart enough to do all stuff.
1048
     * Polymorphism rulez!!!
1049
     *
1050
     * @param SimpleXMLElement $xml         containing the structure of the imported Tracker_FormElement
1051
     * @param array            &$xmlMapping where the newly created formElements indexed by their XML IDs are stored (and values)
1052
     *
1053
     * @return void
1054
     */
1055
     public function continueGetInstanceFromXML(
1056
         $xml,
1057
         &$xmlMapping,
1058
         User\XML\Import\IFindUserFromXMLReference $user_finder
1059
     ) {
1060
        parent::continueGetInstanceFromXML($xml, $xmlMapping, $user_finder);
1061
        // if field is a list add bind
1062
        if ($xml->bind) {
1063
            $bind = $this->getBindFactory()->getInstanceFromXML($xml->bind, $this, $xmlMapping, $user_finder);
1064
            $this->setBind($bind);
1065
        }
1066
    }
1067
1068
    /**
1069
     * Callback called after factory::saveObject. Use this to do post-save actions
1070
     *
1071
     * @param Tracker $tracker The tracker
1072
     *
1073
     * @return void
1074
     */
1075
    public function afterSaveObject(Tracker $tracker) {
1076
        $bind = $this->getBind();
1077
        $this->getListDao()->save($this->getId(), $this->getBindFactory()->getType($bind));
1078
        $bind->saveObject();
1079
    }
1080
1081
    /**
1082
     * Get an instance of Tracker_FormElement_Field_ListDao
1083
     *
1084
     * @return Tracker_FormElement_Field_ListDao
1085
     */
1086
    public function getListDao() {
1087
        return new Tracker_FormElement_Field_ListDao();
1088
    }
1089
1090
    /**
1091
     * Get an instance of Tracker_FormElement_Field_List_BindFactory
1092
     *
1093
     * @return Tracker_FormElement_Field_List_BindFactory
1094
     */
1095
    function getBindFactory() {
1096
        return new Tracker_FormElement_Field_List_BindFactory();
1097
    }
1098
1099
    /**
1100
     * Save the value and return the id
1101
     *
1102
     * @param Tracker_Artifact                $artifact                The artifact
1103
     * @param int                             $changeset_value_id      The id of the changeset_value
1104
     * @param mixed                           $value                   The value submitted by the user
1105
     * @param Tracker_Artifact_ChangesetValue $previous_changesetvalue The data previously stored in the db
1106
     *
1107
     * @return boolean
1108
     */
1109
    protected function saveValue($artifact, $changeset_value_id, $value, Tracker_Artifact_ChangesetValue $previous_changesetvalue = null) {
1110
        return $this->getValueDao()->create($changeset_value_id, $value);
1111
    }
1112
1113
    /**
1114
     * Get the value of this field
1115
     *
1116
     * @param Tracker_Artifact_Changeset $changeset   The changeset (needed in only few cases like 'lud' field)
1117
     * @param int                        $value_id    The id of the value
1118
     * @param boolean                    $has_changed If the changeset value has changed from the rpevious one
1119
     *
1120
     * @return Tracker_Artifact_ChangesetValue or null if not found
1121
     */
1122
    public function getChangesetValue($changeset, $value_id, $has_changed) {
1123
        $changeset_value = null;
1124
        $value_ids = $this->getValueDao()->searchById($value_id, $this->id);
1125
        $bindvalue_ids = array();
1126
        foreach($value_ids as $v) {
0 ignored issues
show
Bug introduced by
The expression $value_ids 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...
1127
            $bindvalue_ids[] = $v['bindvalue_id'];
1128
        }
1129
        $bind_values = array();
1130
        if (count($bindvalue_ids)) {
1131
            $bind_values = $this->getBind()->getBindValues($bindvalue_ids);
1132
        }
1133
        $changeset_value = new Tracker_Artifact_ChangesetValue_List($value_id, $this, $has_changed, $bind_values);
1134
        return $changeset_value;
1135
    }
1136
1137
    /**
1138
     * Get available values of this field for SOAP usage
1139
     * Fields like int, float, date, string don't have available values
1140
     *
1141
     * @return mixed The values or null if there are no specific available values
1142
     */
1143
     public function getSoapAvailableValues() {
1144
         $values = null;
1145
         $bind = $this->getBind();
1146
         if ($bind != null) {
1147
             $values = $bind->getSoapAvailableValues();
1148
         }
1149
         return $values;
1150
     }
1151
1152
     public function getSoapBindingProperties() {
1153
         $bind = $this->getBind();
1154
         return $bind->getSoapBindingProperties();
0 ignored issues
show
Best Practice introduced by
The expression return $bind->getSoapBindingProperties(); seems to be an array, but some of its elements' types (string) are incompatible with the return type of the parent method Tracker_FormElement_Fiel...etSoapBindingProperties of type array<string,null|array>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1155
     }
1156
1157
     public function getFieldDataFromRESTValue(array $value, Tracker_Artifact $artifact = null) {
1158
         if (array_key_exists('bind_value_ids', $value) && is_array($value['bind_value_ids'])) {
1159
             return array_map('intval', $value['bind_value_ids']);
1160
         }
1161
         throw new Tracker_FormElement_InvalidFieldValueException('List fields values must be passed as an array of ids (integer) in \'bind_value_ids\''
1162
            . ' Example: {"field_id": 1548, "bind_value_ids": [457]}');
1163
     }
1164
1165
    public function getFieldDataFromRESTValueByField($value, Tracker_Artifact $artifact = null) {
1166
        throw new Tracker_FormElement_RESTValueByField_NotImplementedException();
1167
    }
1168
1169
     public function getFieldDataFromSoapValue(stdClass $soap_value, Tracker_Artifact $artifact = null) {
1170
         if (isset($soap_value->field_value->bind_value)) {
1171
             if ($this->isMultiple()) {
1172
                 $values = array();
1173
                 foreach ($soap_value->field_value->bind_value as $bind_value) {
1174
                    $values[] = $bind_value->bind_value_id;
1175
                 }
1176
                 return $values;
1177
             } else {
1178
                 return $soap_value->field_value->bind_value[0]->bind_value_id;
1179
             }
1180
         } else {
1181
             return $this->getFieldData($soap_value->field_value->value);
1182
         }
1183
     }
1184
1185
    /**
1186
     * Get the field data for artifact submission
1187
     *
1188
     * @param string the soap field value
1189
     *
1190
     * @return mixed the field data corresponding to the soap_value for artifact submision
1191
     */
1192
    public function getFieldData($soap_value) {
1193
        if ($soap_value === $GLOBALS['Language']->getText('global','none')) {
1194
            return Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID;
1195
        }
1196
1197
        $bind = $this->getBind();
1198
        if ($bind != null) {
1199
            $soap_value = $bind->getFieldData($soap_value, $this->isMultiple());
1200
            if ($soap_value != null) {
1201
                return $soap_value;
1202
            } else {
1203
                return null;
1204
            }
1205
        } else {
1206
            return null;
1207
        }
1208
    }
1209
1210
     /**
1211
     * Check if there are changes between old and new value for this field
1212
     *
1213
     * @param Tracker_Artifact_ChangesetValue $previous_changesetvalue The data stored in the db
1214
     * @param mixed                           $new_value               May be string or array
1215
     *
1216
     * @return bool true if there are differences
1217
     */
1218
    public function hasChanges($previous_changesetvalue, $new_value) {
1219
        if (!is_array($new_value)) {
1220
            $new_value = array($new_value);
1221
        }
1222
        if (empty($new_value)) {
1223
            $new_value = array(Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID);
1224
        }
1225
        if ($previous_changesetvalue) {
1226
            $old_value = $previous_changesetvalue->getValue();
1227
        }
1228
        if (empty($old_value)) {
1229
            $old_value = array(Tracker_FormElement_Field_List_Bind_StaticValue_None::VALUE_ID);
1230
        }
1231
        sort($old_value);
1232
        sort($new_value);
1233
        return $old_value != $new_value;
1234
    }
1235
1236
    /**
1237
     * Say if this fields suport notifications
1238
     *
1239
     * @return bool
1240
     */
1241
    public function isNotificationsSupported() {
1242
        if ($b = $this->getBind()) {
1243
            return $b->isNotificationsSupported();
1244
        }
1245
        return false;
1246
    }
1247
1248
    protected function permission_is_authorized($type, $transition_id, $user_id, $group_id) {
1249
        include_once 'www/project/admin/permissions.php';
1250
1251
        return permission_is_authorized($type, $transition_id, $user_id, $group_id);
1252
    }
1253
1254
    /**
1255
     * Check if the user can make the transition
1256
     *
1257
     * @param int  $transition_id The id of the transition
1258
     * @param PFUser $user          The user. If null, take the current user
1259
     *
1260
     *@return boolean true if user has permission on this field
1261
     */
1262
    public function userCanMakeTransition($transition_id, PFUser $user = null) {
1263
        if ($transition_id) {
1264
            $group_id = $this->getTracker()->getGroupId();
1265
1266
            if (!$user) {
1267
                $user = $this->getCurrentUser();
1268
            }
1269
            return $this->permission_is_authorized('PLUGIN_TRACKER_WORKFLOW_TRANSITION', $transition_id, $user->getId(), $group_id);
1270
        }
1271
        return true;
1272
    }
1273
1274
    /**
1275
     * Get a recipients list for notifications. This is filled by users fields for example.
1276
     *
1277
     * @param Tracker_Artifact_ChangesetValue $changeset_value The changeset
1278
     *
1279
     * @return array
1280
     */
1281
    public function getRecipients(Tracker_Artifact_ChangesetValue $changeset_value) {
1282
        return $this->getBind()->getRecipients($changeset_value);
0 ignored issues
show
Compatibility introduced by
$changeset_value of type object<Tracker_Artifact_ChangesetValue> is not a sub-type of object<Tracker_Artifact_ChangesetValue_List>. It seems like you assume a child class of the class Tracker_Artifact_ChangesetValue to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1283
    }
1284
1285
    protected function getTransitionId($from, $to) {
1286
        return TransitionFactory::instance()->getTransitionId($from, $to);
1287
    }
1288
1289
    public function getDefaultValue() {
1290
        $default_array = $this->getBind()->getDefaultValues();
1291
        if (! $default_array) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $default_array 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...
1292
            return array(Tracker_FormElement_Field_List_Bind::NONE_VALUE);
1293
        }
1294
        return array_keys($default_array);
1295
    }
1296
1297
1298
    public function getDefaultRESTValue() {
1299
        return $this->getBind()->getDefaultRESTValues();
1300
    }
1301
1302
    /**
1303
     * Say if the value is valid. If not valid set the internal has_error to true.
1304
     *
1305
     * @param Tracker_Artifact $artifact The artifact
1306
     * @param mixed            $value    data coming from the request. May be string or array.
1307
     *
1308
     * @return bool true if the value is considered ok
1309
     */
1310
    public function isValid(Tracker_Artifact $artifact, $value) {
1311
        $this->has_errors = ! ($this->isPossibleValue($value) && $this->validate($artifact, $value));
1312
1313
        return ! $this->has_errors;
1314
    }
1315
1316
    /**
1317
     * @return bool
1318
     */
1319
    protected function isPossibleValue($value) {
1320
        $is_possible_value   = true;
1321
        $all_possible_values = $this->getBind()->getAllValues();
1322
1323
        if (is_array($value)) {
1324
            foreach ($value as $id) {
1325
                $is_possible_value = $is_possible_value && $this->checkValueExists($id, $all_possible_values);
1326
            }
1327
        } else {
1328
            $is_possible_value = $this->checkValueExists($value, $all_possible_values);
1329
        }
1330
1331
        return $is_possible_value;
1332
    }
1333
1334
    /**
1335
     * @return bool
1336
     */
1337
    protected function checkValueExists($value_id, array $all_possible_values) {
1338
        return array_key_exists($value_id, $all_possible_values) ||
1339
               $value_id == Tracker_FormElement_Field_List::NONE_VALUE ||
1340
               $value_id == Tracker_FormElement_Field_List::NOT_INDICATED_VALUE;
1341
    }
1342
1343
    /**
1344
     * Validate a required field
1345
     *
1346
     * @param Tracker_Artifact                $artifact             The artifact to check
1347
     * @param mixed                           $value      The submitted value
1348
     *
1349
     * @return boolean true on success or false on failure
1350
     */
1351
    public function isValidRegardingRequiredProperty(Tracker_Artifact $artifact, $value) {
1352
        $this->has_errors = false;
1353
1354
        if ($this->isEmpty($value, $artifact) && $this->isRequired()) {
1355
            $this->addRequiredError();
1356
        }
1357
1358
        return ! $this->has_errors;
1359
    }
1360
1361
    public function isEmpty($value, Tracker_Artifact $artifact) {
1362
        return $this->isNone($value);
1363
    }
1364
1365
    /**
1366
     * @see Tracker_FormElement_Field_Shareable
1367
     */
1368
    public function fixOriginalValueIds(array $value_mapping) {
1369
        $this->getBind()->fixOriginalValueIds($value_mapping);
1370
    }
1371
1372
    /**
1373
     * @see Tracker_FormElement::process()
1374
     */
1375
    public function process(Tracker_IDisplayTrackerLayout $layout, $request, $current_user) {
1376
        parent::process($layout, $request, $current_user);
1377
        if ($request->get('func') == 'get-values') {
1378
            $GLOBALS['Response']->sendJSON($this->getBind()->fetchFormattedForJson());
1379
        }
1380
    }
1381
1382
    public function fetchFormattedForJson() {
1383
        $json = parent::fetchFormattedForJson();
1384
        $json['values'] = $this->getBind()->fetchFormattedForJson();
1385
        return $json;
0 ignored issues
show
Best Practice introduced by
The expression return $json; seems to be an array, but some of its elements' types (array) are incompatible with the return type of the parent method Tracker_FormElement::fetchFormattedForJson of type array<string,integer|string>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1386
    }
1387
1388
    public function getRESTAvailableValues() {
1389
        $values = null;
1390
        $bind = $this->getBind();
1391
        if ($bind != null) {
1392
            $values = $bind->getRESTAvailableValues();
1393
        }
1394
        return $values;
1395
    }
1396
1397
    /**
1398
     * @param string $new_value
1399
     *
1400
     * @return int | null
1401
     */
1402
    public function addBindValue($new_value) {
1403
        return $this->getBind()->addValue($new_value);
1404
    }
1405
}
1406