Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.15% |
11 / 52 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
DbStorage | |
21.15% |
11 / 52 |
|
28.57% |
2 / 7 |
384.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
preprocessNestedSqlArray | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
preprocessSqlArray | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFieldRegexFragment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateOptions | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
240 | |||
validate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
calcUpdates | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 |
1 | <?php |
2 | |
3 | namespace Flow\Data\Storage; |
4 | |
5 | use Flow\Data\ObjectManager; |
6 | use Flow\Data\ObjectStorage; |
7 | use Flow\DbFactory; |
8 | use Flow\Exception\DataModelException; |
9 | use Flow\Model\UUID; |
10 | |
11 | /** |
12 | * Base class for all ObjectStorage implementers |
13 | * which use a database as the backing store. |
14 | * |
15 | * Includes some utility methods for database management and |
16 | * SQL security. |
17 | */ |
18 | abstract class DbStorage implements ObjectStorage { |
19 | /** |
20 | * @var DbFactory |
21 | */ |
22 | protected $dbFactory; |
23 | |
24 | /** |
25 | * The revision columns allowed to be updated |
26 | * |
27 | * @var string[]|true Allow of selective columns to allow, or true to allow |
28 | * everything |
29 | */ |
30 | protected $allowedUpdateColumns = true; |
31 | |
32 | /** |
33 | * This is to prevent 'Update not allowed on xxx' error during moderation when |
34 | * * old cache is not purged and still holds obsolete deleted column |
35 | * * old cache is not purged and doesn't have the newly added column |
36 | * |
37 | * @var string[] Array of columns to ignore |
38 | */ |
39 | protected $obsoleteUpdateColumns = []; |
40 | |
41 | /** |
42 | * @param DbFactory $dbFactory |
43 | */ |
44 | public function __construct( DbFactory $dbFactory ) { |
45 | $this->dbFactory = $dbFactory; |
46 | } |
47 | |
48 | /** |
49 | * Runs preprocessSqlArray on each element of an array. |
50 | * |
51 | * @param array $outer The array to check |
52 | * @return array Preprocessed SQL array. |
53 | * @throws DataModelException |
54 | */ |
55 | protected function preprocessNestedSqlArray( array $outer ) { |
56 | foreach ( $outer as $i => $data ) { |
57 | if ( !is_array( $data ) ) { |
58 | throw new DataModelException( "Unexpected non-array in nested SQL array" ); |
59 | } |
60 | $outer[$i] = $this->preprocessSqlArray( $data ); |
61 | } |
62 | return $outer; |
63 | } |
64 | |
65 | /** |
66 | * Finds UUID objects and returns their database representation. |
67 | * |
68 | * @param array $data Query conditions for IDatabase::select |
69 | * @return array query conditions |
70 | */ |
71 | protected function preprocessSqlArray( array $data ) { |
72 | $data = UUID::convertUUIDs( $data, 'binary' ); |
73 | |
74 | return $data; |
75 | } |
76 | |
77 | /** |
78 | * Returns a regular expression fragment suitable for matching a valid |
79 | * SQL field name, and hopefully no injection attacks |
80 | * @return string Regular expression fragment |
81 | */ |
82 | protected function getFieldRegexFragment() { |
83 | return '\s*[A-Za-z0-9\._]+\s*'; |
84 | } |
85 | |
86 | /** |
87 | * Internal security function to check an options array for |
88 | * SQL injection and other funkiness |
89 | * @todo Currently only supports LIMIT, OFFSET and ORDER BY |
90 | * @param array $options An options array passed to a query. |
91 | * @return bool |
92 | */ |
93 | protected function validateOptions( $options ) { |
94 | static $validUnaryOptions = [ |
95 | 'UNIQUE', |
96 | 'EXPLAIN', |
97 | ]; |
98 | |
99 | $fieldRegex = $this->getFieldRegexFragment(); |
100 | |
101 | foreach ( $options as $key => $value ) { |
102 | if ( is_numeric( $key ) && in_array( strtoupper( $value ), $validUnaryOptions ) ) { |
103 | continue; |
104 | } elseif ( is_numeric( $key ) ) { |
105 | wfDebug( __METHOD__ . ": Unrecognised unary operator $value\n" ); |
106 | return false; |
107 | } |
108 | |
109 | if ( $key === 'LIMIT' ) { |
110 | // LIMIT is one or two integers, separated by a comma. |
111 | if ( !preg_match( '/^\d+(,\d+)?$/', $value ) ) { |
112 | wfDebug( __METHOD__ . ": Invalid LIMIT $value\n" ); |
113 | return false; |
114 | } |
115 | } elseif ( $key === 'ORDER BY' ) { |
116 | // ORDER BY is a list of field names with ASC / DESC afterwards |
117 | if ( is_string( $value ) ) { |
118 | $value = explode( ',', $value ); |
119 | } |
120 | $orderByRegex = "/^\s*$fieldRegex\s*(ASC|DESC)?\s*$/i"; |
121 | |
122 | foreach ( $value as $orderByField ) { |
123 | if ( !preg_match( $orderByRegex, $orderByField ) ) { |
124 | wfDebug( __METHOD__ . ": invalid ORDER BY field $orderByField\n" ); |
125 | return false; |
126 | } |
127 | } |
128 | } elseif ( $key === 'OFFSET' ) { |
129 | // OFFSET is just an integer |
130 | if ( !is_numeric( $value ) ) { |
131 | wfDebug( __METHOD__ . ": non-numeric offset $value\n" ); |
132 | return false; |
133 | } |
134 | } elseif ( $key === 'GROUP BY' ) { |
135 | if ( !preg_match( "/^{$fieldRegex}(,{$fieldRegex})+$/", $value ) ) { |
136 | wfDebug( __METHOD__ . ": invalid GROUP BY field\n" ); |
137 | } |
138 | } else { |
139 | wfDebug( __METHOD__ . ": Unknown option $key\n" ); |
140 | return false; |
141 | } |
142 | } |
143 | |
144 | // Everything passes |
145 | return true; |
146 | } |
147 | |
148 | /** |
149 | * @inheritDoc |
150 | */ |
151 | public function validate( array $row ) { |
152 | return true; |
153 | } |
154 | |
155 | /** |
156 | * Calculates the DB updates to be performed to update data from $old to |
157 | * $new. |
158 | * |
159 | * @param array $old |
160 | * @param array $new |
161 | * @return array |
162 | * @throws DataModelException |
163 | */ |
164 | public function calcUpdates( array $old, array $new ) { |
165 | $changeSet = ObjectManager::calcUpdatesWithoutValidation( $old, $new ); |
166 | |
167 | foreach ( $this->obsoleteUpdateColumns as $val ) { |
168 | // Need to use array_key_exists to check null value |
169 | if ( array_key_exists( $val, $changeSet ) ) { |
170 | unset( $changeSet[$val] ); |
171 | } |
172 | } |
173 | |
174 | if ( is_array( $this->allowedUpdateColumns ) ) { |
175 | $extra = array_diff( array_keys( $changeSet ), $this->allowedUpdateColumns ); |
176 | if ( $extra ) { |
177 | throw new DataModelException( 'Update not allowed on: ' . implode( ', ', $extra ), 'process-data' ); |
178 | } |
179 | } |
180 | |
181 | return $changeSet; |
182 | } |
183 | } |