Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 142 |
|
0.00% |
0 / 28 |
CRAP | |
0.00% |
0 / 1 |
MWOAuthDAO | |
0.00% |
0 / 142 |
|
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 / 14 |
|
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 LogicException; |
7 | use MediaWiki\Context\IContextSource; |
8 | use MediaWiki\Logger\LoggerFactory; |
9 | use MediaWiki\Message\Message; |
10 | use MWException; |
11 | use Psr\Log\LoggerInterface; |
12 | use stdClass; |
13 | use Wikimedia\Rdbms\DBError; |
14 | use Wikimedia\Rdbms\DBReadOnlyError; |
15 | use Wikimedia\Rdbms\IDatabase; |
16 | use Wikimedia\Rdbms\IDBAccessObject; |
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 | $queryBuilder = $db->newSelectQueryBuilder() |
105 | ->select( array_values( static::getFieldColumnMap() ) ) |
106 | ->from( static::getTable() ) |
107 | ->where( [ static::getIdColumn() => (int)$id ] ) |
108 | ->caller( __METHOD__ ); |
109 | if ( $flags & IDBAccessObject::READ_LOCKING ) { |
110 | $queryBuilder->forUpdate(); |
111 | } |
112 | $row = $queryBuilder->fetchRow(); |
113 | |
114 | if ( $row ) { |
115 | $class = static::getConsumerClass( (array)$row ); |
116 | $consumer = new $class(); |
117 | $consumer->loadFromRow( $db, $row ); |
118 | return $consumer; |
119 | } else { |
120 | return false; |
121 | } |
122 | } |
123 | |
124 | /** |
125 | * Get the value of a field |
126 | * |
127 | * @param string $name |
128 | * @return mixed |
129 | * @throws LogicException |
130 | */ |
131 | final public function get( $name ) { |
132 | if ( !static::hasField( $name ) ) { |
133 | throw new LogicException( "Object has no '$name' field." ); |
134 | } |
135 | return $this->$name; |
136 | } |
137 | |
138 | /** |
139 | * Set the value of a field |
140 | * |
141 | * @param string $name |
142 | * @param mixed $value |
143 | * @return mixed The old value |
144 | * @throws Exception |
145 | */ |
146 | final public function setField( $name, $value ) { |
147 | $old = $this->setFields( [ $name => $value ] ); |
148 | return $old[$name]; |
149 | } |
150 | |
151 | /** |
152 | * Set the values for a set of fields |
153 | * |
154 | * @param array $values (field => value) map |
155 | * @throws LogicException |
156 | * @return array Map of old values |
157 | */ |
158 | final public function setFields( array $values ) { |
159 | $old = []; |
160 | foreach ( $values as $name => $value ) { |
161 | if ( !static::hasField( $name ) ) { |
162 | throw new LogicException( "Object has no '$name' field." ); |
163 | } |
164 | $old[$name] = $this->$name; |
165 | $this->$name = $value; |
166 | if ( $old[$name] !== $value ) { |
167 | $this->daoPending = true; |
168 | } |
169 | } |
170 | $this->normalizeValues(); |
171 | return $old; |
172 | } |
173 | |
174 | /** |
175 | * @return string[] |
176 | */ |
177 | final public function getFieldNames() { |
178 | return array_keys( static::getFieldColumnMap() ); |
179 | } |
180 | |
181 | /** |
182 | * @param IDatabase $dbw |
183 | * @return bool |
184 | * @throws DBReadOnlyError |
185 | */ |
186 | public function save( IDatabase $dbw ) { |
187 | global $wgMWOAuthReadOnly; |
188 | |
189 | $uniqueId = $this->getIdValue(); |
190 | $idColumn = static::getIdColumn(); |
191 | if ( $wgMWOAuthReadOnly ) { |
192 | throw new DBReadOnlyError( $dbw, __CLASS__ . ": tried to save while db is read-only" ); |
193 | } |
194 | if ( $this->daoOrigin === 'db' ) { |
195 | if ( $this->daoPending ) { |
196 | $this->logger->debug( get_class( $this ) . ': performing DB update; object changed.' ); |
197 | $dbw->newUpdateQueryBuilder() |
198 | ->update( static::getTable() ) |
199 | ->set( $this->getRowArray( $dbw ) ) |
200 | ->where( [ $idColumn => $uniqueId ] ) |
201 | ->caller( __METHOD__ ) |
202 | ->execute(); |
203 | $this->daoPending = false; |
204 | return $dbw->affectedRows() > 0; |
205 | } else { |
206 | $this->logger->debug( get_class( $this ) . ': skipping DB update; object unchanged.' ); |
207 | return false; |
208 | } |
209 | } else { |
210 | $this->logger->debug( get_class( $this ) . ': performing DB update; new object.' ); |
211 | $afield = static::getAutoIncrField(); |
212 | $acolumn = $afield !== null ? static::getColumn( $afield ) : null; |
213 | $row = $this->getRowArray( $dbw ); |
214 | if ( $acolumn !== null && $row[$acolumn] === null ) { |
215 | // auto-increment field should be omitted, not set null, for |
216 | // auto-incrementing behavior |
217 | unset( $row[$acolumn] ); |
218 | } |
219 | $dbw->newInsertQueryBuilder() |
220 | ->insertInto( static::getTable() ) |
221 | ->row( $row ) |
222 | ->caller( __METHOD__ ) |
223 | ->execute(); |
224 | if ( $afield !== null ) { |
225 | // update field for auto-increment field |
226 | $this->$afield = $dbw->insertId(); |
227 | } |
228 | $this->daoPending = false; |
229 | return true; |
230 | } |
231 | } |
232 | |
233 | /** |
234 | * @param IDatabase $dbw |
235 | * @return bool |
236 | * @throws DBReadOnlyError |
237 | */ |
238 | public function delete( IDatabase $dbw ) { |
239 | global $wgMWOAuthReadOnly; |
240 | |
241 | $uniqueId = $this->getIdValue(); |
242 | $idColumn = static::getIdColumn(); |
243 | if ( $wgMWOAuthReadOnly ) { |
244 | throw new DBReadOnlyError( $dbw, __CLASS__ . ": tried to delete while db is read-only" ); |
245 | } |
246 | if ( $this->daoOrigin === 'db' ) { |
247 | $dbw->newDeleteQueryBuilder() |
248 | ->deleteFrom( static::getTable() ) |
249 | ->where( [ $idColumn => $uniqueId ] ) |
250 | ->caller( __METHOD__ ) |
251 | ->execute(); |
252 | $this->daoPending = true; |
253 | return $dbw->affectedRows() > 0; |
254 | } else { |
255 | return false; |
256 | } |
257 | } |
258 | |
259 | /** |
260 | * Get the schema information for this object type |
261 | * |
262 | * This should return an associative array with: |
263 | * - idField : a field with an int/hex UNIQUE identifier |
264 | * - autoIncrField : a field that auto-increments in the DB (or NULL if none) |
265 | * - table : a table name |
266 | * - fieldColumnMap : a map of field names to column names |
267 | * |
268 | * @return array |
269 | */ |
270 | protected static function getSchema() { |
271 | // Note: declaring this abstract raises E_STRICT |
272 | throw new MWException( "getSchema() not defined in " . self::class ); |
273 | } |
274 | |
275 | /** |
276 | * Get the access control check methods for this object type |
277 | * |
278 | * This returns a map of field names to method names. |
279 | * The methods check if a context user has access to the field, |
280 | * returning true if they do and a Message object otherwise. |
281 | * The methods take (field name, IContextSource) as arguments. |
282 | * |
283 | * @see MWOAuthDAO::userCanAccess() |
284 | * @see MWOAuthDAOAccessControl |
285 | * |
286 | * @throws LogicException Subclasses must override |
287 | * @return array<string,string> Map of (field name => name of method that checks access) |
288 | */ |
289 | protected static function getFieldPermissionChecks() { |
290 | // Note: declaring this abstract raises E_STRICT |
291 | throw new LogicException( "getFieldPermissionChecks() not defined in " . self::class ); |
292 | } |
293 | |
294 | /** |
295 | * @return string |
296 | */ |
297 | final protected static function getTable() { |
298 | $schema = static::getSchema(); |
299 | return $schema['table']; |
300 | } |
301 | |
302 | /** |
303 | * @return array<string,string> |
304 | */ |
305 | final protected static function getFieldColumnMap() { |
306 | $schema = static::getSchema(); |
307 | return $schema['fieldColumnMap']; |
308 | } |
309 | |
310 | /** |
311 | * @param string $field |
312 | * @return string |
313 | */ |
314 | final protected static function getColumn( $field ) { |
315 | $schema = static::getSchema(); |
316 | return $schema['fieldColumnMap'][$field]; |
317 | } |
318 | |
319 | /** |
320 | * @param string $field |
321 | * @return bool |
322 | */ |
323 | final protected static function hasField( $field ) { |
324 | $schema = static::getSchema(); |
325 | return isset( $schema['fieldColumnMap'][$field] ); |
326 | } |
327 | |
328 | /** |
329 | * @return string|null |
330 | */ |
331 | final protected static function getAutoIncrField() { |
332 | $schema = static::getSchema(); |
333 | return $schema['autoIncrField'] ?? null; |
334 | } |
335 | |
336 | /** |
337 | * @return string |
338 | */ |
339 | final protected static function getIdColumn() { |
340 | $schema = static::getSchema(); |
341 | return $schema['fieldColumnMap'][$schema['idField']]; |
342 | } |
343 | |
344 | /** |
345 | * @return int|string |
346 | */ |
347 | final protected function getIdValue() { |
348 | $schema = static::getSchema(); |
349 | $field = $schema['idField']; |
350 | return $this->$field; |
351 | } |
352 | |
353 | /** |
354 | * @param array $values |
355 | */ |
356 | final protected function loadFromValues( array $values ) { |
357 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
358 | if ( !array_key_exists( $field, $values ) ) { |
359 | throw new MWException( get_class( $this ) . " requires '$field' field." ); |
360 | } |
361 | $this->$field = $values[$field]; |
362 | } |
363 | $this->normalizeValues(); |
364 | $this->daoOrigin = 'new'; |
365 | $this->daoPending = true; |
366 | } |
367 | |
368 | /** |
369 | * Subclasses should make this normalize fields (e.g. timestamps) |
370 | * |
371 | * @return void |
372 | */ |
373 | abstract protected function normalizeValues(); |
374 | |
375 | /** |
376 | * @param IDatabase $db |
377 | * @param stdClass|array $row |
378 | * @return void |
379 | */ |
380 | final protected function loadFromRow( IDatabase $db, $row ) { |
381 | $row = $this->decodeRow( $db, (array)$row ); |
382 | $values = []; |
383 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
384 | $values[$field] = $row[$column]; |
385 | } |
386 | $this->loadFromValues( $values ); |
387 | $this->daoOrigin = 'db'; |
388 | $this->daoPending = false; |
389 | } |
390 | |
391 | /** |
392 | * Subclasses should make this to encode DB fields (e.g. timestamps). |
393 | * This must also flatten any PHP data structures into flat values. |
394 | * |
395 | * @param IDatabase $db |
396 | * @param array $row |
397 | * @return array |
398 | */ |
399 | abstract protected function encodeRow( IDatabase $db, $row ); |
400 | |
401 | /** |
402 | * Subclasses should make this to decode DB fields (e.g. timestamps). |
403 | * This can also expand some flat values (e.g. JSON) into PHP data structures. |
404 | * Note: this does not need to handle what normalizeValues() already does. |
405 | * |
406 | * @param IDatabase $db |
407 | * @param array $row |
408 | * @return array |
409 | */ |
410 | abstract protected function decodeRow( IDatabase $db, $row ); |
411 | |
412 | /** |
413 | * @param IDatabase $db |
414 | * @return array |
415 | */ |
416 | final protected function getRowArray( IDatabase $db ) { |
417 | $row = []; |
418 | foreach ( static::getFieldColumnMap() as $field => $column ) { |
419 | $row[$column] = $this->$field; |
420 | } |
421 | return $this->encodeRow( $db, $row ); |
422 | } |
423 | |
424 | /** |
425 | * Check if a user (from the context) can view a field |
426 | * |
427 | * @see MWOAuthDAO::userCanAccess() |
428 | * @see MWOAuthDAOAccessControl |
429 | * |
430 | * @param string $name |
431 | * @param IContextSource $context |
432 | * @return Message|true Returns on success or a Message if the user lacks access |
433 | */ |
434 | final public function userCanAccess( $name, IContextSource $context ) { |
435 | $map = static::getFieldPermissionChecks(); |
436 | if ( isset( $map[$name] ) ) { |
437 | $method = $map[$name]; |
438 | return $this->$method( $name, $context ); |
439 | } else { |
440 | return true; |
441 | } |
442 | } |
443 | |
444 | /** |
445 | * Get the current conflict token value for a user |
446 | * |
447 | * @param IContextSource $context |
448 | * @return string Hex token |
449 | */ |
450 | final public function getChangeToken( IContextSource $context ) { |
451 | $map = []; |
452 | foreach ( $this->getFieldNames() as $field ) { |
453 | if ( $this->userCanAccess( $field, $context ) ) { |
454 | $map[$field] = $this->$field; |
455 | } else { |
456 | // don't convey this information |
457 | $map[$field] = null; |
458 | } |
459 | } |
460 | return hash_hmac( |
461 | 'sha1', |
462 | serialize( $map ), |
463 | $context->getUser()->getId() . '#' . $this->getIdValue() |
464 | ); |
465 | } |
466 | |
467 | /** |
468 | * Compare an old change token to the current one |
469 | * |
470 | * @param IContextSource $context |
471 | * @param string $oldToken |
472 | * @return bool Whether the current is unchanged |
473 | */ |
474 | final public function checkChangeToken( IContextSource $context, $oldToken ) { |
475 | return ( $this->getChangeToken( $context ) === $oldToken ); |
476 | } |
477 | |
478 | /** |
479 | * Update whether this object should be written to the data store |
480 | * @param bool $pending set to true to mark this object as needing to write its data |
481 | */ |
482 | public function setPending( $pending ) { |
483 | $this->daoPending = $pending; |
484 | } |
485 | |
486 | /** |
487 | * Update the origin of this object |
488 | * @param string $source source of the object |
489 | * 'new': Treat this as a new object to the datastore (insert on save) |
490 | * 'db': Treat this as already in the datastore (update on save) |
491 | */ |
492 | public function updateOrigin( $source ) { |
493 | $this->daoOrigin = $source; |
494 | } |
495 | } |