MediaWiki REL1_40
ActorMigrationBase.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\User;
24
25use InvalidArgumentException;
26use ReflectionClass;
27use Wikimedia\IPUtils;
30
40 private $joinCache = [];
41
43 private $readStage;
44
46 private $writeStage;
47
49 private $actorStoreFactory;
50
52 private $fieldInfos;
53
55 private $allowUnknown;
56
100 public function __construct(
101 $fieldInfos,
102 $stage,
103 ActorStoreFactory $actorStoreFactory,
104 $options = []
105 ) {
106 $this->fieldInfos = $fieldInfos;
107 $this->allowUnknown = $options['allowUnknown'] ?? true;
108
109 $writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK;
110 $readStage = $stage & SCHEMA_COMPAT_READ_MASK;
111 if ( $writeStage === 0 ) {
112 throw new InvalidArgumentException( '$stage must include a write mode' );
113 }
114 if ( $readStage === 0 ) {
115 throw new InvalidArgumentException( '$stage must include a read mode' );
116 }
117 if ( !in_array(
119 ) ) {
120 throw new InvalidArgumentException( 'Cannot read multiple schemas' );
121 }
122 if ( $readStage === SCHEMA_COMPAT_READ_OLD && !( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
123 throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
124 }
125 if ( $readStage === SCHEMA_COMPAT_READ_TEMP && !( $writeStage & SCHEMA_COMPAT_WRITE_TEMP ) ) {
126 throw new InvalidArgumentException( 'Cannot read the temp schema without also writing it' );
127 }
128 if ( $readStage === SCHEMA_COMPAT_READ_NEW && !( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
129 throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
130 }
131 $this->readStage = $readStage;
132 $this->writeStage = $writeStage;
133
134 $this->actorStoreFactory = $actorStoreFactory;
135 }
136
145 protected function getFieldInfo( $key ) {
146 if ( isset( $this->fieldInfos[$key] ) ) {
147 return $this->fieldInfos[$key];
148 } elseif ( $this->allowUnknown ) {
149 return [];
150 } else {
151 throw new InvalidArgumentException( $this->getInstanceName() . ": unknown key $key" );
152 }
153 }
154
163 protected function getInstanceName() {
164 if ( ( new ReflectionClass( $this ) )->isAnonymous() ) {
165 // Mostly for PHPUnit
166 return self::class;
167 } else {
168 return static::class;
169 }
170 }
171
179 protected function checkDeprecation( $key ) {
180 $fieldInfo = $this->getFieldInfo( $key );
181 if ( isset( $fieldInfo['removedVersion'] ) ) {
182 $removedVersion = $fieldInfo['removedVersion'];
183 $component = $fieldInfo['component'] ?? 'MediaWiki';
184 throw new InvalidArgumentException(
185 "Use of {$this->getInstanceName()} for '$key' was removed in $component $removedVersion"
186 );
187 }
188 if ( isset( $fieldInfo['deprecatedVersion'] ) ) {
189 $deprecatedVersion = $fieldInfo['deprecatedVersion'];
190 $component = $fieldInfo['component'] ?? 'MediaWiki';
191 wfDeprecated( "{$this->getInstanceName()} for '$key'", $deprecatedVersion, $component, 3 );
192 }
193 }
194
200 public function isAnon( $field ) {
201 return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NULL" : "$field = 0";
202 }
203
209 public function isNotAnon( $field ) {
210 return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NOT NULL" : "$field != 0";
211 }
212
218 private function getFieldNames( $key ) {
219 $fieldInfo = $this->getFieldInfo( $key );
220 $textField = $fieldInfo['textField'] ?? $key . '_text';
221 $actorField = $fieldInfo['actorField'] ?? substr( $key, 0, -5 ) . '_actor';
222 return [ $textField, $actorField ];
223 }
224
231 private function getTempTableInfo( $key ) {
232 $fieldInfo = $this->getFieldInfo( $key );
233 return $fieldInfo['tempTable'] ?? null;
234 }
235
248 public function getJoin( $key ) {
249 $this->checkDeprecation( $key );
250
251 if ( !isset( $this->joinCache[$key] ) ) {
252 $tables = [];
253 $fields = [];
254 $joins = [];
255
256 [ $text, $actor ] = $this->getFieldNames( $key );
257
258 if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
259 $fields[$key] = $key;
260 $fields[$text] = $text;
261 $fields[$actor] = 'NULL';
262 } elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
263 $tempTableInfo = $this->getTempTableInfo( $key );
264 if ( $tempTableInfo ) {
265 $alias = "temp_$key";
266 $tables[$alias] = $tempTableInfo['table'];
267 $joins[$alias] = [
268 'JOIN',
269 "{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}",
270 ];
271 $joinField = "{$alias}.{$tempTableInfo['field']}";
272 } else {
273 $joinField = $actor;
274 }
275
276 $alias = "actor_$key";
277 $tables[$alias] = 'actor';
278 $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$joinField}" ];
279
280 $fields[$key] = "{$alias}.actor_user";
281 $fields[$text] = "{$alias}.actor_name";
282 $fields[$actor] = $joinField;
283 } else /* SCHEMA_COMPAT_READ_NEW */ {
284 $alias = "actor_$key";
285 $tables[$alias] = 'actor';
286 $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ];
287
288 $fields[$key] = "{$alias}.actor_user";
289 $fields[$text] = "{$alias}.actor_name";
290 $fields[$actor] = $actor;
291 }
292
293 $this->joinCache[$key] = [
294 'tables' => $tables,
295 'fields' => $fields,
296 'joins' => $joins,
297 ];
298 }
299
300 return $this->joinCache[$key];
301 }
302
312 public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) {
313 $this->checkDeprecation( $key );
314
315 if ( $this->getTempTableInfo( $key ) ) {
316 throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" );
317 }
318
319 [ $text, $actor ] = $this->getFieldNames( $key );
320 $ret = [];
321 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
322 $ret[$key] = $user->getId();
323 $ret[$text] = $user->getName();
324 }
325 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP || $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
326 $ret[$actor] =
327 $this->actorStoreFactory->getActorNormalization( $dbw->getDomainID() )->acquireActorId( $user, $dbw );
328 }
329 return $ret;
330 }
331
344 public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) {
345 $this->checkDeprecation( $key );
346
347 $fieldInfo = $this->getFieldInfo( $key );
348 $tempTableInfo = $fieldInfo['tempTable'] ?? null;
349 if ( isset( $fieldInfo['formerTempTableVersion'] ) ) {
351 __METHOD__ . " for $key",
352 $fieldInfo['formerTempTableVersion'],
353 $fieldInfo['component'] ?? 'MediaWiki'
354 );
355 } elseif ( !$tempTableInfo ) {
356 throw new InvalidArgumentException( "Must use getInsertValues() for $key" );
357 }
358
359 [ $text, $actor ] = $this->getFieldNames( $key );
360 $ret = [];
361 $callback = null;
362
363 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
364 $ret[$key] = $user->getId();
365 $ret[$text] = $user->getName();
366 }
367 if ( $this->writeStage & ( SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW ) ) {
368 $id = $this->actorStoreFactory
369 ->getActorNormalization( $dbw->getDomainID() )
370 ->acquireActorId( $user, $dbw );
371
372 if ( $tempTableInfo ) {
373 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP ) {
374 $func = __METHOD__;
375 $callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $dbw, $id, $func ) {
376 $set = [ $tempTableInfo['field'] => $id ];
377 foreach ( $tempTableInfo['extra'] as $to => $from ) {
378 if ( !array_key_exists( $from, $extra ) ) {
379 throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
380 }
381 $set[$to] = $extra[$from];
382 }
383 $dbw->upsert(
384 $tempTableInfo['table'],
385 [ $tempTableInfo['pk'] => $pk ] + $set,
386 [ [ $tempTableInfo['pk'] ] ],
387 $set,
388 $func
389 );
390 };
391 }
392 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
393 $ret[$actor] = $id;
394 }
395 } else {
396 $ret[$actor] = $id;
397 }
398 }
399
400 if ( $callback === null ) {
401 // Make a validation-only callback if there was temp table info
402 if ( $tempTableInfo ) {
403 $func = __METHOD__;
404 $callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $func ) {
405 foreach ( $tempTableInfo['extra'] as $from ) {
406 if ( !array_key_exists( $from, $extra ) ) {
407 throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
408 }
409 }
410 };
411 } else {
412 $callback = static function ( $pk, array $extra ) {
413 };
414 }
415 }
416 return [ $ret, $callback ];
417 }
418
443 public function getWhere( IReadableDatabase $db, $key, $users, $useId = true ) {
444 $this->checkDeprecation( $key );
445
446 $tables = [];
447 $conds = [];
448 $joins = [];
449
450 if ( $users instanceof UserIdentity ) {
451 $users = [ $users ];
452 } elseif ( $users === null || $users === false ) {
453 // DWIM
454 $users = [];
455 } elseif ( !is_array( $users ) ) {
456 $what = is_object( $users ) ? get_class( $users ) : gettype( $users );
457 throw new InvalidArgumentException(
458 __METHOD__ . ": Value for \$users must be a UserIdentity or array, got $what"
459 );
460 }
461
462 // Get information about all the passed users
463 $ids = [];
464 $names = [];
465 $actors = [];
466 foreach ( $users as $user ) {
467 if ( $useId && $user->isRegistered() ) {
468 $ids[] = $user->getId();
469 } else {
470 // make sure to use normalized form of IP for anonymous users
471 $names[] = IPUtils::sanitizeIP( $user->getName() );
472 }
473 $actorId = $this->actorStoreFactory
474 ->getActorNormalization( $db->getDomainID() )
475 ->findActorId( $user, $db );
476
477 if ( $actorId ) {
478 $actors[] = $actorId;
479 }
480 }
481
482 [ $text, $actor ] = $this->getFieldNames( $key );
483
484 // Combine data into conditions to be ORed together
485 if ( $this->readStage === SCHEMA_COMPAT_READ_NEW ) {
486 if ( $actors ) {
487 $conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase::LIST_AND );
488 }
489 } elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
490 if ( $actors ) {
491 $tempTableInfo = $this->getTempTableInfo( $key );
492 if ( $tempTableInfo ) {
493 $alias = "temp_$key";
494 $tables[$alias] = $tempTableInfo['table'];
495 $joins[$alias] = [
496 'JOIN',
497 "{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}",
498 ];
499 $joinField = "{$alias}.{$tempTableInfo['field']}";
500 } else {
501 $joinField = $actor;
502 }
503 $conds['actor'] = $db->makeList( [ $joinField => $actors ], IDatabase::LIST_AND );
504 }
505 } else {
506 if ( $ids ) {
507 $conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase::LIST_AND );
508 }
509 if ( $names ) {
510 $conds['username'] = $db->makeList( [ $text => $names ], IDatabase::LIST_AND );
511 }
512 }
513
514 return [
515 'tables' => $tables,
516 'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0',
517 'orconds' => $conds,
518 'joins' => $joins,
519 ];
520 }
521}
522
523class_alias( ActorMigrationBase::class, 'ActorMigrationBase' );
const SCHEMA_COMPAT_WRITE_TEMP
Definition Defines.php:267
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:270
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:265
const SCHEMA_COMPAT_READ_TEMP
Definition Defines.php:268
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:266
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:269
const SCHEMA_COMPAT_WRITE_MASK
Definition Defines.php:271
const SCHEMA_COMPAT_READ_MASK
Definition Defines.php:272
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
This abstract base class helps migrate core and extension code to use the actor table.
getJoin( $key)
Get SELECT fields and joins for the actor key.
getInstanceName()
Get a name for this instance to use in error messages.
__construct( $fieldInfos, $stage, ActorStoreFactory $actorStoreFactory, $options=[])
getWhere(IReadableDatabase $db, $key, $users, $useId=true)
Get WHERE condition for the actor.
getFieldInfo( $key)
Get config information about a field.
isNotAnon( $field)
Return an SQL condition to test if a user field is non-anonymous.
getInsertValues(IDatabase $dbw, $key, UserIdentity $user)
Get UPDATE fields for the actor.
getInsertValuesWithTempTable(IDatabase $dbw, $key, UserIdentity $user)
Get UPDATE fields for the actor.
isAnon( $field)
Return an SQL condition to test if a user field is anonymous.
checkDeprecation( $key)
Issue deprecation warning/error as appropriate.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:36
upsert( $table, array $rows, $uniqueKeys, array $set, $fname=__METHOD__)
Upsert row(s) into a table, in the provided order, while updating conflicting rows.
A database connection without write operations.
getDomainID()
Return the currently selected domain ID.
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.