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