Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.91% covered (warning)
84.91%
180 / 212
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoFieldDescription
84.91% covered (warning)
84.91%
180 / 212
70.00% covered (warning)
70.00%
7 / 10
116.24
0.00% covered (danger)
0.00%
0 / 1
 newFromString
88.14% covered (warning)
88.14%
52 / 59
0.00% covered (danger)
0.00%
0 / 1
20.67
 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
27
 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 / 19
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 $i => $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 ( $fieldDescription->mType == 'Text' && array_key_exists( 'unique', $fieldDescription->mOtherParams ) ) {
132            throw new MWException( "'unique' is not allowed for fields of type 'Text'." );
133        }
134        if ( $fieldDescription->mType == 'Boolean' && $fieldDescription->mIsList == true ) {
135            throw new MWException( "Error: 'list' is not allowed for fields of type 'Boolean'." );
136        }
137
138        return $fieldDescription;
139    }
140
141    /**
142     * @param array $descriptionData
143     * @return \CargoFieldDescription
144     */
145    public static function newFromDBArray( $descriptionData ) {
146        $fieldDescription = new CargoFieldDescription();
147        foreach ( $descriptionData as $param => $value ) {
148            if ( $param == 'type' ) {
149                $fieldDescription->mType = $value;
150            } elseif ( $param == 'size' ) {
151                $fieldDescription->mSize = $value;
152            } elseif ( $param == 'dependent on' ) {
153                $fieldDescription->mDependentOn = $value;
154            } elseif ( $param == 'isList' ) {
155                $fieldDescription->mIsList = true;
156            } elseif ( $param == 'delimiter' ) {
157                $fieldDescription->mDelimiter = $value;
158            } elseif ( $param == 'allowedValues' ) {
159                $fieldDescription->mAllowedValues = $value;
160            } elseif ( $param == 'mandatory' ) {
161                $fieldDescription->mIsMandatory = true;
162            } elseif ( $param == 'unique' ) {
163                $fieldDescription->mIsUnique = true;
164            } elseif ( $param == 'regex' ) {
165                $fieldDescription->mRegex = $value;
166            } elseif ( $param == 'hidden' ) {
167                $fieldDescription->mIsHidden = true;
168            } elseif ( $param == 'hierarchy' ) {
169                $fieldDescription->mIsHierarchy = true;
170            } elseif ( $param == 'hierarchyStructure' ) {
171                $fieldDescription->mHierarchyStructure = $value;
172            } else {
173                $fieldDescription->mOtherParams[$param] = $value;
174            }
175        }
176        return $fieldDescription;
177    }
178
179    public function getDelimiter() {
180        // Make "\n" represent a newline.
181        return str_replace( '\n', "\n", $this->mDelimiter );
182    }
183
184    public function setDelimiter( $delimiter ) {
185        $this->mDelimiter = $delimiter;
186    }
187
188    public function isDateOrDatetime() {
189        return in_array( $this->mType, [ 'Date', 'Start date', 'End date', 'Datetime', 'Start datetime', 'End datetime' ] );
190    }
191
192    public function getFieldSize() {
193        if ( $this->isDateOrDatetime() ) {
194            return null;
195        } elseif ( in_array( $this->mType, [ 'Integer', 'Float', 'Rating', 'Boolean', 'Text', 'Wikitext', 'Searchtext' ] ) ) {
196            return null;
197        // This leaves String, Page, etc. - see CargoUtils::fieldTypeToSQLType().
198        } elseif ( $this->mSize != null ) {
199            return $this->mSize;
200        } else {
201            global $wgCargoDefaultStringBytes;
202            return $wgCargoDefaultStringBytes;
203        }
204    }
205
206    /**
207     * @return array
208     */
209    public function toDBArray() {
210        $descriptionData = [];
211        $descriptionData['type'] = $this->mType;
212        if ( $this->mSize != null ) {
213            $descriptionData['size'] = $this->mSize;
214        }
215        if ( $this->mDependentOn != null ) {
216            $descriptionData['dependent on'] = $this->mDependentOn;
217        }
218        if ( $this->mIsList ) {
219            $descriptionData['isList'] = true;
220        }
221        if ( $this->mDelimiter != null ) {
222            $descriptionData['delimiter'] = $this->mDelimiter;
223        }
224        if ( $this->mAllowedValues != null ) {
225            $descriptionData['allowedValues'] = $this->mAllowedValues;
226        }
227        if ( $this->mIsMandatory ) {
228            $descriptionData['mandatory'] = true;
229        }
230        if ( $this->mIsUnique ) {
231            $descriptionData['unique'] = true;
232        }
233        if ( $this->mRegex != null ) {
234            $descriptionData['regex'] = $this->mRegex;
235        }
236        if ( $this->mIsHidden ) {
237            $descriptionData['hidden'] = true;
238        }
239        if ( $this->mIsHierarchy ) {
240            $descriptionData['hierarchy'] = true;
241            $descriptionData['hierarchyStructure'] = $this->mHierarchyStructure;
242        }
243        foreach ( $this->mOtherParams as $otherParam => $value ) {
244            $descriptionData[$otherParam] = $value;
245        }
246
247        return $descriptionData;
248    }
249
250    public function prepareAndValidateValue( $fieldValue ) {
251        // @TODO - also set, and return, an error message and/or code
252        // if the returned value is different from the incoming value.
253        // @TODO - it might make sense to create a new class around
254        // this function, like "CargoFieldValue" -
255        // CargoStore::getDateValueAndPrecision() could move there too.
256        $fieldValue = trim( $fieldValue );
257        if ( $fieldValue == '' ) {
258            if ( $this->isDateOrDatetime() ) {
259                // If it's a date field, it has to be null,
260                // not blank, for DB storage to work correctly.
261                // Possibly this is true for other types as well.
262                return [ 'value' => null ];
263            }
264            return [ 'value' => $fieldValue ];
265        }
266
267        $newValue = $precision = null;
268
269        $fieldType = $this->mType;
270        if ( $this->mAllowedValues != null ) {
271            $allowedValues = $this->mAllowedValues;
272            if ( $this->mIsList ) {
273                $delimiter = $this->getDelimiter();
274                $individualValues = explode( $delimiter, $fieldValue );
275                $valuesToBeKept = [];
276                foreach ( $individualValues as $individualValue ) {
277                    $realIndividualVal = trim( $individualValue );
278                    if ( in_array( $realIndividualVal, $allowedValues ) ) {
279                        $valuesToBeKept[] = $realIndividualVal;
280                    }
281                }
282                $newValue = implode( $delimiter, $valuesToBeKept );
283            } else {
284                if ( in_array( $fieldValue, $allowedValues ) ) {
285                    $newValue = $fieldValue;
286                }
287            }
288        }
289
290        if ( $this->isDateOrDatetime() ) {
291            if ( $this->mIsList ) {
292                $delimiter = $this->getDelimiter();
293                $individualValues = explode( $delimiter, $fieldValue );
294                // There's unfortunately only one precision
295                // value per field, even if it holds more than
296                // one date - store the most "precise" of the
297                // precision values.
298                $maxPrecision = CargoStore::YEAR_ONLY;
299                $dateValues = [];
300                foreach ( $individualValues as $individualValue ) {
301                    $realIndividualVal = trim( $individualValue );
302                    if ( $realIndividualVal == '' ) {
303                        continue;
304                    }
305                    list( $dateValue, $curPrecision ) = CargoStore::getDateValueAndPrecision( $realIndividualVal, $fieldType );
306                    $dateValues[] = $dateValue;
307                    if ( $curPrecision < $maxPrecision ) {
308                        $maxPrecision = $curPrecision;
309                    }
310                }
311                $newValue = implode( $delimiter, $dateValues );
312                $precision = $maxPrecision;
313            } else {
314                list( $newValue, $precision ) = CargoStore::getDateValueAndPrecision( $fieldValue, $fieldType );
315            }
316        } elseif ( $fieldType == 'Integer' ) {
317            // Remove digit-grouping character.
318            global $wgCargoDigitGroupingCharacter;
319            if ( $this->mIsList ) {
320                $delimiter = $this->getDelimiter();
321                if ( $delimiter != $wgCargoDigitGroupingCharacter ) {
322                    $fieldValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
323                }
324                $individualValues = explode( $delimiter, $fieldValue );
325                foreach ( $individualValues as &$individualValue ) {
326                    if ( !is_int( $individualValue ) ) {
327                        $individualValue = round( $individualValue );
328                    }
329                }
330                $newValue = implode( $delimiter, $individualValues );
331            } else {
332                $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
333                if ( !is_int( $newValue ) ) {
334                    $newValue = round( $newValue );
335                }
336            }
337        } elseif ( $fieldType == 'Float' || $fieldType == 'Rating' ) {
338            // Remove digit-grouping character, and change
339            // decimal mark to '.' if it's anything else.
340            global $wgCargoDigitGroupingCharacter;
341            global $wgCargoDecimalMark;
342            $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue );
343            $newValue = str_replace( $wgCargoDecimalMark, '.', $newValue );
344        } elseif ( $fieldType == 'Boolean' ) {
345            // True = 1, "yes"
346            // False = 0, "no"
347            $msgForNo = wfMessage( 'htmlform-no' )->text();
348            if ( $fieldValue === 0
349                || $fieldValue === '0'
350                || strtolower( $fieldValue ) === 'no'
351                || strtolower( $fieldValue ) == strtolower( $msgForNo ) ) {
352                $newValue = '0';
353            } else {
354                $newValue = '1';
355            }
356        } else {
357            $newValue = $fieldValue;
358        }
359
360        $valueArray = [ 'value' => $newValue ];
361        if ( $precision !== null ) {
362            $valueArray['precision'] = $precision;
363        }
364
365        return $valueArray;
366    }
367
368    public function prettyPrintType() {
369        $typeDesc = '<tt>' . $this->mType . '</tt>';
370        if ( $this->mIsList ) {
371            $delimiter = '<tt>' . $this->mDelimiter . '</tt>';
372            $typeDesc = wfMessage( 'cargo-cargotables-listof',
373                $typeDesc, $delimiter )->parse();
374        }
375        return $typeDesc;
376    }
377
378    public function prettyPrintTypeAndAttributes() {
379        $text = $this->prettyPrintType();
380
381        $attributesStrings = [];
382        if ( $this->mIsMandatory ) {
383            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-mandatory' )->text() ];
384        }
385        if ( $this->mIsUnique ) {
386            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-unique' )->text() ];
387        }
388        if ( $this->mAllowedValues !== null ) {
389            $allowedValuesStr = implode( ' &middot; ', $this->mAllowedValues );
390            $attributesStrings[] = [ wfMessage( 'cargo-cargotables-allowedvalues' )->text(),
391                $allowedValuesStr ];
392        }
393        if ( count( $attributesStrings ) == 0 ) {
394            return $text;
395        }
396
397        $attributeDisplayStrings = [];
398        foreach ( $attributesStrings as $attributesStrs ) {
399            $displayString = '<span class="cargoFieldName">' .
400                $attributesStrs[0] . '</span>';
401            if ( count( $attributesStrs ) > 1 ) {
402                $displayString .= ' ' . $attributesStrs[1];
403            }
404            $attributeDisplayStrings[] = $displayString;
405        }
406        $text .= ' (' . implode( '; ', $attributeDisplayStrings ) . ')';
407
408        return $text;
409    }
410
411}