3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\User
;
23 use InvalidArgumentException
;
26 use Wikimedia\IPUtils
;
27 use Wikimedia\Rdbms\IDatabase
;
28 use Wikimedia\Rdbms\IReadableDatabase
;
31 * Help migrate core and extension code with the actor table migration.
36 class ActorMigrationBase
{
37 /** @var array[] Cache for `self::getJoin()` */
38 private $joinCache = [];
40 /** @var int One of the SCHEMA_COMPAT_READ_* values */
43 /** @var int A combination of the SCHEMA_COMPAT_WRITE_* flags */
46 protected ActorStoreFactory
$actorStoreFactory;
51 private bool $allowUnknown;
53 private bool $forImport = false;
56 * @param array $fieldInfos An array of associative arrays, giving configuration
57 * information about fields which are being migrated. Subkeys are:
58 * - removedVersion: The version in which the field was removed
59 * - deprecatedVersion: The version in which the field was deprecated
60 * - component: The component for removedVersion and deprecatedVersion.
62 * - textField: Override the old text field name. Default {$key}_text.
63 * - actorField: Override the actor field name. Default {$key}_actor.
64 * All subkeys are optional.
69 * @param int $stage The migration stage. This is a combination of
70 * SCHEMA_COMPAT_* flags:
71 * - SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_WRITE_OLD: Use the old schema,
72 * with *_user and *_user_text fields.
73 * - SCHEMA_COMPAT_READ_NEW, SCHEMA_COMPAT_WRITE_NEW: Use the new
74 * schema. All relevant tables join directly to the actor table.
76 * @param ActorStoreFactory $actorStoreFactory
77 * @param array $options Array of other options. May contain:
78 * - allowUnknown: Allow fields not present in $fieldInfos. True by default.
80 public function __construct(
83 ActorStoreFactory
$actorStoreFactory,
86 $this->fieldInfos
= $fieldInfos;
87 $this->allowUnknown
= $options['allowUnknown'] ??
true;
89 $writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK
;
90 $readStage = $stage & SCHEMA_COMPAT_READ_MASK
;
91 if ( $writeStage === 0 ) {
92 throw new InvalidArgumentException( '$stage must include a write mode' );
94 if ( $readStage === 0 ) {
95 throw new InvalidArgumentException( '$stage must include a read mode' );
97 if ( !in_array( $readStage, [ SCHEMA_COMPAT_READ_OLD
, SCHEMA_COMPAT_READ_NEW
] ) ) {
98 throw new InvalidArgumentException( 'Cannot read multiple schemas' );
100 if ( $readStage === SCHEMA_COMPAT_READ_OLD
&& !( $writeStage & SCHEMA_COMPAT_WRITE_OLD
) ) {
101 throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
103 if ( $readStage === SCHEMA_COMPAT_READ_NEW
&& !( $writeStage & SCHEMA_COMPAT_WRITE_NEW
) ) {
104 throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
106 $this->readStage
= $readStage;
107 $this->writeStage
= $writeStage;
109 $this->actorStoreFactory
= $actorStoreFactory;
113 * Get an instance that allows IP actor creation
116 public static function newMigrationForImport() {
117 throw new LogicException( __METHOD__
. " must be overridden" );
121 * Get config information about a field.
123 * @stable to override
128 protected function getFieldInfo( $key ) {
129 if ( isset( $this->fieldInfos
[$key] ) ) {
130 return $this->fieldInfos
[$key];
131 } elseif ( $this->allowUnknown
) {
134 throw new InvalidArgumentException( $this->getInstanceName() . ": unknown key $key" );
139 * Get a name for this instance to use in error messages
141 * @stable to override
144 * @throws \ReflectionException
146 protected function getInstanceName() {
147 if ( ( new ReflectionClass( $this ) )->isAnonymous() ) {
148 // Mostly for PHPUnit
151 return static::class;
156 * Issue deprecation warning/error as appropriate.
162 protected function checkDeprecation( $key ) {
163 $fieldInfo = $this->getFieldInfo( $key );
164 if ( isset( $fieldInfo['removedVersion'] ) ) {
165 $removedVersion = $fieldInfo['removedVersion'];
166 $component = $fieldInfo['component'] ??
'MediaWiki';
167 throw new InvalidArgumentException(
168 "Use of {$this->getInstanceName()} for '$key' was removed in $component $removedVersion"
171 if ( isset( $fieldInfo['deprecatedVersion'] ) ) {
172 $deprecatedVersion = $fieldInfo['deprecatedVersion'];
173 $component = $fieldInfo['component'] ??
'MediaWiki';
174 wfDeprecated( "{$this->getInstanceName()} for '$key'", $deprecatedVersion, $component, 3 );
179 * Return an SQL condition to test if a user field is anonymous
180 * @param string $field Field name or SQL fragment
183 public function isAnon( $field ) {
184 return ( $this->readStage
& SCHEMA_COMPAT_READ_NEW
) ?
"$field IS NULL" : "$field = 0";
188 * Return an SQL condition to test if a user field is non-anonymous
189 * @param string $field Field name or SQL fragment
192 public function isNotAnon( $field ) {
193 return ( $this->readStage
& SCHEMA_COMPAT_READ_NEW
) ?
"$field IS NOT NULL" : "$field != 0";
197 * @param string $key A key such as "rev_user" identifying the actor
198 * field being fetched.
199 * @return string[] [ $text, $actor ]
201 private function getFieldNames( $key ) {
202 $fieldInfo = $this->getFieldInfo( $key );
203 $textField = $fieldInfo['textField'] ??
$key . '_text';
204 $actorField = $fieldInfo['actorField'] ??
substr( $key, 0, -5 ) . '_actor';
205 return [ $textField, $actorField ];
209 * Get SELECT fields and joins for the actor key
211 * @param string $key A key such as "rev_user" identifying the actor
212 * field being fetched.
213 * @return array[] With three keys:
214 * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables`
215 * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields`
216 * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds`
217 * All tables, fields, and joins are aliased, so `+` is safe to use.
218 * @phan-return array{tables:string[],fields:string[],joins:array}
220 public function getJoin( $key ) {
221 $this->checkDeprecation( $key );
223 if ( !isset( $this->joinCache
[$key] ) ) {
228 [ $text, $actor ] = $this->getFieldNames( $key );
230 if ( $this->readStage
=== SCHEMA_COMPAT_READ_OLD
) {
231 $fields[$key] = $key;
232 $fields[$text] = $text;
233 $fields[$actor] = 'NULL';
234 } else /* SCHEMA_COMPAT_READ_NEW */ {
235 $alias = "actor_$key";
236 $tables[$alias] = 'actor';
237 $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ];
239 $fields[$key] = "{$alias}.actor_user";
240 $fields[$text] = "{$alias}.actor_name";
241 $fields[$actor] = $actor;
244 $this->joinCache
[$key] = [
251 return $this->joinCache
[$key];
255 * Get UPDATE fields for the actor
257 * @param IDatabase $dbw Database to use for creating an actor ID, if necessary
258 * @param string $key A key such as "rev_user" identifying the actor
259 * field being fetched.
260 * @param UserIdentity $user User to set in the update
261 * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
263 public function getInsertValues( IDatabase
$dbw, $key, UserIdentity
$user ) {
264 $this->checkDeprecation( $key );
266 [ $text, $actor ] = $this->getFieldNames( $key );
268 if ( $this->writeStage
& SCHEMA_COMPAT_WRITE_OLD
) {
269 $ret[$key] = $user->getId();
270 $ret[$text] = $user->getName();
272 if ( $this->writeStage
& SCHEMA_COMPAT_WRITE_NEW
) {
273 $ret[$actor] = $this->getActorNormalization( $dbw->getDomainID() )
274 ->acquireActorId( $user, $dbw );
280 * Get WHERE condition for the actor
282 * @param IReadableDatabase $db Database to use for quoting and list-making
283 * @param string $key A key such as "rev_user" identifying the actor
284 * field being fetched.
285 * @param UserIdentity|UserIdentity[]|null|false $users Users to test for.
286 * Passing null, false, or the empty array will return 'conds' that never match,
287 * and an empty array for 'orconds'.
288 * @param bool $useId If false, don't try to query by the user ID.
289 * Intended for use with rc_user since it has an index on
290 * (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp).
291 * @return array With four keys:
292 * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables`
293 * - conds: (string) to include in the `$cond` to `IDatabase->select()` or `SelectQueryBuilder::conds`
294 * - orconds: (string[]) array of alternatives in case a union of multiple
295 * queries would be more efficient than a query with OR. May have keys
296 * 'actor', 'userid', 'username'.
297 * Since 1.32, this is guaranteed to contain just one alternative if
298 * $users contains a single user.
299 * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds`
300 * All tables and joins are aliased, so `+` is safe to use.
301 * @phan-return array{tables:string[],conds:string,orconds:string[],joins:array}
303 public function getWhere( IReadableDatabase
$db, $key, $users, $useId = true ) {
304 $this->checkDeprecation( $key );
310 if ( $users instanceof UserIdentity
) {
312 } elseif ( $users === null ||
$users === false ) {
315 } elseif ( !is_array( $users ) ) {
316 $what = get_debug_type( $users );
317 throw new InvalidArgumentException(
318 __METHOD__
. ": Value for \$users must be a UserIdentity or array, got $what"
322 // Get information about all the passed users
326 foreach ( $users as $user ) {
327 if ( $useId && $user->isRegistered() ) {
328 $ids[] = $user->getId();
330 // make sure to use normalized form of IP for anonymous users
331 $names[] = IPUtils
::sanitizeIP( $user->getName() );
333 $actorId = $this->getActorNormalization( $db->getDomainID() )
334 ->findActorId( $user, $db );
337 $actors[] = $actorId;
341 [ $text, $actor ] = $this->getFieldNames( $key );
343 // Combine data into conditions to be ORed together
344 if ( $this->readStage
=== SCHEMA_COMPAT_READ_NEW
) {
346 $conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase
::LIST_AND
);
350 $conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase
::LIST_AND
);
353 $conds['username'] = $db->makeList( [ $text => $names ], IDatabase
::LIST_AND
);
359 'conds' => $conds ?
$db->makeList( array_values( $conds ), IDatabase
::LIST_OR
) : '1=0',
366 * @internal For use immediately after construction only
367 * @param bool $forImport
369 public function setForImport( bool $forImport ): void
{
370 $this->forImport
= $forImport;
374 * @param string $domainId
375 * @return ActorNormalization
377 protected function getActorNormalization( $domainId ): ActorNormalization
{
378 if ( $this->forImport
) {
379 return $this->actorStoreFactory
->getActorNormalizationForImport( $domainId );
381 return $this->actorStoreFactory
->getActorNormalization( $domainId );
386 /** @deprecated class alias since 1.40 */
387 class_alias( ActorMigrationBase
::class, 'ActorMigrationBase' );