Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 140 |
|
0.00% |
0 / 28 |
CRAP | |
0.00% |
0 / 1 |
MWOAuthDAO | |
0.00% |
0 / 140 |
|
0.00% |
0 / 28 |
2652 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
newFromArray | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getConsumerClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newFromRow | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromId | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
get | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setField | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setFields | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getFieldNames | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
save | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
72 | |||
delete | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getSchema | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFieldPermissionChecks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTable | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFieldColumnMap | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getColumn | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
hasField | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getAutoIncrField | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getIdColumn | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getIdValue | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
loadFromValues | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
normalizeValues | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
loadFromRow | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
encodeRow | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
decodeRow | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getRowArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
userCanAccess | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getChangeToken | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
checkChangeToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPending | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
updateOrigin | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Backend; |
4 | |
5 | use Exception; |
6 | use IContextSource; |
7 | use IDBAccessObject; |
8 | use LogicException; |
9 | use MediaWiki\Logger\LoggerFactory; |
10 | use Message; |
11 | use MWException; |
12 | use Psr\Log\LoggerInterface; |
13 | use stdClass; |
14 | use Wikimedia\Rdbms\DBError; |
15 | use Wikimedia\Rdbms\DBReadOnlyError; |
16 | use Wikimedia\Rdbms\IDatabase; |
17 | |
18 | /** |
19 | * (c) Aaron Schulz 2013, GPL |
20 | * |
21 | * This program is free software; you can redistribute it and/or modify |
22 | * it under the terms of the GNU General Public License as published by |
23 | * the Free Software Foundation; either version 2 of the License, or |
24 | * (at your option) any later version. |
25 | * |
26 | * This program is distributed in the hope that it will be useful, |
27 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
28 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
29 | * GNU General Public License for more details. |
30 | * |
31 | * You should have received a copy of the GNU General Public License along |
32 | * with this program; if not, write to the Free Software Foundation, Inc., |
33 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
34 | * http://www.gnu.org/copyleft/gpl.html |
35 | */ |
36 | |
37 | /** |
38 | * Representation of a Data Access Object |
39 | */ |
40 | abstract class MWOAuthDAO { |
41 | /** @var string object construction origin */ |
42 | private $daoOrigin = 'new'; |
43 | /** @var bool whether fields changed or the field is new */ |
44 | private $daoPending = true; |
45 | |
46 | /** @var LoggerInterface */ |
47 | protected $logger; |
48 | |
49 | /** |
50 | * @throws LogicException |
51 | */ |
52 | final protected function __construct() { |
53 | $fields = array_keys( static::getFieldPermissionChecks() ); |
54 | if ( array_diff( $fields, $this->getFieldNames() ) ) { |
55 | throw new LogicException( "Invalid field(s) defined in access check methods." ); |
56 | } |
57 | $this->logger = LoggerFactory::getInstance( 'OAuth' ); |
58 | } |
59 | |
60 | /** |
61 | * @param array $values (field => value) map |
62 | * @return static |
63 | */ |
64 | final public static function newFromArray( array $values ) { |
65 | $class = static::getConsumerClass( $values ); |
66 | $consumer = new $class(); |
67 | |
68 | // Make sure oauth_version is set - for backwards compat |
69 | $values['oauth_version'] ??= Consumer::OAUTH_VERSION_1; |
70 | $consumer->loadFromValues( $values ); |
71 | return $consumer; |
72 | } |
73 | |
74 | /** |
75 | * Determine and return the correct consumer class |
76 | * |
77 | * @param array $data |
78 | * @return string |
79 | */ |
80 | protected static function getConsumerClass( array $data ) { |
81 | return static::class; |
82 | } |
83 | |
84 | /** |
85 | * @param IDatabase $db |
86 | * @param array|stdClass $row |
87 | * @return static |
88 | */ |
89 | final public static function newFromRow( IDatabase $db, $row ) { |
90 | $class = static::getConsumerClass( (array)$row ); |
91 | $consumer = new $class(); |
92 | $consumer->loadFromRow( $db, $row ); |
93 | return $consumer; |
94 | } |
95 | |
96 | /** |
97 | * @param IDatabase $db |
98 | * @param int $id |
99 | * @param int $flags IDBAccessObject::READ_* bitfield |
100 | * @return static|bool Returns false if not found |
101 | * @throws DBError |
102 | */ |
103 | final public static function newFromId( IDatabase $db, $id, $flags = 0 ) { |
104 | $row = $db->selectRow( static::getTable(), |
105 | array_values( static::getFieldColumnMap() ), |
106 | [ static::getIdColumn() => (int)$id ], |
107 | __METHOD__, |
108 | ( $flags & IDBAccessObject::READ_LOCKING ) ? [ 'FOR UPDATE' ] : [] |
109 | ); |
110 | |
111 | if ( $row ) { |
112 | $class = static::getConsumerClass( (array)$row ); |
113 | $consumer = new $class(); |
114 | $consumer->loadFromRow( $db, $row ); |
115 | return $consumer; |
116 | } else { |
117 | return false; |
118 | } |
119 | } |
120 | |
121 | /** |
122 | * Get the value of a field |
123 | * |
124 | * @param string $name |
125 | * @return mixed |
126 | * @throws LogicException |
127 | */ |
128 | final public function get( $name ) { |
129 | if ( !static::hasField( $name ) ) { |
130 | throw new LogicException( "Object has no '$name' field." ); |
131 | } |
132 | return $this->$name; |
133 | } |
134 | |
135 | /** |
136 | * Set the value of a field |
137 | * |
138 | * @param string $name |
139 | * @param mixed $value |
140 | * @return mixed The old value |
141 | * @throws Exception |
142 | */ |
143 | final public function setField( $name, $value ) { |
144 | $old = $this->setFields( [ $name => $value ] ); |
145 | return $old[$name]; |
146 | } |
147 | |
148 | /** |
149 | * Set the values for a set of fields |
150 | * |
151 | * @param array $values (field => value) map |
152 | * @throws LogicException |
153 | * @return array Map of old values |
154 | */ |
155 | final public function setFields( array $values ) { |
156 | $old = []; |
157 | foreach ( $values as $name => $value ) { |
158 | if ( !static::hasField( $name ) ) { |
159 | throw new LogicException( "Object has no '$name' field." ); |
160 | } |
161 | $old[$name] = $this->$name; |
162 | $this->$name = $value; |
163 | if ( $old[$name] !== $value ) { |
164 | $this->daoPending = true; |
165 | } |
166 | } |
167 | $this->normalizeValues(); |
168 | return $old; |
169 | } |
170 | |
171 | /** |
172 | * @return string[] |
173 | */ |
174 | final public function getFieldNames() { |
175 | return array_keys( static::getFieldColumnMap() ); |
176 | } |
177 | |
178 | /** |
179 | * @param IDatabase $dbw |
180 | * @return bool |
181 | * @throws DBReadOnlyError |
182 | */ |
183 | public function save( IDatabase $dbw ) { |
184 | global $wgMWOAuthReadOnly; |
185 | |
186 | $uniqueId = $this->getIdValue(); |
187 | $idColumn = static::getIdColumn(); |
188 | if ( $wgMWOAuthReadOnly ) { |
189 | throw new DBReadOnlyError( $dbw, __CLASS__ . ": tried to save while db is read-only" ); |
190 | } |
191 | if ( $this->daoOrigin === 'db' ) { |
192 | if ( $this->daoPending ) { |
193 | $this->logger->debug( get_class( $this ) . ': performing DB update; object changed.' ); |
194 | $dbw->update( |
195 | static::getTable(), |
196 | $this->getRowArray( $dbw ), |
197 | [ $idColumn => $uniqueId ], |
198 | __METHOD__ |
199 | ); |
200 | $this->daoPending = false; |
201 | return $dbw->affectedRows() > 0; |
202 | } else { |
203 | $this->logger->debug( get_class( $this ) . ': skipping DB update; object unchanged.' ); |
204 | return false; |
205 | } |
206 | } else { |
207 | $this->logger->debug( get_class( $this ) . ': performing DB update; new object.' ); |
208 | $afield = static::getAutoIncrField(); |
209 | $acolumn = $afield !== null ? static::getColumn( $afield ) : null; |
210 | $row = $this->getRowArray( $dbw ); |
211 | if ( $acolumn !== null && $row[$acolumn] === null ) { |
212 | // auto-increment field should be omitted, not set null, for |
213 | // auto-incrementing behavior |
214 | unset( $row[$acolumn] ); |
215 | } |
216 | $dbw->insert( |
217 | static::getTable(), |
218 | $row, |
219 | __METHOD__ |
220 | ); |
221 | if ( $afield !== null ) { |
222 | // update field for auto-increment field |
223 | $this->$afield = $dbw->insertId(); |
224 | } |
225 | $this->daoPending = false; |
226 | return true; |
227 | } |
228 | } |
229 | |
230 | /** |
231 | * @param IDatabase $dbw |
232 | * @return bool |
233 | * @throws DBReadOnlyError |
234 | */ |
235 | public function delete( IDatabase $dbw ) { |
236 | global $wgMWOAuthReadOnly; |
237 | |
238 | $uniqueId = $this->getIdValue(); |
239 | $idColumn = static::getIdColumn(); |
240 | if ( $wgMWOAuthReadOnly ) { |
241 | throw new DBReadOnlyError( $dbw, __CLASS__ . ": tried to delete while db is read-only" ); |
242 | } |
243 | if ( $this->daoOrigin === 'db' ) { |
244 | $dbw->delete( |
245 | static::getTable(), |
246 | [ $idColumn => $uniqueId ], |
247 | __METHOD__ |
248 | ); |
249 | $this->daoPending = true; |
250 | return $dbw->affectedRows() > 0; |
251 | } else { |
252 | return false; |
253 | } |
254 | } |
255 | |
256 | /** |
257 | * Get the schema information for this object type |
258 | * |
259 | * This should return an associative array with: |
260 | * - idField : a field with an int/hex UNIQUE identifier |
261 | * - autoIncrField : a field that auto-increments in the DB (or NULL if none) |
262 | * - table : a table name |
263 | * - fieldColumnMap : a map of field names to column names |
264 | * |
265 | * @return array |
266 | */ |
267 | protected static function getSchema() { |
268 | // Note: declaring this abstract raises E_STRICT |
269 | throw new MWException( "getSchema() not defined in " . self::class ); |
270 | } |
271 | |
272 | /** |
273 | * Get the access control check methods for this object type |
274 | * |
275 | * This returns a map of field names to method names. |
276 | * The methods check if a context user has access to the field, |
277 | * returning true if they do and a Message object otherwise. |
278 | * The methods take (field name, IContextSource) as arguments. |
279 | * |
280 | * @see MWOAuthDAO::userCanAccess() |
281 | * @see MWOAuthDAOAccessControl |
282 | * |
283 | * @throws LogicException Subclasses must override |
284 | * @return array<string,string> Map of (field name => name of method that checks access) |
285 | */ |
286 | protected static function getFieldPermissionChecks() { |
287 | // Note: declaring this abstract raises E_STRICT |
288 | throw new LogicException( "getFieldPermissionChecks() not defined in " . self::class ); |
289 | } |
290 | |
291 | /** |
292 | * @return string |
293 | */ |
294 | final protected static function getTable() { |
295 | $schema = static::getSchema(); |
296 | return $schema['table']; |
297 | } |
298 | |
299 | /** |
300 | * @return array<string,string> |
301 | */ |
302 | final protected static function getFieldColumnMap() { |
303 | $schema = static::getSchema(); |
304 | return $schema['fieldColumnMap']; |
305 | } |
306 | |
307 | /** |
308 | * @param string $field |
309 | * @return string |
310 | */ |
311 | final protected static function getColumn( $field ) { |
312 | $schema = static::getSchema(); |
313 | return $schema['fieldColumnMap'][$field]; |
314 | } |
315 | |
316 | /** |
317 | * @param string $field |
318 | * @return bool |
319 | */ |
320 | final protected static function hasField( $field ) { |
321 | $schema = static::getSchema(); |
322 | return isset( $schema['fieldColumnMap'][$field] ); |
323 | } |
324 | |
325 | /** |
326 | * @return string|null |
327 | */ |
328 | final protected static function getAutoIncrField() { |
329 | $schema = static::getSchema(); |
330 | return $schema['autoIncrField'] ?? null; |
331 | } |
332 | |
333 | /** |
334 | * @return string |
335 | */ |
336 | final protected static function getIdColumn() { |
337 | $schema = static::getSchema(); |
338 | return $schema['fieldColumnMap'][$schema['idField']]; |
339 | } |
340 | |
341 | /** |
342 | * @return int|string |
343 | */ |
344 | final protected function getIdValue() { |
345 | $schema = static::getSchema(); |
346 | $field = $schema['idField']; |
347 | return $this->$field; |
348 | } |
349 | |
350 | /** |
351 | * @param array $values |
352 | */ |
353 | final protected function loadFromValues( array $values ) { |
354 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
355 | if ( !array_key_exists( $field, $values ) ) { |
356 | throw new MWException( get_class( $this ) . " requires '$field' field." ); |
357 | } |
358 | $this->$field = $values[$field]; |
359 | } |
360 | $this->normalizeValues(); |
361 | $this->daoOrigin = 'new'; |
362 | $this->daoPending = true; |
363 | } |
364 | |
365 | /** |
366 | * Subclasses should make this normalize fields (e.g. timestamps) |
367 | * |
368 | * @return void |
369 | */ |
370 | abstract protected function normalizeValues(); |
371 | |
372 | /** |
373 | * @param IDatabase $db |
374 | * @param stdClass|array $row |
375 | * @return void |
376 | */ |
377 | final protected function loadFromRow( IDatabase $db, $row ) { |
378 | $row = $this->decodeRow( $db, (array)$row ); |
379 | $values = []; |
380 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
381 | $values[$field] = $row[$column]; |
382 | } |
383 | $this->loadFromValues( $values ); |
384 | $this->daoOrigin = 'db'; |
385 | $this->daoPending = false; |
386 | } |
387 | |
388 | /** |
389 | * Subclasses should make this to encode DB fields (e.g. timestamps). |
390 | * This must also flatten any PHP data structures into flat values. |
391 | * |
392 | * @param IDatabase $db |
393 | * @param array $row |
394 | * @return array |
395 | */ |
396 | abstract protected function encodeRow( IDatabase $db, $row ); |
397 | |
398 | /** |
399 | * Subclasses should make this to decode DB fields (e.g. timestamps). |
400 | * This can also expand some flat values (e.g. JSON) into PHP data structures. |
401 | * Note: this does not need to handle what normalizeValues() already does. |
402 | * |
403 | * @param IDatabase $db |
404 | * @param array $row |
405 | * @return array |
406 | */ |
407 | abstract protected function decodeRow( IDatabase $db, $row ); |
408 | |
409 | /** |
410 | * @param IDatabase $db |
411 | * @return array |
412 | */ |
413 | final protected function getRowArray( IDatabase $db ) { |
414 | $row = []; |
415 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
416 | $row[$column] = $this->$field; |
417 | } |
418 | return $this->encodeRow( $db, $row ); |
419 | } |
420 | |
421 | /** |
422 | * Check if a user (from the context) can view a field |
423 | * |
424 | * @see MWOAuthDAO::userCanAccess() |
425 | * @see MWOAuthDAOAccessControl |
426 | * |
427 | * @param string $name |
428 | * @param IContextSource $context |
429 | * @return Message|true Returns on success or a Message if the user lacks access |
430 | */ |
431 | final public function userCanAccess( $name, IContextSource $context ) { |
432 | $map = static::getFieldPermissionChecks(); |
433 | if ( isset( $map[$name] ) ) { |
434 | $method = $map[$name]; |
435 | return $this->$method( $name, $context ); |
436 | } else { |
437 | return true; |
438 | } |
439 | } |
440 | |
441 | /** |
442 | * Get the current conflict token value for a user |
443 | * |
444 | * @param IContextSource $context |
445 | * @return string Hex token |
446 | */ |
447 | final public function getChangeToken( IContextSource $context ) { |
448 | $map = []; |
449 | foreach ( $this->getFieldNames() as $field ) { |
450 | if ( $this->userCanAccess( $field, $context ) ) { |
451 | $map[$field] = $this->$field; |
452 | } else { |
453 | // don't convey this information |
454 | $map[$field] = null; |
455 | } |
456 | } |
457 | return hash_hmac( |
458 | 'sha1', |
459 | serialize( $map ), |
460 | $context->getUser()->getId() . '#' . $this->getIdValue() |
461 | ); |
462 | } |
463 | |
464 | /** |
465 | * Compare an old change token to the current one |
466 | * |
467 | * @param IContextSource $context |
468 | * @param string $oldToken |
469 | * @return bool Whether the current is unchanged |
470 | */ |
471 | final public function checkChangeToken( IContextSource $context, $oldToken ) { |
472 | return ( $this->getChangeToken( $context ) === $oldToken ); |
473 | } |
474 | |
475 | /** |
476 | * Update whether this object should be written to the data store |
477 | * @param bool $pending set to true to mark this object as needing to write its data |
478 | */ |
479 | public function setPending( $pending ) { |
480 | $this->daoPending = $pending; |
481 | } |
482 | |
483 | /** |
484 | * Update the origin of this object |
485 | * @param string $source source of the object |
486 | * 'new': Treat this as a new object to the datastore (insert on save) |
487 | * 'db': Treat this as already in the datastore (update on save) |
488 | */ |
489 | public function updateOrigin( $source ) { |
490 | $this->daoOrigin = $source; |
491 | } |
492 | } |