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