Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.41% covered (warning)
83.41%
181 / 217
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoFieldDescription
83.41% covered (warning)
83.41%
181 / 217
63.64% covered (warning)
63.64%
7 / 11
125.17
0.00% covered (danger)
0.00%
0 / 1
 newFromString
88.33% covered (warning)
88.33%
53 / 60
0.00% covered (danger)
0.00%
0 / 1
20.64
 newFromDBArray
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
14
 getDelimiter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDelimiter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDateOrDatetime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFieldSize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 toDBArray
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
12
 prepareAndValidateValue
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
26
 prettyPrintAllowedValues
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 prettyPrintType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 prettyPrintTypeAndAttributes
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3/**
4 * CargoFieldDescription - holds the attributes of a single field as defined
5 * in the #cargo_declare parser function.
6 *
7 * @author Yaron Koren
8 * @ingroup Cargo
9 */
10class CargoFieldDescription {
11    public $mType;
12    public $mSize;
13    public $mDependentOn = [];
14    public $mIsList = false;
15    private $mDelimiter;
16    public $mAllowedValues = null;
17    public $mIsMandatory = false;
18    public $mIsUnique = false;
19    public $mRegex = null;
20    public $mIsHidden = false;
21    public $mIsHierarchy = false;
22    public $mHierarchyStructure = null;
23    public $mOtherParams = [];
24
25    /**
26     * Initializes from a string within the #cargo_declare function.
27     *
28     * @param string $fieldDescriptionStr
29     * @return \CargoFieldDescription|null
30     */
31    public static function newFromString( $fieldDescriptionStr ) {
32        $fieldDescription = new CargoFieldDescription();
33
34        if ( strpos( strtolower( $fieldDescriptionStr ), 'list' ) === 0 ) {
35            $matches = [];
36            $foundMatch = preg_match( '/[Ll][Ii][Ss][Tt] \((.*)\) [Oo][Ff] (.*)/is', $fieldDescriptionStr, $matches );
37            if ( !$foundMatch ) {
38                // Return a true error message here?
39                return null;
40            }
41            $fieldDescription->mIsList = true;
42            $fieldDescription->mDelimiter = $matches[1];
43            $fieldDescriptionStr = $matches[2];
44        }
45
46        CargoUtils::validateFieldDescriptionString( $fieldDescriptionStr );
47
48        // There may be additional parameters, in/ parentheses.
49        $matches = [];
50        $foundMatch2 = preg_match( '/([^(]*)\s*\((.*)\)/s', $fieldDescriptionStr, $matches );
51        $allowedValuesParam = "";
52        if ( $foundMatch2 ) {
53            $fieldDescriptionStr = trim( $matches[1] );
54            $extraParamsString = $matches[2];
55            $extraParams = explode( ';', $extraParamsString );
56            foreach ( $extraParams as $extraParam ) {
57                $extraParamParts = explode( '=', $extraParam, 2 );
58                if ( count( $extraParamParts ) == 1 ) {
59                    $paramKey = strtolower( trim( $extraParamParts[0] ) );
60                    if ( $paramKey == 'hierarchy' ) {
61                        $fieldDescription->mIsHierarchy = true;
62                    }
63                    $fieldDescription->mOtherParams[$paramKey] = true;
64                } else {
65                    $paramKey = strtolower( trim( $extraParamParts[0] ) );
66                    $paramValue = trim( $extraParamParts[1] );
67                    if ( $paramKey == 'allowed values' ) {
68                        // we do not assign allowed values to fieldDescription here,
69                        // because we don't know yet if it's a hierarchy or an enumeration
70                        $allowedValuesParam = $paramValue;
71                    } elseif ( $paramKey == 'size' ) {
72                        $fieldDescription->mSize = $paramValue;
73                    } elseif ( $paramKey == 'dependent on' ) {
74                        $fieldDescription->mDependentOn = array_map( 'trim', explode( ',', $paramValue ) );
75                    } else {
76                        $fieldDescription->mOtherParams[$paramKey] = $paramValue;
77                    }
78                }
79            }
80            if ( $allowedValuesParam !== "" ) {
81                $allowedValuesArray = [];
82                if ( $fieldDescription->mIsHierarchy == true ) {
83                    // $paramValue contains "*" hierarchy structure
84                    CargoUtils::validateHierarchyStructure( trim( $allowedValuesParam ) );
85                    $fieldDescription->mHierarchyStructure = trim( $allowedValuesParam );
86                    // now make the allowed values param similar to the syntax
87                    // used by other fields
88                    $hierarchyNodesArray = explode( "\n", $allowedValuesParam );
89                    foreach ( $hierarchyNodesArray as $node ) {
90                        // Remove prefix of multiple "*"
91                        $allowedValuesArray[] = trim( preg_replace( '/^[*]*/', '', $node ) );
92                    }
93                } else {
94                    // Replace the comma/delimiter
95                    // substitution with a character
96                    // that has no chance of being
97                    // included in the values list -
98                    // namely, the ASCII beep.
99
100                    // The delimiter can't be a
101                    // semicolon, because that's
102                    // already used to separate
103                    // "extra parameters", so just
104                    // hardcode it to a semicolon.
105                    $delimiter = ',';
106                    $allowedValuesStr = str_replace( "\\$delimiter", "\a", $allowedValuesParam );
107                    $allowedValuesTempArray = explode( $delimiter, $allowedValuesStr );
108                    foreach ( $allowedValuesTempArray as $value ) {
109                        if ( $value == '' ) {
110                            continue;
111                        }
112                        // Replace beep back with delimiter, trim.
113                        $value = str_replace( "\a", $delimiter, trim( $value ) );
114                        $allowedValuesArray[] = $value;
115                    }
116                }
117                $fieldDescription->mAllowedValues = $allowedValuesArray;
118            }
119        }
120
121        // What's left will be the type, hopefully.
122        // Allow any capitalization of the type.
123        $type = ucfirst( strtolower( $fieldDescriptionStr ) );
124        // The 'URL' type has special capitalization.
125        if ( $type == 'Url' ) {
126            $type = 'URL';
127        }
128        $fieldDescription->mType = $type;
129
130        // Validation.
131        if ( in_array( $type, [ 'Text', 'Wikitext', 'Searchtext' ] ) &&
132            array_key_exists( 'unique', $fieldDescription->mOtherParams ) ) {
133            throw new MWException( "'unique' is not allowed for fields of type '$type'." );
134        }
135        if ( $fieldDescription->mType == 'Boolean' && $fieldDescription->mIsList == true ) {
136            throw new MWException( "Error: 'list' is not allowed for fields of type 'Boolean'." );
137        }
138
139        return $fieldDescription;
140    }
141
142    /**
143     * @param array $descriptionData
144     * @return \CargoFieldDescription
145     */
146    public static function newFromDBArray( $descriptionData ) {
147        $fieldDescription = new CargoFieldDescription();
148        foreach ( $descriptionData as $param => $value ) {
149            if ( $param == 'type' ) {
150                $fieldDescription->mType = $value;
151            } elseif ( $param == 'size' ) {
152                $fieldDescription->mSize = $value;
153            } elseif ( $param == 'dependent on' ) {
154                $fieldDescription->mDependentOn = $value;
155            } elseif ( $param == 'isList' ) {
156                $fieldDescription->mIsList = true;
157            } elseif ( $param == 'delimiter' ) {
158                $fieldDescription->mDelimiter = $value;
159            } elseif ( $param == 'allowedValues' ) {
160                $fieldDescription->mAllowedValues = $value;
161            } elseif ( $param == 'mandatory' ) {
162                $fieldDescription->mIsMandatory = true;
163            } elseif ( $param == 'unique' ) {
164                $fieldDescription->mIsUnique = true;
165            } elseif ( $param == 'regex' ) {
166                $fieldDescription->mRegex = $value;
167            } elseif ( $param == 'hidden' ) {
168                $fieldDescription->mIsHidden = true;
169            } elseif ( $param == 'hierarchy' ) {
170                $fieldDescription->mIsHierarchy = true;
171            } elseif ( $param == 'hierarchyStructure' ) {
172                $fieldDescription->mHierarchyStructure = $value;
173            } else {
174                $fieldDescription->mOtherParams[$param] = $value;
175            }
176        }
177        return $fieldDescription;
178    }
179
180    public function getDelimiter() {
181        // Make "\n" represent a newline.
182        return str_replace( '\n', "\n", $this->mDelimiter ?? '' );
183    }
184
185    public function setDelimiter( $delimiter ) {
186        $this->mDelimiter = $delimiter;
187    }
188
189    public function isDateOrDatetime() {
190        return in_array( $this->mType, [ 'Date', 'Start date', 'End date', 'Datetime', 'Start datetime', 'End datetime' ] );
191    }
192
193    public function getFieldSize() {
194        if ( $this->isDateOrDatetime() ) {
195            return null;
196        } elseif ( in_array( $this->mType, [ 'Integer', 'Float', 'Rating', 'Boolean', 'Text', 'Wikitext', 'Searchtext' ] ) ) {
197            return null;
198        // This leaves String, Page, etc. - see CargoUtils::fieldTypeToSQLType().
199        } elseif ( $this->mSize != null ) {
200            return $this->mSize;
201        } else {
202            global $wgCargoDefaultStringBytes;
203            return $wgCargoDefaultStringBytes;
204        }
205    }
206
207    /**
208     * @return array
209     */
210    public function toDBArray() {
211        $descriptionData = [];
212        $descriptionData['type'] = $this->mType;
213        if ( $this->mSize != null ) {
214            $descriptionData['size'] = $this->mSize;
215        }
216        if ( $this->mDependentOn != null ) {
217            $descriptionData['dependent on'] = $this->mDependentOn;
218        }
219        if ( $this->mIsList ) {
220            $descriptionData['isList'] = true;
221        }
222        if ( $this->mDelimiter != null ) {
223            $descriptionData['delimiter'] = $this->mDelimiter;
224        }
225        if ( $this->mAllowedValues != null ) {
226            $descriptionData['allowedValues'] = $this->mAllowedValues;
227        }
228        if ( $this->mIsMandatory ) {
229            $descriptionData['mandatory'] = true;
230        }
231        if ( $this->mIsUnique ) {
232            $descriptionData['unique'] = true;
233        }
234        if ( $this->mRegex != null ) {
235            $descriptionData['regex'] = $this->mRegex;
236        }
237        if ( $this->mIsHidden ) {
238            $descriptionData['hidden'] = true;
239        }
240        if ( $this->mIsHierarchy ) {
241            $descriptionData['hierarchy'] = true;
242            $descriptionData['hierarchyStructure'] = $this->mHierarchyStructure;
243        }
244        foreach ( $this->mOtherParams as $otherParam => $value ) {
245            $descriptionData[$otherParam] = $value;
246        }
247
248        return $descriptionData;
249    }
250
251    public function prepareAndValidateValue( $fieldValue ) {
252        // @TODO - also set, and return, an error message and/or code
253        // if the returned value is different from the incoming value.
254        // @TODO - it might make sense to create a new class around
255        // this function, like "CargoFieldValue" -
256        // CargoStore::getDateValueAndPrecision() could move there too.
257
258        // When a `false` (the boolean, not the string 'false') is passed
259        // by Lua, `trim( false )` is called. This presumably casts `false`
260        // to a string, which in PHP is an empty string. This does not affect
261        // `true` however, as `true` casted to a string is '1'.
262        // We use '0' if $fieldValue is a boolean false.
263        $fieldValue = $fieldValue === false ? '0' : trim( $fieldValue );
264        if ( $fieldValue == '' ) {
265            if ( $this->isDateOrDatetime() ) {
266                // If it's a date field, it has to be null,
267                // not blank, for DB storage to work correctly.
268                // Possibly this is true for other types as well.
269                return [ 'value' => null ];
270            }
271            return [ 'value' => $fieldValue ];
272        }
273
274        $fieldType = $this->mType;
275        if ( $this->mAllowedValues != null ) {
276            $allowedValues = $this->mAllowedValues;
277            if ( $this->mIsList ) {
278                $delimiter = $this->getDelimiter();
279                $individualValues = explode( $delimiter, $fieldValue );
280                $valuesToBeKept = [];
281                foreach ( $individualValues as $individualValue ) {
282                    $realIndividualVal = trim( $individualValue );
283                    if ( in_array( $realIndividualVal, $allowedValues ) ) {
284                        $valuesToBeKept[] = $realIndividualVal;
285                    }
286                }
287                // FIXME: This is dead code, it's overwritten immediately
288                $newValue = implode( $delimiter, $valuesToBeKept );
289            } else {
290                if ( in_array( $fieldValue, $allowedValues ) ) {
291                    // FIXME: This is dead code, it's overwritten immediately
292                    $newValue = $fieldValue;
293                }
294            }
295        }
296
297        $precision = null;
298        if ( $this->isDateOrDatetime() ) {
299            if ( $this->mIsList ) {
300                $delimiter = $this->getDelimiter();
301                $individualValues = explode( $delimiter, $fieldValue );
302                // There's unfortunately only one precision
303                // value per field, even if it holds more than
304                // one date - store the most "precise" of the
305                // precision values.
306                $maxPrecision = CargoStore::YEAR_ONLY;
307                $dateValues = [];
308                foreach ( $individualValues as $individualValue ) {
309                    $realIndividualVal = trim( $individualValue );
310                    if ( $realIndividualVal == '' ) {
311                        continue;
312                    }
313                    [ $dateValue, $curPrecision ] = CargoStore::getDateValueAndPrecision( $realIndividualVal, $fieldType );
314                    $dateValues[] = $dateValue;
315                    if ( $curPrecision < $maxPrecision ) {
316                        $maxPrecision = $curPrecision;
317                    }
318                }
319                $newValue = implode( $delimiter, $dateValues );
320                $precision = $maxPrecision;
321            } else {
322                [ $newValue, $precision ] = CargoStore::getDateValueAndPrecision( $fieldValue, $fieldType );
323            }
324        } elseif ( $fieldType == 'Integer' ) {
325            // Remove digit-grouping character.
326            global $wgCargoDigitGroupingCharacter;
327            if ( $this->mIsList ) {
328                $delimiter = $this->getDelimiter();
329                if ( $delimiter != $wgCargoDigitGroupingCharacter ) {
330                    $fieldValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
331                }
332                $individualValues = explode( $delimiter, $fieldValue );
333                foreach ( $individualValues as &$individualValue ) {
334                    $individualValue = round( floatval( $individualValue ) );
335                }
336                $newValue = implode( $delimiter, $individualValues );
337            } else {
338                $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
339                $newValue = round( floatval( $newValue ) );
340            }
341        } elseif ( $fieldType == 'Float' || $fieldType == 'Rating' ) {
342            // Remove digit-grouping character, and change
343            // decimal mark to '.' if it's anything else.
344            global $wgCargoDigitGroupingCharacter;
345            global $wgCargoDecimalMark;
346            $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
347            $newValue = str_replace( $wgCargoDecimalMark, '.', $newValue );
348        } elseif ( $fieldType == 'Boolean' ) {
349            // True = 1, "yes"
350            // False = 0, "no"
351            $msgForNo = wfMessage( 'htmlform-no' )->text();
352            if ( $fieldValue === 0
353                || $fieldValue === '0'
354                || strtolower( $fieldValue ) === 'no'
355                || strtolower( $fieldValue ) == strtolower( $msgForNo ) ) {
356                $newValue = '0';
357            } else {
358                $newValue = '1';
359            }
360        } else {
361            $newValue = $fieldValue;
362        }
363
364        $valueArray = [ 'value' => $newValue ];
365        if ( $precision !== null ) {
366            $valueArray['precision'] = $precision;
367        }
368
369        return $valueArray;
370    }
371
372    public function prettyPrintAllowedValues() {
373        $escapedAllowedValues = array_map( 'htmlspecialchars', $this->mAllowedValues );
374        return implode( ' &middot; ', $escapedAllowedValues );
375    }
376
377    public function prettyPrintType() {
378        $typeDesc = Html::element( 'tt', null, $this->mType );
379        if ( $this->mIsList ) {
380            $delimiter = Html::element( 'tt', null, $this->mDelimiter );
381            $typeDesc = wfMessage( 'cargo-cargotables-listof' )
382                ->rawParams( $typeDesc, $delimiter )->escaped();
383        }
384        return $typeDesc;
385    }
386
387    public function prettyPrintTypeAndAttributes() {
388        $text = $this->prettyPrintType();
389
390        $attributesStrings = [];
391        if ( $this->mIsMandatory ) {
392            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-mandatory' )->escaped() ];
393        }
394        if ( $this->mIsUnique ) {
395            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-unique' )->escaped() ];
396        }
397        if ( $this->mAllowedValues !== null ) {
398            $allowedValuesStr = $this->prettyPrintAllowedValues();
399            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-allowedvalues' )->escaped(),
400                $allowedValuesStr ];
401        }
402        if ( count( $attributesStrings ) == 0 ) {
403            return $text;
404        }
405
406        $attributeDisplayStrings = [];
407        foreach ( $attributesStrings as $attributesStrs ) {
408            $displayString = '<span class="cargoFieldName">' .
409                $attributesStrs[0] . '</span>';
410            if ( count( $attributesStrs ) > 1 ) {
411                $displayString .= ' ' . $attributesStrs[1];
412            }
413            $attributeDisplayStrings[] = $displayString;
414        }
415        $text .= ' (' . implode( '; ', $attributeDisplayStrings ) . ')';
416
417        return $text;
418    }
419
420}