Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.28% |
288 / 319 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
LazyVariableComputer | |
90.28% |
288 / 319 |
|
50.00% |
3 / 6 |
80.16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
compute | |
88.84% |
223 / 251 |
|
0.00% |
0 / 1 |
69.69 | |||
getLinksFromDB | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getLastPageAuthors | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
4.00 | |||
getContentModelFromRevision | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
parseNonEditWikitext | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\Variables; |
4 | |
5 | use MediaWiki\Content\ContentHandler; |
6 | use MediaWiki\Content\TextContent; |
7 | use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner; |
8 | use MediaWiki\Extension\AbuseFilter\Parser\AFPData; |
9 | use MediaWiki\Extension\AbuseFilter\TextExtractor; |
10 | use MediaWiki\ExternalLinks\ExternalLinksLookup; |
11 | use MediaWiki\ExternalLinks\LinkFilter; |
12 | use MediaWiki\Language\Language; |
13 | use MediaWiki\Parser\ParserFactory; |
14 | use MediaWiki\Parser\ParserOptions; |
15 | use MediaWiki\Permissions\PermissionManager; |
16 | use MediaWiki\Permissions\RestrictionStore; |
17 | use MediaWiki\Revision\RevisionLookup; |
18 | use MediaWiki\Revision\RevisionRecord; |
19 | use MediaWiki\Revision\RevisionStore; |
20 | use MediaWiki\Revision\SlotRecord; |
21 | use MediaWiki\Storage\PreparedUpdate; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\User\ExternalUserNames; |
24 | use MediaWiki\User\User; |
25 | use MediaWiki\User\UserEditTracker; |
26 | use MediaWiki\User\UserGroupManager; |
27 | use MediaWiki\User\UserIdentity; |
28 | use MediaWiki\User\UserIdentityUtils; |
29 | use Psr\Log\LoggerInterface; |
30 | use stdClass; |
31 | use StringUtils; |
32 | use UnexpectedValueException; |
33 | use Wikimedia\Diff\Diff; |
34 | use Wikimedia\Diff\UnifiedDiffFormatter; |
35 | use Wikimedia\IPUtils; |
36 | use Wikimedia\ObjectCache\WANObjectCache; |
37 | use Wikimedia\Rdbms\Database; |
38 | use Wikimedia\Rdbms\LBFactory; |
39 | use Wikimedia\Rdbms\SelectQueryBuilder; |
40 | use WikiPage; |
41 | |
42 | /** |
43 | * Service used to compute lazy-loaded variable. |
44 | * @internal |
45 | */ |
46 | class LazyVariableComputer { |
47 | public const SERVICE_NAME = 'AbuseFilterLazyVariableComputer'; |
48 | |
49 | /** |
50 | * @var float The amount of time to subtract from profiling |
51 | * @todo This is a hack |
52 | */ |
53 | public static $profilingExtraTime = 0; |
54 | |
55 | /** @var TextExtractor */ |
56 | private $textExtractor; |
57 | |
58 | /** @var AbuseFilterHookRunner */ |
59 | private $hookRunner; |
60 | |
61 | /** @var LoggerInterface */ |
62 | private $logger; |
63 | |
64 | /** @var LBFactory */ |
65 | private $lbFactory; |
66 | |
67 | /** @var WANObjectCache */ |
68 | private $wanCache; |
69 | |
70 | /** @var RevisionLookup */ |
71 | private $revisionLookup; |
72 | |
73 | /** @var RevisionStore */ |
74 | private $revisionStore; |
75 | |
76 | /** @var Language */ |
77 | private $contentLanguage; |
78 | |
79 | /** @var ParserFactory */ |
80 | private $parserFactory; |
81 | |
82 | /** @var UserEditTracker */ |
83 | private $userEditTracker; |
84 | |
85 | /** @var UserGroupManager */ |
86 | private $userGroupManager; |
87 | |
88 | /** @var PermissionManager */ |
89 | private $permissionManager; |
90 | |
91 | /** @var RestrictionStore */ |
92 | private $restrictionStore; |
93 | |
94 | /** @var UserIdentityUtils */ |
95 | private $userIdentityUtils; |
96 | |
97 | /** @var string */ |
98 | private $wikiID; |
99 | |
100 | /** |
101 | * @param TextExtractor $textExtractor |
102 | * @param AbuseFilterHookRunner $hookRunner |
103 | * @param LoggerInterface $logger |
104 | * @param LBFactory $lbFactory |
105 | * @param WANObjectCache $wanCache |
106 | * @param RevisionLookup $revisionLookup |
107 | * @param RevisionStore $revisionStore |
108 | * @param Language $contentLanguage |
109 | * @param ParserFactory $parserFactory |
110 | * @param UserEditTracker $userEditTracker |
111 | * @param UserGroupManager $userGroupManager |
112 | * @param PermissionManager $permissionManager |
113 | * @param RestrictionStore $restrictionStore |
114 | * @param UserIdentityUtils $userIdentityUtils |
115 | * @param string $wikiID |
116 | */ |
117 | public function __construct( |
118 | TextExtractor $textExtractor, |
119 | AbuseFilterHookRunner $hookRunner, |
120 | LoggerInterface $logger, |
121 | LBFactory $lbFactory, |
122 | WANObjectCache $wanCache, |
123 | RevisionLookup $revisionLookup, |
124 | RevisionStore $revisionStore, |
125 | Language $contentLanguage, |
126 | ParserFactory $parserFactory, |
127 | UserEditTracker $userEditTracker, |
128 | UserGroupManager $userGroupManager, |
129 | PermissionManager $permissionManager, |
130 | RestrictionStore $restrictionStore, |
131 | UserIdentityUtils $userIdentityUtils, |
132 | string $wikiID |
133 | ) { |
134 | $this->textExtractor = $textExtractor; |
135 | $this->hookRunner = $hookRunner; |
136 | $this->logger = $logger; |
137 | $this->lbFactory = $lbFactory; |
138 | $this->wanCache = $wanCache; |
139 | $this->revisionLookup = $revisionLookup; |
140 | $this->revisionStore = $revisionStore; |
141 | $this->contentLanguage = $contentLanguage; |
142 | $this->parserFactory = $parserFactory; |
143 | $this->userEditTracker = $userEditTracker; |
144 | $this->userGroupManager = $userGroupManager; |
145 | $this->permissionManager = $permissionManager; |
146 | $this->restrictionStore = $restrictionStore; |
147 | $this->userIdentityUtils = $userIdentityUtils; |
148 | $this->wikiID = $wikiID; |
149 | } |
150 | |
151 | /** |
152 | * XXX: $getVarCB is a hack to hide the cyclic dependency with VariablesManager. See T261069 for possible |
153 | * solutions. This might also be merged into VariablesManager, but it would bring a ton of dependencies. |
154 | * @todo Should we remove $vars parameter (check hooks)? |
155 | * |
156 | * @param LazyLoadedVariable $var |
157 | * @param VariableHolder $vars |
158 | * @param callable $getVarCB |
159 | * @phan-param callable(string $name):AFPData $getVarCB |
160 | * @return AFPData |
161 | */ |
162 | public function compute( LazyLoadedVariable $var, VariableHolder $vars, callable $getVarCB ) { |
163 | $parameters = $var->getParameters(); |
164 | $varMethod = $var->getMethod(); |
165 | $result = null; |
166 | |
167 | if ( !$this->hookRunner->onAbuseFilter_interceptVariable( |
168 | $varMethod, |
169 | $vars, |
170 | $parameters, |
171 | $result |
172 | ) ) { |
173 | return $result instanceof AFPData |
174 | ? $result : AFPData::newFromPHPVar( $result ); |
175 | } |
176 | |
177 | switch ( $varMethod ) { |
178 | case 'diff': |
179 | $text1Var = $parameters['oldtext-var']; |
180 | $text2Var = $parameters['newtext-var']; |
181 | $text1 = $getVarCB( $text1Var )->toString(); |
182 | $text2 = $getVarCB( $text2Var )->toString(); |
183 | // T74329: if there's no text, don't return an array with the empty string |
184 | $text1 = $text1 === '' ? [] : explode( "\n", $text1 ); |
185 | $text2 = $text2 === '' ? [] : explode( "\n", $text2 ); |
186 | $diffs = new Diff( $text1, $text2 ); |
187 | $format = new UnifiedDiffFormatter(); |
188 | $result = $format->format( $diffs ); |
189 | break; |
190 | case 'diff-split': |
191 | $diff = $getVarCB( $parameters['diff-var'] )->toString(); |
192 | $line_prefix = $parameters['line-prefix']; |
193 | $diff_lines = explode( "\n", $diff ); |
194 | $result = []; |
195 | foreach ( $diff_lines as $line ) { |
196 | if ( ( $line[0] ?? '' ) === $line_prefix ) { |
197 | $result[] = substr( $line, 1 ); |
198 | } |
199 | } |
200 | break; |
201 | case 'array-diff': |
202 | $baseVar = $parameters['base-var']; |
203 | $minusVar = $parameters['minus-var']; |
204 | |
205 | $baseArray = $getVarCB( $baseVar )->toNative(); |
206 | $minusArray = $getVarCB( $minusVar )->toNative(); |
207 | |
208 | $result = array_diff( $baseArray, $minusArray ); |
209 | break; |
210 | case 'links-from-wikitext': |
211 | // This should ONLY be used when sharing a parse operation with the edit. |
212 | |
213 | /** @var WikiPage $article */ |
214 | $article = $parameters['article']; |
215 | if ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) { |
216 | // Shared with the edit, don't count it in profiling |
217 | $startTime = microtime( true ); |
218 | $textVar = $parameters['text-var']; |
219 | |
220 | $new_text = $getVarCB( $textVar )->toString(); |
221 | $content = ContentHandler::makeContent( $new_text, $article->getTitle() ); |
222 | $editInfo = $article->prepareContentForEdit( |
223 | $content, |
224 | null, |
225 | $parameters['contextUserIdentity'] |
226 | ); |
227 | $result = LinkFilter::getIndexedUrlsNonReversed( |
228 | array_keys( $editInfo->output->getExternalLinks() ) |
229 | ); |
230 | self::$profilingExtraTime += ( microtime( true ) - $startTime ); |
231 | break; |
232 | } |
233 | // Otherwise fall back to database |
234 | case 'links-from-wikitext-or-database': |
235 | // TODO: use Content object instead, if available! |
236 | /** @var WikiPage $article */ |
237 | $article ??= $parameters['article']; |
238 | |
239 | // this inference is ugly, but the name isn't accessible from here |
240 | // and we only want this for debugging |
241 | $textVar = $parameters['text-var']; |
242 | $varName = str_starts_with( $textVar, 'old_' ) ? 'old_links' : 'all_links'; |
243 | if ( $parameters['forFilter'] ?? false ) { |
244 | $this->logger->debug( "Loading $varName from DB" ); |
245 | $links = $this->getLinksFromDB( $article ); |
246 | } elseif ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) { |
247 | $this->logger->debug( "Loading $varName from Parser" ); |
248 | |
249 | $wikitext = $getVarCB( $textVar )->toString(); |
250 | $editInfo = $this->parseNonEditWikitext( |
251 | $wikitext, |
252 | $article, |
253 | $parameters['contextUserIdentity'] |
254 | ); |
255 | $links = LinkFilter::getIndexedUrlsNonReversed( |
256 | array_keys( $editInfo->output->getExternalLinks() ) |
257 | ); |
258 | } else { |
259 | // TODO: Get links from Content object. But we don't have the content object. |
260 | // And for non-text content, $wikitext is usually not going to be a valid |
261 | // serialization, but rather some dummy text for filtering. |
262 | $links = []; |
263 | } |
264 | |
265 | $result = $links; |
266 | break; |
267 | case 'links-from-update': |
268 | /** @var PreparedUpdate $update */ |
269 | $update = $parameters['update']; |
270 | // Shared with the edit, don't count it in profiling |
271 | $startTime = microtime( true ); |
272 | $result = LinkFilter::getIndexedUrlsNonReversed( |
273 | array_keys( $update->getParserOutputForMetaData()->getExternalLinks() ) |
274 | ); |
275 | self::$profilingExtraTime += ( microtime( true ) - $startTime ); |
276 | break; |
277 | case 'links-from-database': |
278 | /** @var WikiPage $article */ |
279 | $article = $parameters['article']; |
280 | $this->logger->debug( 'Loading old_links from DB' ); |
281 | $result = $this->getLinksFromDB( $article ); |
282 | break; |
283 | case 'parse-wikitext': |
284 | // Should ONLY be used when sharing a parse operation with the edit. |
285 | // TODO: use Content object instead, if available! |
286 | /* @var WikiPage $article */ |
287 | $article = $parameters['article']; |
288 | if ( $article->getContentModel() === CONTENT_MODEL_WIKITEXT ) { |
289 | // Shared with the edit, don't count it in profiling |
290 | $startTime = microtime( true ); |
291 | $textVar = $parameters['wikitext-var']; |
292 | |
293 | $new_text = $getVarCB( $textVar )->toString(); |
294 | $content = ContentHandler::makeContent( $new_text, $article->getTitle() ); |
295 | $editInfo = $article->prepareContentForEdit( |
296 | $content, |
297 | null, |
298 | $parameters['contextUserIdentity'] |
299 | ); |
300 | if ( isset( $parameters['pst'] ) && $parameters['pst'] ) { |
301 | $result = $editInfo->pstContent->serialize( $editInfo->format ); |
302 | } else { |
303 | // Note: as of core change r727361, the PP limit comments (which we don't want to be here) |
304 | // are already excluded. |
305 | $result = $editInfo->getOutput()->getText(); |
306 | } |
307 | self::$profilingExtraTime += ( microtime( true ) - $startTime ); |
308 | } else { |
309 | $result = ''; |
310 | } |
311 | break; |
312 | case 'html-from-update': |
313 | /** @var PreparedUpdate $update */ |
314 | $update = $parameters['update']; |
315 | // Shared with the edit, don't count it in profiling |
316 | $startTime = microtime( true ); |
317 | $result = $update->getCanonicalParserOutput()->getText(); |
318 | self::$profilingExtraTime += ( microtime( true ) - $startTime ); |
319 | break; |
320 | case 'strip-html': |
321 | $htmlVar = $parameters['html-var']; |
322 | $html = $getVarCB( $htmlVar )->toString(); |
323 | $stripped = StringUtils::delimiterReplace( '<', '>', '', $html ); |
324 | // We strip extra spaces to the right because the stripping above |
325 | // could leave a lot of whitespace. |
326 | // @fixme Find a better way to do this. |
327 | $result = TextContent::normalizeLineEndings( $stripped ); |
328 | break; |
329 | case 'load-recent-authors': |
330 | $result = $this->getLastPageAuthors( $parameters['title'] ); |
331 | break; |
332 | case 'load-first-author': |
333 | $revision = $this->revisionLookup->getFirstRevision( $parameters['title'] ); |
334 | if ( $revision ) { |
335 | // TODO T233241 |
336 | $user = $revision->getUser(); |
337 | $result = $user === null ? '' : $user->getName(); |
338 | } else { |
339 | $result = ''; |
340 | } |
341 | break; |
342 | case 'get-page-restrictions': |
343 | $action = $parameters['action']; |
344 | /** @var Title $title */ |
345 | $title = $parameters['title']; |
346 | $result = $this->restrictionStore->getRestrictions( $title, $action ); |
347 | break; |
348 | case 'user-unnamed-ip': |
349 | $user = $parameters['user']; |
350 | $result = null; |
351 | |
352 | // Don't return an IP for past events (eg. revisions, logs) |
353 | // This could leak IPs to users who don't have IP viewing rights |
354 | if ( !$parameters['rc'] && |
355 | // Reveal IPs for: |
356 | // - temporary accounts: temporary account names will replace the IP in the `user_name` |
357 | // variable. This variable restores this access. |
358 | // - logged-out users: This supports the transition to the use of temporary accounts |
359 | // so that filter maintainers on pre-transition wikis can migrate `user_name` to `user_unnamed_ip` |
360 | // where necessary and see no disruption on transition. |
361 | // |
362 | // This variable should only ever be exposed for these use cases and shouldn't be extended |
363 | // to registered accounts, as that would leak account PII to users without the right to see |
364 | // that information |
365 | ( $this->userIdentityUtils->isTemp( $user ) || IPUtils::isIPAddress( $user->getName() ) ) ) { |
366 | $result = $user->getRequest()->getIP(); |
367 | } |
368 | break; |
369 | case 'user-type': |
370 | /** @var UserIdentity $userIdentity */ |
371 | $userIdentity = $parameters['user-identity']; |
372 | if ( $this->userIdentityUtils->isNamed( $userIdentity ) ) { |
373 | $result = 'named'; |
374 | } elseif ( $this->userIdentityUtils->isTemp( $userIdentity ) ) { |
375 | $result = 'temp'; |
376 | } elseif ( IPUtils::isIPAddress( $userIdentity->getName() ) ) { |
377 | $result = 'ip'; |
378 | } elseif ( ExternalUserNames::isExternal( $userIdentity->getName() ) ) { |
379 | $result = 'external'; |
380 | } else { |
381 | $result = 'unknown'; |
382 | } |
383 | break; |
384 | case 'user-editcount': |
385 | /** @var UserIdentity $userIdentity */ |
386 | $userIdentity = $parameters['user-identity']; |
387 | $result = $this->userEditTracker->getUserEditCount( $userIdentity ); |
388 | break; |
389 | case 'user-emailconfirm': |
390 | /** @var User $user */ |
391 | $user = $parameters['user']; |
392 | $result = $user->getEmailAuthenticationTimestamp(); |
393 | break; |
394 | case 'user-groups': |
395 | /** @var UserIdentity $userIdentity */ |
396 | $userIdentity = $parameters['user-identity']; |
397 | $result = $this->userGroupManager->getUserEffectiveGroups( $userIdentity ); |
398 | break; |
399 | case 'user-rights': |
400 | /** @var UserIdentity $userIdentity */ |
401 | $userIdentity = $parameters['user-identity']; |
402 | $result = $this->permissionManager->getUserPermissions( $userIdentity ); |
403 | break; |
404 | case 'user-block': |
405 | // @todo Support partial blocks? |
406 | /** @var User $user */ |
407 | $user = $parameters['user']; |
408 | $result = (bool)$user->getBlock(); |
409 | break; |
410 | case 'user-age': |
411 | /** @var User $user */ |
412 | $user = $parameters['user']; |
413 | $asOf = $parameters['asof']; |
414 | |
415 | if ( !$user->isRegistered() ) { |
416 | $result = 0; |
417 | } else { |
418 | // HACK: If there's no registration date, assume 2008-01-15, Wikipedia Day |
419 | // in the year before the new user log was created. See T243469. |
420 | $registration = $user->getRegistration() ?? "20080115000000"; |
421 | $result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $registration ); |
422 | } |
423 | break; |
424 | case 'page-age': |
425 | /** @var Title $title */ |
426 | $title = $parameters['title']; |
427 | |
428 | $firstRev = $this->revisionLookup->getFirstRevision( $title ); |
429 | $firstRevisionTime = $firstRev ? $firstRev->getTimestamp() : null; |
430 | if ( !$firstRevisionTime ) { |
431 | $result = 0; |
432 | break; |
433 | } |
434 | |
435 | $asOf = $parameters['asof']; |
436 | $result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $firstRevisionTime ); |
437 | break; |
438 | case 'revision-age-by-id': |
439 | $timestamp = $this->revisionLookup->getTimestampFromId( $parameters['revid'] ); |
440 | if ( !$timestamp ) { |
441 | $result = null; |
442 | break; |
443 | } |
444 | $asOf = $parameters['asof']; |
445 | $result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $timestamp ); |
446 | break; |
447 | case 'revision-age-by-title': |
448 | /** @var Title $title */ |
449 | $title = $parameters['title']; |
450 | $revRec = $this->revisionLookup->getRevisionByTitle( $title ); |
451 | if ( !$revRec ) { |
452 | $result = null; |
453 | break; |
454 | } |
455 | $asOf = $parameters['asof']; |
456 | $result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $revRec->getTimestamp() ); |
457 | break; |
458 | case 'previous-revision-age': |
459 | $revRec = $this->revisionLookup->getRevisionById( $parameters['revid'] ); |
460 | if ( !$revRec ) { |
461 | $result = null; |
462 | break; |
463 | } |
464 | $prev = $this->revisionLookup->getPreviousRevision( $revRec ); |
465 | if ( !$prev ) { |
466 | $result = null; |
467 | break; |
468 | } |
469 | $asOf = $parameters['asof'] ?? $revRec->getTimestamp(); |
470 | $result = (int)wfTimestamp( TS_UNIX, $asOf ) - (int)wfTimestamp( TS_UNIX, $prev->getTimestamp() ); |
471 | break; |
472 | case 'length': |
473 | $s = $getVarCB( $parameters['length-var'] )->toString(); |
474 | $result = strlen( $s ); |
475 | break; |
476 | case 'subtract-int': |
477 | $v1 = $getVarCB( $parameters['val1-var'] )->toInt(); |
478 | $v2 = $getVarCB( $parameters['val2-var'] )->toInt(); |
479 | $result = $v1 - $v2; |
480 | break; |
481 | case 'content-model-by-id': |
482 | $revRec = $this->revisionLookup->getRevisionById( $parameters['revid'] ); |
483 | $result = $this->getContentModelFromRevision( $revRec ); |
484 | break; |
485 | case 'revision-text-by-id': |
486 | $revRec = $this->revisionLookup->getRevisionById( $parameters['revid'] ); |
487 | $result = $this->textExtractor->revisionToString( $revRec, $parameters['contextUser'] ); |
488 | break; |
489 | case 'get-wiki-name': |
490 | $result = $this->wikiID; |
491 | break; |
492 | case 'get-wiki-language': |
493 | $result = $this->contentLanguage->getCode(); |
494 | break; |
495 | default: |
496 | if ( $this->hookRunner->onAbuseFilter_computeVariable( |
497 | $varMethod, |
498 | $vars, |
499 | $parameters, |
500 | $result |
501 | ) ) { |
502 | throw new UnexpectedValueException( 'Unknown variable compute type ' . $varMethod ); |
503 | } |
504 | } |
505 | |
506 | return $result instanceof AFPData ? $result : AFPData::newFromPHPVar( $result ); |
507 | } |
508 | |
509 | /** |
510 | * @param WikiPage $article |
511 | * @return array |
512 | */ |
513 | private function getLinksFromDB( WikiPage $article ) { |
514 | $id = $article->getId(); |
515 | if ( !$id ) { |
516 | return []; |
517 | } |
518 | |
519 | return ExternalLinksLookup::getExternalLinksForPage( |
520 | $id, |
521 | $this->lbFactory->getReplicaDatabase(), |
522 | __METHOD__ |
523 | ); |
524 | } |
525 | |
526 | /** |
527 | * @todo Move to MW core (T272050) |
528 | * @param Title $title |
529 | * @return string[] Usernames of the last 10 (unique) authors from $title |
530 | */ |
531 | private function getLastPageAuthors( Title $title ) { |
532 | if ( !$title->exists() ) { |
533 | return []; |
534 | } |
535 | |
536 | $fname = __METHOD__; |
537 | |
538 | return $this->wanCache->getWithSetCallback( |
539 | $this->wanCache->makeKey( 'last-10-authors', 'revision', $title->getLatestRevID() ), |
540 | WANObjectCache::TTL_MINUTE, |
541 | function ( $oldValue, &$ttl, array &$setOpts ) use ( $title, $fname ) { |
542 | $dbr = $this->lbFactory->getReplicaDatabase(); |
543 | |
544 | $setOpts += Database::getCacheSetOptions( $dbr ); |
545 | // Get the last 100 edit authors with a trivial query (avoid T116557) |
546 | $revQuery = $this->revisionStore->getQueryInfo(); |
547 | $revAuthors = $dbr->newSelectQueryBuilder() |
548 | ->tables( $revQuery['tables'] ) |
549 | ->field( $revQuery['fields']['rev_user_text'] ) |
550 | ->where( [ |
551 | 'rev_page' => $title->getArticleID(), |
552 | // TODO Should deleted names be counted in the 10 authors? If yes, this check should |
553 | // be moved inside the foreach |
554 | 'rev_deleted' => 0 |
555 | ] ) |
556 | ->caller( $fname ) |
557 | // Some pages have < 10 authors but many revisions (e.g. bot pages) |
558 | ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC ) |
559 | ->limit( 100 ) |
560 | // Force index per T116557 |
561 | ->useIndex( [ 'revision' => 'rev_page_timestamp' ] ) |
562 | ->joinConds( $revQuery['joins'] ) |
563 | ->fetchFieldValues(); |
564 | // Get the last 10 distinct authors within this set of edits |
565 | $users = []; |
566 | foreach ( $revAuthors as $author ) { |
567 | $users[$author] = 1; |
568 | if ( count( $users ) >= 10 ) { |
569 | break; |
570 | } |
571 | } |
572 | |
573 | return array_keys( $users ); |
574 | } |
575 | ); |
576 | } |
577 | |
578 | /** |
579 | * @param ?RevisionRecord $revision |
580 | * @return string |
581 | */ |
582 | private function getContentModelFromRevision( ?RevisionRecord $revision ): string { |
583 | // this is consistent with what is done on various places in RunVariableGenerator |
584 | // and RCVariableGenerator |
585 | if ( $revision !== null ) { |
586 | $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW ); |
587 | return $content->getModel(); |
588 | } |
589 | return ''; |
590 | } |
591 | |
592 | /** |
593 | * It's like WikiPage::prepareContentForEdit, but not for editing (old wikitext usually) |
594 | * |
595 | * @param string $wikitext |
596 | * @param WikiPage $article |
597 | * @param UserIdentity $userIdentity Context user |
598 | * |
599 | * @return stdClass |
600 | */ |
601 | private function parseNonEditWikitext( $wikitext, WikiPage $article, UserIdentity $userIdentity ) { |
602 | static $cache = []; |
603 | |
604 | $cacheKey = md5( $wikitext ) . ':' . $article->getTitle()->getPrefixedText(); |
605 | |
606 | if ( !isset( $cache[$cacheKey] ) ) { |
607 | $options = ParserOptions::newFromUser( $userIdentity ); |
608 | $cache[$cacheKey] = (object)[ |
609 | 'output' => $this->parserFactory->getInstance()->parse( $wikitext, $article->getTitle(), $options ) |
610 | ]; |
611 | } |
612 | |
613 | return $cache[$cacheKey]; |
614 | } |
615 | } |