MediaWiki  master
ActorMigrationBase.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\User;
24 
25 use InvalidArgumentException;
26 use ReflectionClass;
27 use 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'] ) ) {
350  wfDeprecated(
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->newInsertQueryBuilder()
384  ->insertInto( $tempTableInfo['table'] )
385  ->row( [ $tempTableInfo['pk'] => $pk ] + $set )
386  ->onDuplicateKeyUpdate()
387  ->uniqueIndexFields( [ $tempTableInfo['pk'] ] )
388  ->set( $set )
389  ->caller( $func )->execute();
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 
526 class_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 LIST_OR
Definition: Defines.php:46
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:267
const SCHEMA_COMPAT_WRITE_MASK
Definition: Defines.php:269
const LIST_AND
Definition: Defines.php:43
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.
Definition: ActorCache.php:21