Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.87% |
111 / 117 |
|
53.85% |
7 / 13 |
CRAP | |
0.00% |
0 / 1 |
| ActorMigrationBase | |
94.87% |
111 / 117 |
|
53.85% |
7 / 13 |
45.27 | |
0.00% |
0 / 1 |
| __construct | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
8.01 | |||
| newMigrationForImport | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getFieldInfo | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| getInstanceName | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| checkDeprecation | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| isAnon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| isNotAnon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getFieldNames | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getJoin | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
3 | |||
| getInsertValues | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
| getWhere | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
14 | |||
| setForImport | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getActorNormalization | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\User; |
| 8 | |
| 9 | use InvalidArgumentException; |
| 10 | use LogicException; |
| 11 | use ReflectionClass; |
| 12 | use Wikimedia\IPUtils; |
| 13 | use Wikimedia\Rdbms\IDatabase; |
| 14 | use Wikimedia\Rdbms\IReadableDatabase; |
| 15 | |
| 16 | /** |
| 17 | * Help migrate core and extension code with the actor table migration. |
| 18 | * |
| 19 | * @stable to extend |
| 20 | * @since 1.37 |
| 21 | */ |
| 22 | class ActorMigrationBase { |
| 23 | /** @var array[] Cache for `self::getJoin()` */ |
| 24 | private $joinCache = []; |
| 25 | |
| 26 | /** @var int One of the SCHEMA_COMPAT_READ_* values */ |
| 27 | private $readStage; |
| 28 | |
| 29 | /** @var int A combination of the SCHEMA_COMPAT_WRITE_* flags */ |
| 30 | private $writeStage; |
| 31 | |
| 32 | protected ActorStoreFactory $actorStoreFactory; |
| 33 | |
| 34 | /** @var array */ |
| 35 | private $fieldInfos; |
| 36 | |
| 37 | private bool $allowUnknown; |
| 38 | |
| 39 | private bool $forImport = false; |
| 40 | |
| 41 | /** |
| 42 | * @param array $fieldInfos An array of associative arrays, giving configuration |
| 43 | * information about fields which are being migrated. Subkeys are: |
| 44 | * - removedVersion: The version in which the field was removed |
| 45 | * - deprecatedVersion: The version in which the field was deprecated |
| 46 | * - component: The component for removedVersion and deprecatedVersion. |
| 47 | * Default: MediaWiki. |
| 48 | * - textField: Override the old text field name. Default {$key}_text. |
| 49 | * - actorField: Override the actor field name. Default {$key}_actor. |
| 50 | * All subkeys are optional. |
| 51 | * |
| 52 | * @stable to override |
| 53 | * @stable to call |
| 54 | * |
| 55 | * @param int $stage The migration stage. This is a combination of |
| 56 | * SCHEMA_COMPAT_* flags: |
| 57 | * - SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_WRITE_OLD: Use the old schema, |
| 58 | * with *_user and *_user_text fields. |
| 59 | * - SCHEMA_COMPAT_READ_NEW, SCHEMA_COMPAT_WRITE_NEW: Use the new |
| 60 | * schema. All relevant tables join directly to the actor table. |
| 61 | * |
| 62 | * @param ActorStoreFactory $actorStoreFactory |
| 63 | * @param array $options Array of other options. May contain: |
| 64 | * - allowUnknown: Allow fields not present in $fieldInfos. True by default. |
| 65 | */ |
| 66 | public function __construct( |
| 67 | $fieldInfos, |
| 68 | $stage, |
| 69 | ActorStoreFactory $actorStoreFactory, |
| 70 | $options = [] |
| 71 | ) { |
| 72 | $this->fieldInfos = $fieldInfos; |
| 73 | $this->allowUnknown = $options['allowUnknown'] ?? true; |
| 74 | |
| 75 | $writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK; |
| 76 | $readStage = $stage & SCHEMA_COMPAT_READ_MASK; |
| 77 | if ( $writeStage === 0 ) { |
| 78 | throw new InvalidArgumentException( '$stage must include a write mode' ); |
| 79 | } |
| 80 | if ( $readStage === 0 ) { |
| 81 | throw new InvalidArgumentException( '$stage must include a read mode' ); |
| 82 | } |
| 83 | if ( !in_array( $readStage, [ SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_READ_NEW ] ) ) { |
| 84 | throw new InvalidArgumentException( 'Cannot read multiple schemas' ); |
| 85 | } |
| 86 | if ( $readStage === SCHEMA_COMPAT_READ_OLD && !( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) ) { |
| 87 | throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' ); |
| 88 | } |
| 89 | if ( $readStage === SCHEMA_COMPAT_READ_NEW && !( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) ) { |
| 90 | throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' ); |
| 91 | } |
| 92 | $this->readStage = $readStage; |
| 93 | $this->writeStage = $writeStage; |
| 94 | |
| 95 | $this->actorStoreFactory = $actorStoreFactory; |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * Get an instance that allows IP actor creation |
| 100 | * @return self |
| 101 | */ |
| 102 | public static function newMigrationForImport() { |
| 103 | throw new LogicException( __METHOD__ . " must be overridden" ); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * Get config information about a field. |
| 108 | * |
| 109 | * @stable to override |
| 110 | * |
| 111 | * @param string $key |
| 112 | * @return array |
| 113 | */ |
| 114 | protected function getFieldInfo( $key ) { |
| 115 | if ( isset( $this->fieldInfos[$key] ) ) { |
| 116 | return $this->fieldInfos[$key]; |
| 117 | } elseif ( $this->allowUnknown ) { |
| 118 | return []; |
| 119 | } else { |
| 120 | throw new InvalidArgumentException( $this->getInstanceName() . ": unknown key $key" ); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Get a name for this instance to use in error messages |
| 126 | * |
| 127 | * @stable to override |
| 128 | * |
| 129 | * @return string |
| 130 | * @throws \ReflectionException |
| 131 | */ |
| 132 | protected function getInstanceName() { |
| 133 | if ( ( new ReflectionClass( $this ) )->isAnonymous() ) { |
| 134 | // Mostly for PHPUnit |
| 135 | return self::class; |
| 136 | } else { |
| 137 | return static::class; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Issue deprecation warning/error as appropriate. |
| 143 | * |
| 144 | * @internal |
| 145 | * |
| 146 | * @param string $key |
| 147 | */ |
| 148 | protected function checkDeprecation( $key ) { |
| 149 | $fieldInfo = $this->getFieldInfo( $key ); |
| 150 | if ( isset( $fieldInfo['removedVersion'] ) ) { |
| 151 | $removedVersion = $fieldInfo['removedVersion']; |
| 152 | $component = $fieldInfo['component'] ?? 'MediaWiki'; |
| 153 | throw new InvalidArgumentException( |
| 154 | "Use of {$this->getInstanceName()} for '$key' was removed in $component $removedVersion" |
| 155 | ); |
| 156 | } |
| 157 | if ( isset( $fieldInfo['deprecatedVersion'] ) ) { |
| 158 | $deprecatedVersion = $fieldInfo['deprecatedVersion']; |
| 159 | $component = $fieldInfo['component'] ?? 'MediaWiki'; |
| 160 | wfDeprecated( "{$this->getInstanceName()} for '$key'", $deprecatedVersion, $component, 3 ); |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Return an SQL condition to test if a user field is anonymous |
| 166 | * @param string $field Field name or SQL fragment |
| 167 | * @return string |
| 168 | */ |
| 169 | public function isAnon( $field ) { |
| 170 | return ( $this->readStage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NULL" : "$field = 0"; |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Return an SQL condition to test if a user field is non-anonymous |
| 175 | * @param string $field Field name or SQL fragment |
| 176 | * @return string |
| 177 | */ |
| 178 | public function isNotAnon( $field ) { |
| 179 | return ( $this->readStage & SCHEMA_COMPAT_READ_NEW ) ? "$field IS NOT NULL" : "$field != 0"; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * @param string $key A key such as "rev_user" identifying the actor |
| 184 | * field being fetched. |
| 185 | * @return string[] [ $text, $actor ] |
| 186 | */ |
| 187 | private function getFieldNames( $key ) { |
| 188 | $fieldInfo = $this->getFieldInfo( $key ); |
| 189 | $textField = $fieldInfo['textField'] ?? $key . '_text'; |
| 190 | $actorField = $fieldInfo['actorField'] ?? substr( $key, 0, -5 ) . '_actor'; |
| 191 | return [ $textField, $actorField ]; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Get SELECT fields and joins for the actor key |
| 196 | * |
| 197 | * @param string $key A key such as "rev_user" identifying the actor |
| 198 | * field being fetched. |
| 199 | * @return array[] With three keys: |
| 200 | * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` |
| 201 | * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` |
| 202 | * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` |
| 203 | * All tables, fields, and joins are aliased, so `+` is safe to use. |
| 204 | * @phan-return array{tables:string[],fields:string[],joins:array} |
| 205 | */ |
| 206 | public function getJoin( $key ) { |
| 207 | $this->checkDeprecation( $key ); |
| 208 | |
| 209 | if ( !isset( $this->joinCache[$key] ) ) { |
| 210 | $tables = []; |
| 211 | $fields = []; |
| 212 | $joins = []; |
| 213 | |
| 214 | [ $text, $actor ] = $this->getFieldNames( $key ); |
| 215 | |
| 216 | if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) { |
| 217 | $fields[$key] = $key; |
| 218 | $fields[$text] = $text; |
| 219 | $fields[$actor] = 'NULL'; |
| 220 | } else /* SCHEMA_COMPAT_READ_NEW */ { |
| 221 | $alias = "actor_$key"; |
| 222 | $tables[$alias] = 'actor'; |
| 223 | $joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ]; |
| 224 | |
| 225 | $fields[$key] = "{$alias}.actor_user"; |
| 226 | $fields[$text] = "{$alias}.actor_name"; |
| 227 | $fields[$actor] = $actor; |
| 228 | } |
| 229 | |
| 230 | $this->joinCache[$key] = [ |
| 231 | 'tables' => $tables, |
| 232 | 'fields' => $fields, |
| 233 | 'joins' => $joins, |
| 234 | ]; |
| 235 | } |
| 236 | |
| 237 | return $this->joinCache[$key]; |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | * Get UPDATE fields for the actor |
| 242 | * |
| 243 | * @param IDatabase $dbw Database to use for creating an actor ID, if necessary |
| 244 | * @param string $key A key such as "rev_user" identifying the actor |
| 245 | * field being fetched. |
| 246 | * @param UserIdentity $user User to set in the update |
| 247 | * @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()` |
| 248 | */ |
| 249 | public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) { |
| 250 | $this->checkDeprecation( $key ); |
| 251 | |
| 252 | [ $text, $actor ] = $this->getFieldNames( $key ); |
| 253 | $ret = []; |
| 254 | if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) { |
| 255 | $ret[$key] = $user->getId(); |
| 256 | $ret[$text] = $user->getName(); |
| 257 | } |
| 258 | if ( $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) { |
| 259 | $ret[$actor] = $this->getActorNormalization( $dbw->getDomainID() ) |
| 260 | ->acquireActorId( $user, $dbw ); |
| 261 | } |
| 262 | return $ret; |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Get WHERE condition for the actor |
| 267 | * |
| 268 | * @param IReadableDatabase $db Database to use for quoting and list-making |
| 269 | * @param string $key A key such as "rev_user" identifying the actor |
| 270 | * field being fetched. |
| 271 | * @param UserIdentity|UserIdentity[]|null|false $users Users to test for. |
| 272 | * Passing null, false, or the empty array will return 'conds' that never match, |
| 273 | * and an empty array for 'orconds'. |
| 274 | * @param bool $useId If false, don't try to query by the user ID. |
| 275 | * Intended for use with rc_user since it has an index on |
| 276 | * (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp). |
| 277 | * @return array With four keys: |
| 278 | * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` |
| 279 | * - conds: (string) to include in the `$cond` to `IDatabase->select()` or `SelectQueryBuilder::conds` |
| 280 | * - orconds: (string[]) array of alternatives in case a union of multiple |
| 281 | * queries would be more efficient than a query with OR. May have keys |
| 282 | * 'actor', 'userid', 'username'. |
| 283 | * Since 1.32, this is guaranteed to contain just one alternative if |
| 284 | * $users contains a single user. |
| 285 | * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` |
| 286 | * All tables and joins are aliased, so `+` is safe to use. |
| 287 | * @phan-return array{tables:string[],conds:string,orconds:string[],joins:array} |
| 288 | */ |
| 289 | public function getWhere( IReadableDatabase $db, $key, $users, $useId = true ) { |
| 290 | $this->checkDeprecation( $key ); |
| 291 | |
| 292 | $tables = []; |
| 293 | $conds = []; |
| 294 | $joins = []; |
| 295 | |
| 296 | if ( $users instanceof UserIdentity ) { |
| 297 | $users = [ $users ]; |
| 298 | } elseif ( $users === null || $users === false ) { |
| 299 | // DWIM |
| 300 | $users = []; |
| 301 | } elseif ( !is_array( $users ) ) { |
| 302 | $what = get_debug_type( $users ); |
| 303 | throw new InvalidArgumentException( |
| 304 | __METHOD__ . ": Value for \$users must be a UserIdentity or array, got $what" |
| 305 | ); |
| 306 | } |
| 307 | |
| 308 | // Get information about all the passed users |
| 309 | $ids = []; |
| 310 | $names = []; |
| 311 | $actors = []; |
| 312 | foreach ( $users as $user ) { |
| 313 | if ( $useId && $user->isRegistered() ) { |
| 314 | $ids[] = $user->getId(); |
| 315 | } else { |
| 316 | // make sure to use normalized form of IP for anonymous users |
| 317 | $names[] = IPUtils::sanitizeIP( $user->getName() ); |
| 318 | } |
| 319 | $actorId = $this->getActorNormalization( $db->getDomainID() ) |
| 320 | ->findActorId( $user, $db ); |
| 321 | |
| 322 | if ( $actorId ) { |
| 323 | $actors[] = $actorId; |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | [ $text, $actor ] = $this->getFieldNames( $key ); |
| 328 | |
| 329 | // Combine data into conditions to be ORed together |
| 330 | if ( $this->readStage === SCHEMA_COMPAT_READ_NEW ) { |
| 331 | if ( $actors ) { |
| 332 | $conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase::LIST_AND ); |
| 333 | } |
| 334 | } else { |
| 335 | if ( $ids ) { |
| 336 | $conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase::LIST_AND ); |
| 337 | } |
| 338 | if ( $names ) { |
| 339 | $conds['username'] = $db->makeList( [ $text => $names ], IDatabase::LIST_AND ); |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | return [ |
| 344 | 'tables' => $tables, |
| 345 | 'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0', |
| 346 | 'orconds' => $conds, |
| 347 | 'joins' => $joins, |
| 348 | ]; |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * @internal For use immediately after construction only |
| 353 | * @param bool $forImport |
| 354 | */ |
| 355 | public function setForImport( bool $forImport ): void { |
| 356 | $this->forImport = $forImport; |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * @param string $domainId |
| 361 | * @return ActorNormalization |
| 362 | */ |
| 363 | protected function getActorNormalization( $domainId ): ActorNormalization { |
| 364 | if ( $this->forImport ) { |
| 365 | return $this->actorStoreFactory->getActorNormalizationForImport( $domainId ); |
| 366 | } else { |
| 367 | return $this->actorStoreFactory->getActorNormalization( $domainId ); |
| 368 | } |
| 369 | } |
| 370 | } |