Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.19% covered (warning)
84.19%
181 / 215
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoFieldDescription
84.19% covered (warning)
84.19%
181 / 215
70.00% covered (warning)
70.00%
7 / 10
116.93
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
25
 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        $fieldValue = trim( $fieldValue );
258        if ( $fieldValue == '' ) {
259            if ( $this->isDateOrDatetime() ) {
260                // If it's a date field, it has to be null,
261                // not blank, for DB storage to work correctly.
262                // Possibly this is true for other types as well.
263                return [ 'value' => null ];
264            }
265            return [ 'value' => $fieldValue ];
266        }
267
268        $fieldType = $this->mType;
269        if ( $this->mAllowedValues != null ) {
270            $allowedValues = $this->mAllowedValues;
271            if ( $this->mIsList ) {
272                $delimiter = $this->getDelimiter();
273                $individualValues = explode( $delimiter, $fieldValue );
274                $valuesToBeKept = [];
275                foreach ( $individualValues as $individualValue ) {
276                    $realIndividualVal = trim( $individualValue );
277                    if ( in_array( $realIndividualVal, $allowedValues ) ) {
278                        $valuesToBeKept[] = $realIndividualVal;
279                    }
280                }
281                // FIXME: This is dead code, it's overwritten immediately
282                $newValue = implode( $delimiter, $valuesToBeKept );
283            } else {
284                if ( in_array( $fieldValue, $allowedValues ) ) {
285                    // FIXME: This is dead code, it's overwritten immediately
286                    $newValue = $fieldValue;
287                }
288            }
289        }
290
291        $precision = null;
292        if ( $this->isDateOrDatetime() ) {
293            if ( $this->mIsList ) {
294                $delimiter = $this->getDelimiter();
295                $individualValues = explode( $delimiter, $fieldValue );
296                // There's unfortunately only one precision
297                // value per field, even if it holds more than
298                // one date - store the most "precise" of the
299                // precision values.
300                $maxPrecision = CargoStore::YEAR_ONLY;
301                $dateValues = [];
302                foreach ( $individualValues as $individualValue ) {
303                    $realIndividualVal = trim( $individualValue );
304                    if ( $realIndividualVal == '' ) {
305                        continue;
306                    }
307                    [ $dateValue, $curPrecision ] = CargoStore::getDateValueAndPrecision( $realIndividualVal, $fieldType );
308                    $dateValues[] = $dateValue;
309                    if ( $curPrecision < $maxPrecision ) {
310                        $maxPrecision = $curPrecision;
311                    }
312                }
313                $newValue = implode( $delimiter, $dateValues );
314                $precision = $maxPrecision;
315            } else {
316                [ $newValue, $precision ] = CargoStore::getDateValueAndPrecision( $fieldValue, $fieldType );
317            }
318        } elseif ( $fieldType == 'Integer' ) {
319            // Remove digit-grouping character.
320            global $wgCargoDigitGroupingCharacter;
321            if ( $this->mIsList ) {
322                $delimiter = $this->getDelimiter();
323                if ( $delimiter != $wgCargoDigitGroupingCharacter ) {
324                    $fieldValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
325                }
326                $individualValues = explode( $delimiter, $fieldValue );
327                foreach ( $individualValues as &$individualValue ) {
328                    $individualValue = round( floatval( $individualValue ) );
329                }
330                $newValue = implode( $delimiter, $individualValues );
331            } else {
332                $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
333                $newValue = round( floatval( $newValue ) );
334            }
335        } elseif ( $fieldType == 'Float' || $fieldType == 'Rating' ) {
336            // Remove digit-grouping character, and change
337            // decimal mark to '.' if it's anything else.
338            global $wgCargoDigitGroupingCharacter;
339            global $wgCargoDecimalMark;
340            $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
341            $newValue = str_replace( $wgCargoDecimalMark, '.', $newValue );
342        } elseif ( $fieldType == 'Boolean' ) {
343            // True = 1, "yes"
344            // False = 0, "no"
345            $msgForNo = wfMessage( 'htmlform-no' )->text();
346            if ( $fieldValue === 0
347                || $fieldValue === '0'
348                || strtolower( $fieldValue ) === 'no'
349                || strtolower( $fieldValue ) == strtolower( $msgForNo ) ) {
350                $newValue = '0';
351            } else {
352                $newValue = '1';
353            }
354        } else {
355            $newValue = $fieldValue;
356        }
357
358        $valueArray = [ 'value' => $newValue ];
359        if ( $precision !== null ) {
360            $valueArray['precision'] = $precision;
361        }
362
363        return $valueArray;
364    }
365
366    public function prettyPrintType() {
367        $typeDesc = '<tt>' . $this->mType . '</tt>';
368        if ( $this->mIsList ) {
369            $delimiter = '<tt>' . $this->mDelimiter . '</tt>';
370            $typeDesc = wfMessage( 'cargo-cargotables-listof',
371                $typeDesc, $delimiter )->parse();
372        }
373        return $typeDesc;
374    }
375
376    public function prettyPrintTypeAndAttributes() {
377        $text = $this->prettyPrintType();
378
379        $attributesStrings = [];
380        if ( $this->mIsMandatory ) {
381            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-mandatory' )->text() ];
382        }
383        if ( $this->mIsUnique ) {
384            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-unique' )->text() ];
385        }
386        if ( $this->mAllowedValues !== null ) {
387            $allowedValuesStr = implode( ' &middot; ', $this->mAllowedValues );
388            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-allowedvalues' )->text(),
389                $allowedValuesStr ];
390        }
391        if ( count( $attributesStrings ) == 0 ) {
392            return $text;
393        }
394
395        $attributeDisplayStrings = [];
396        foreach ( $attributesStrings as $attributesStrs ) {
397            $displayString = '<span class="cargoFieldName">' .
398                $attributesStrs[0] . '</span>';
399            if ( count( $attributesStrs ) > 1 ) {
400                $displayString .= ' ' . $attributesStrs[1];
401            }
402            $attributeDisplayStrings[] = $displayString;
403        }
404        $text .= ' (' . implode( '; ', $attributeDisplayStrings ) . ')';
405
406        return $text;
407    }
408
409}