MediaWiki 1.41.2
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
48 private ActorStoreFactory $actorStoreFactory;
49
51 private $fieldInfos;
52
53 private bool $allowUnknown;
54
98 public function __construct(
99 $fieldInfos,
100 $stage,
101 ActorStoreFactory $actorStoreFactory,
102 $options = []
103 ) {
104 $this->fieldInfos = $fieldInfos;
105 $this->allowUnknown = $options['allowUnknown'] ?? true;
106
107 $writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK;
108 $readStage = $stage & SCHEMA_COMPAT_READ_MASK;
109 if ( $writeStage === 0 ) {
110 throw new InvalidArgumentException( '$stage must include a write mode' );
111 }
112 if ( $readStage === 0 ) {
113 throw new InvalidArgumentException( '$stage must include a read mode' );
114 }
115 if ( !in_array(
117 ) ) {
118 throw new InvalidArgumentException( 'Cannot read multiple schemas' );
119 }
120 if ( $readStage === SCHEMA_COMPAT_READ_OLD && !( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
121 throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
122 }
123 if ( $readStage === SCHEMA_COMPAT_READ_TEMP && !( $writeStage & SCHEMA_COMPAT_WRITE_TEMP ) ) {
124 throw new InvalidArgumentException( 'Cannot read the temp schema without also writing it' );
125 }
126 if ( $readStage === SCHEMA_COMPAT_READ_NEW && !( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
127 throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
128 }
129 $this->readStage = $readStage;
130 $this->writeStage = $writeStage;
131
132 $this->actorStoreFactory = $actorStoreFactory;
133 }
134
143 protected function getFieldInfo( $key ) {
144 if ( isset( $this->fieldInfos[$key] ) ) {
145 return $this->fieldInfos[$key];
146 } elseif ( $this->allowUnknown ) {
147 return [];
148 } else {
149 throw new InvalidArgumentException( $this->getInstanceName() . ": unknown key $key" );
150 }
151 }
152
161 protected function getInstanceName() {
162 if ( ( new ReflectionClass( $this ) )->isAnonymous() ) {
163 // Mostly for PHPUnit
164 return self::class;
165 } else {
166 return static::class;
167 }
168 }
169
177 protected function checkDeprecation( $key ) {
178 $fieldInfo = $this->getFieldInfo( $key );
179 if ( isset( $fieldInfo['removedVersion'] ) ) {
180 $removedVersion = $fieldInfo['removedVersion'];
181 $component = $fieldInfo['component'] ?? 'MediaWiki';
182 throw new InvalidArgumentException(
183 "Use of {$this->getInstanceName()} for '$key' was removed in $component $removedVersion"
184 );
185 }
186 if ( isset( $fieldInfo['deprecatedVersion'] ) ) {
187 $deprecatedVersion = $fieldInfo['deprecatedVersion'];
188 $component = $fieldInfo['component'] ?? 'MediaWiki';
189 wfDeprecated( "{$this->getInstanceName()} for '$key'", $deprecatedVersion, $component, 3 );
190 }
191 }
192
198 public function isAnon( $field ) {
199 return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NULL" : "$field = 0";
200 }
201
207 public function isNotAnon( $field ) {
208 return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NOT NULL" : "$field != 0";
209 }
210
216 private function getFieldNames( $key ) {
217 $fieldInfo = $this->getFieldInfo( $key );
218 $textField = $fieldInfo['textField'] ?? $key . '_text';
219 $actorField = $fieldInfo['actorField'] ?? substr( $key, 0, -5 ) . '_actor';
220 return [ $textField, $actorField ];
221 }
222
229 private function getTempTableInfo( $key ) {
230 $fieldInfo = $this->getFieldInfo( $key );
231 return $fieldInfo['tempTable'] ?? null;
232 }
233
246 public function getJoin( $key ) {
247 $this->checkDeprecation( $key );
248
249 if ( !isset( $this->joinCache[$key] ) ) {
250 $tables = [];
251 $fields = [];
252 $joins = [];
253
254 [ $text, $actor ] = $this->getFieldNames( $key );
255
256 if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
257 $fields[$key] = $key;
258 $fields[$text] = $text;
259 $fields[$actor] = 'NULL';
260 } elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
261 $tempTableInfo = $this->getTempTableInfo( $key );
262 if ( $tempTableInfo ) {
263 $alias = "temp_$key";
264 $tables[$alias] = $tempTableInfo['table'];
265 $joins[$alias] = [
266 'JOIN',
267 "{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}",
268 ];
269 $joinField = "{$alias}.{$tempTableInfo['field']}";
270 } else {
271 $joinField = $actor;
272 }
273
274 $alias = "actor_$key";
275 $tables[$alias] = 'actor';
276 $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$joinField}" ];
277
278 $fields[$key] = "{$alias}.actor_user";
279 $fields[$text] = "{$alias}.actor_name";
280 $fields[$actor] = $joinField;
281 } else /* SCHEMA_COMPAT_READ_NEW */ {
282 $alias = "actor_$key";
283 $tables[$alias] = 'actor';
284 $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ];
285
286 $fields[$key] = "{$alias}.actor_user";
287 $fields[$text] = "{$alias}.actor_name";
288 $fields[$actor] = $actor;
289 }
290
291 $this->joinCache[$key] = [
292 'tables' => $tables,
293 'fields' => $fields,
294 'joins' => $joins,
295 ];
296 }
297
298 return $this->joinCache[$key];
299 }
300
310 public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) {
311 $this->checkDeprecation( $key );
312
313 if ( $this->getTempTableInfo( $key ) ) {
314 throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" );
315 }
316
317 [ $text, $actor ] = $this->getFieldNames( $key );
318 $ret = [];
319 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
320 $ret[$key] = $user->getId();
321 $ret[$text] = $user->getName();
322 }
323 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP || $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
324 $ret[$actor] =
325 $this->actorStoreFactory->getActorNormalization( $dbw->getDomainID() )->acquireActorId( $user, $dbw );
326 }
327 return $ret;
328 }
329
342 public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) {
343 $this->checkDeprecation( $key );
344
345 $fieldInfo = $this->getFieldInfo( $key );
346 $tempTableInfo = $fieldInfo['tempTable'] ?? null;
347 if ( isset( $fieldInfo['formerTempTableVersion'] ) ) {
349 __METHOD__ . " for $key",
350 $fieldInfo['formerTempTableVersion'],
351 $fieldInfo['component'] ?? 'MediaWiki'
352 );
353 } elseif ( !$tempTableInfo ) {
354 throw new InvalidArgumentException( "Must use getInsertValues() for $key" );
355 }
356
357 [ $text, $actor ] = $this->getFieldNames( $key );
358 $ret = [];
359 $callback = null;
360
361 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
362 $ret[$key] = $user->getId();
363 $ret[$text] = $user->getName();
364 }
365 if ( $this->writeStage & ( SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW ) ) {
366 $id = $this->actorStoreFactory
367 ->getActorNormalization( $dbw->getDomainID() )
368 ->acquireActorId( $user, $dbw );
369
370 if ( $tempTableInfo ) {
371 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP ) {
372 $func = __METHOD__;
373 $callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $dbw, $id, $func ) {
374 $set = [ $tempTableInfo['field'] => $id ];
375 foreach ( $tempTableInfo['extra'] as $to => $from ) {
376 if ( !array_key_exists( $from, $extra ) ) {
377 throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
378 }
379 $set[$to] = $extra[$from];
380 }
382 ->insertInto( $tempTableInfo['table'] )
383 ->row( [ $tempTableInfo['pk'] => $pk ] + $set )
384 ->onDuplicateKeyUpdate()
385 ->uniqueIndexFields( [ $tempTableInfo['pk'] ] )
386 ->set( $set )
387 ->caller( $func )->execute();
388 };
389 }
390 if ( $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
391 $ret[$actor] = $id;
392 }
393 } else {
394 $ret[$actor] = $id;
395 }
396 }
397
398 if ( $callback === null ) {
399 // Make a validation-only callback if there was temp table info
400 if ( $tempTableInfo ) {
401 $func = __METHOD__;
402 $callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $func ) {
403 foreach ( $tempTableInfo['extra'] as $from ) {
404 if ( !array_key_exists( $from, $extra ) ) {
405 throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
406 }
407 }
408 };
409 } else {
410 $callback = static function ( $pk, array $extra ) {
411 };
412 }
413 }
414 return [ $ret, $callback ];
415 }
416
441 public function getWhere( IReadableDatabase $db, $key, $users, $useId = true ) {
442 $this->checkDeprecation( $key );
443
444 $tables = [];
445 $conds = [];
446 $joins = [];
447
448 if ( $users instanceof UserIdentity ) {
449 $users = [ $users ];
450 } elseif ( $users === null || $users === false ) {
451 // DWIM
452 $users = [];
453 } elseif ( !is_array( $users ) ) {
454 $what = is_object( $users ) ? get_class( $users ) : gettype( $users );
455 throw new InvalidArgumentException(
456 __METHOD__ . ": Value for \$users must be a UserIdentity or array, got $what"
457 );
458 }
459
460 // Get information about all the passed users
461 $ids = [];
462 $names = [];
463 $actors = [];
464 foreach ( $users as $user ) {
465 if ( $useId && $user->isRegistered() ) {
466 $ids[] = $user->getId();
467 } else {
468 // make sure to use normalized form of IP for anonymous users
469 $names[] = IPUtils::sanitizeIP( $user->getName() );
470 }
471 $actorId = $this->actorStoreFactory
472 ->getActorNormalization( $db->getDomainID() )
473 ->findActorId( $user, $db );
474
475 if ( $actorId ) {
476 $actors[] = $actorId;
477 }
478 }
479
480 [ $text, $actor ] = $this->getFieldNames( $key );
481
482 // Combine data into conditions to be ORed together
483 if ( $this->readStage === SCHEMA_COMPAT_READ_NEW ) {
484 if ( $actors ) {
485 $conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase::LIST_AND );
486 }
487 } elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
488 if ( $actors ) {
489 $tempTableInfo = $this->getTempTableInfo( $key );
490 if ( $tempTableInfo ) {
491 $alias = "temp_$key";
492 $tables[$alias] = $tempTableInfo['table'];
493 $joins[$alias] = [
494 'JOIN',
495 "{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}",
496 ];
497 $joinField = "{$alias}.{$tempTableInfo['field']}";
498 } else {
499 $joinField = $actor;
500 }
501 $conds['actor'] = $db->makeList( [ $joinField => $actors ], IDatabase::LIST_AND );
502 }
503 } else {
504 if ( $ids ) {
505 $conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase::LIST_AND );
506 }
507 if ( $names ) {
508 $conds['username'] = $db->makeList( [ $text => $names ], IDatabase::LIST_AND );
509 }
510 }
511
512 return [
513 'tables' => $tables,
514 'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0',
515 'orconds' => $conds,
516 'joins' => $joins,
517 ];
518 }
519}
520
524class_alias( ActorMigrationBase::class, 'ActorMigrationBase' );
const SCHEMA_COMPAT_WRITE_TEMP
Definition Defines.php:265
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:268
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:263
const SCHEMA_COMPAT_READ_TEMP
Definition Defines.php:266
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:264
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:267
const SCHEMA_COMPAT_WRITE_MASK
Definition Defines.php:269
const SCHEMA_COMPAT_READ_MASK
Definition Defines.php:270
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
newInsertQueryBuilder()
Get an InsertQueryBuilder bound to this connection.
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.
Utility class for bot passwords.