Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.05% covered (danger)
19.05%
60 / 315
6.90% covered (danger)
6.90%
2 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
19.05% covered (danger)
19.05%
60 / 315
6.90% covered (danger)
6.90%
2 / 29
5512.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 onPageMoveComplete
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
6.07
 onRevisionFromEditComplete
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 onPageSaveComplete
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 onLinksUpdateComplete
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 addToPageTriageQueue
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
9.08
 flushUserStatusCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 shouldShowNoIndex
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 shouldNoIndexForNewArticleReasons
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 shouldNoIndexForMagicWordReasons
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isNewEnoughToNoIndex
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 onArticleViewFooter
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
210
 maybeShowUnpatrolLink
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 onMarkPatrolledComplete
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 onBlockIpComplete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onUnblockUserComplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 toolbarContentLanguageMessages
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 toolbarConfig
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 onBeforeCreateEchoEvent
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 locateUsersForNotification
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 onLocalUserCreated
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onORESCheckModels
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsAllowedAdd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onApiMain__moduleManager
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onPageDeleteComplete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 onPageUndeleteComplete
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3namespace MediaWiki\Extension\PageTriage;
4
5use ApiDisabled;
6use Article;
7use ExtensionRegistry;
8use IBufferingStatsdDataFactory;
9use ManualLogEntry;
10use MediaWiki\Api\Hook\ApiMain__moduleManagerHook;
11use MediaWiki\Auth\Hook\LocalUserCreatedHook;
12use MediaWiki\ChangeTags\Hook\ChangeTagsAllowedAddHook;
13use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
14use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
15use MediaWiki\Config\Config;
16use MediaWiki\Deferred\DeferredUpdates;
17use MediaWiki\Extension\Notifications\Model\Event;
18use MediaWiki\Extension\PageTriage\ArticleCompile\ArticleCompileProcessor;
19use MediaWiki\Extension\PageTriage\Notifications\PageTriageAddDeletionTagPresentationModel;
20use MediaWiki\Extension\PageTriage\Notifications\PageTriageAddMaintenanceTagPresentationModel;
21use MediaWiki\Extension\PageTriage\Notifications\PageTriageMarkAsReviewedPresentationModel;
22use MediaWiki\Hook\BlockIpCompleteHook;
23use MediaWiki\Hook\LinksUpdateCompleteHook;
24use MediaWiki\Hook\MarkPatrolledCompleteHook;
25use MediaWiki\Hook\PageMoveCompleteHook;
26use MediaWiki\Hook\UnblockUserCompleteHook;
27use MediaWiki\Html\Html;
28use MediaWiki\MediaWikiServices;
29use MediaWiki\Output\OutputPage;
30use MediaWiki\Page\Hook\ArticleViewFooterHook;
31use MediaWiki\Page\Hook\PageDeleteCompleteHook;
32use MediaWiki\Page\Hook\PageUndeleteCompleteHook;
33use MediaWiki\Page\Hook\RevisionFromEditCompleteHook;
34use MediaWiki\Page\PageIdentity;
35use MediaWiki\Page\ProperPageIdentity;
36use MediaWiki\Page\WikiPageFactory;
37use MediaWiki\Parser\ParserOutput;
38use MediaWiki\Permissions\Authority;
39use MediaWiki\Permissions\PermissionManager;
40use MediaWiki\ResourceLoader\Context;
41use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
42use MediaWiki\Revision\RevisionLookup;
43use MediaWiki\Revision\RevisionRecord;
44use MediaWiki\Revision\RevisionStore;
45use MediaWiki\Revision\SlotRecord;
46use MediaWiki\Storage\Hook\PageSaveCompleteHook;
47use MediaWiki\Title\Title;
48use MediaWiki\Title\TitleFactory;
49use MediaWiki\User\Options\UserOptionsManager;
50use MediaWiki\User\User;
51use MediaWiki\User\UserIdentity;
52use MediaWiki\Utils\MWTimestamp;
53use MediaWiki\WikiMap\WikiMap;
54use RecentChange;
55use Wikimedia\Rdbms\Database;
56use WikiPage;
57
58class Hooks implements
59    ApiMain__moduleManagerHook,
60    ListDefinedTagsHook,
61    ChangeTagsListActiveHook,
62    ChangeTagsAllowedAddHook,
63    PageMoveCompleteHook,
64    RevisionFromEditCompleteHook,
65    PageSaveCompleteHook,
66    LinksUpdateCompleteHook,
67    ArticleViewFooterHook,
68    PageDeleteCompleteHook,
69    MarkPatrolledCompleteHook,
70    BlockIpCompleteHook,
71    UnblockUserCompleteHook,
72    ResourceLoaderGetConfigVarsHook,
73    LocalUserCreatedHook,
74    PageUndeleteCompleteHook
75{
76
77    private const TAG_NAME = 'pagetriage';
78
79    /** @var Config */
80    private Config $config;
81    /** @var QueueManager */
82    private QueueManager $queueManager;
83
84    /** @var RevisionLookup */
85    private RevisionLookup $revisionLookup;
86
87    /** @var IBufferingStatsdDataFactory */
88    private IBufferingStatsdDataFactory $statsdDataFactory;
89
90    /** @var PermissionManager */
91    private PermissionManager $permissionManager;
92
93    /** @var RevisionStore */
94    private RevisionStore $revisionStore;
95
96    /** @var TitleFactory */
97    private TitleFactory $titleFactory;
98
99    /** @var UserOptionsManager */
100    private UserOptionsManager $userOptionsManager;
101
102    /** @var WikiPageFactory */
103    private WikiPageFactory $wikiPageFactory;
104
105    /**
106     * @param Config $config
107     * @param RevisionLookup $revisionLookup
108     * @param IBufferingStatsdDataFactory $statsdDataFactory
109     * @param PermissionManager $permissionManager
110     * @param RevisionStore $revisionStore
111     * @param TitleFactory $titleFactory
112     * @param UserOptionsManager $userOptionsManager
113     * @param QueueManager $queueManager
114     * @param WikiPageFactory $wikiPageFactory
115     */
116    public function __construct(
117        Config $config,
118        RevisionLookup $revisionLookup,
119        IBufferingStatsdDataFactory $statsdDataFactory,
120        PermissionManager $permissionManager,
121        RevisionStore $revisionStore,
122        TitleFactory $titleFactory,
123        UserOptionsManager $userOptionsManager,
124        QueueManager $queueManager,
125        WikiPageFactory $wikiPageFactory
126    ) {
127        $this->config = $config;
128        $this->revisionLookup = $revisionLookup;
129        $this->statsdDataFactory = $statsdDataFactory;
130        $this->permissionManager = $permissionManager;
131        $this->revisionStore = $revisionStore;
132        $this->titleFactory = $titleFactory;
133        $this->userOptionsManager = $userOptionsManager;
134        $this->queueManager = $queueManager;
135        $this->wikiPageFactory = $wikiPageFactory;
136    }
137
138    /** @inheritDoc */
139    public function onPageMoveComplete(
140        $oldTitle,
141        $newTitle,
142        $user,
143        $oldid,
144        $newid,
145        $reason,
146        $revisionRecord
147    ) {
148        // Mark a page as unreviewed after moving the page from non-main(article) namespace to
149        // main(article) namespace
150        // Delete cache for record if it's in pagetriage queue
151        $articleMetadata = new ArticleMetadata( [ $oldid ] );
152        $articleMetadata->flushMetadataFromCache();
153
154        $oldTitle = $this->titleFactory->newFromLinkTarget( $oldTitle );
155        $newTitle = $this->titleFactory->newFromLinkTarget( $newTitle );
156
157        // Delete user status cache
158        self::flushUserStatusCache( $oldTitle->toPageIdentity() );
159        self::flushUserStatusCache( $newTitle->toPageIdentity() );
160
161        $oldNamespace = $oldTitle->getNamespace();
162        $newNamespace = $newTitle->getNamespace();
163        // Do nothing further on if
164        // 1. the page move is within the same namespace or
165        // 2. the new page is not in either the main or draft namespaces
166        $draftNsId = $this->config->get( 'PageTriageDraftNamespaceId' );
167        if (
168            $oldNamespace === $newNamespace
169            || !in_array( $newNamespace, [ NS_MAIN, $draftNsId ], true )
170        ) {
171            return;
172        }
173
174        // If not a new record to pagetriage queue, do nothing.
175        if ( !self::addToPageTriageQueue( $oldid, $newTitle, $user ) ) {
176            return;
177        }
178        // Item was newly added to PageTriage queue in master DB, compile metadata.
179        $acp = ArticleCompileProcessor::newFromPageId( [ $oldid ] );
180        if ( $acp ) {
181            // Since this is a title move, the only component requiring DB_PRIMARY will be
182            // BasicData.
183            $acp->configComponentDb(
184                ArticleCompileProcessor::getSafeComponentDbConfigForCompilation()
185            );
186            $acp->compileMetadata();
187        }
188    }
189
190    /** @inheritDoc */
191    public function onRevisionFromEditComplete( $wikiPage, $rev, $baseID, $user, &$tags ) {
192        // Check if a page is created from a redirect page, then insert into it PageTriage Queue
193        // Note: Page will be automatically marked as triaged for users with autopatrol right
194        if ( !in_array( $wikiPage->getTitle()->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
195            return;
196        }
197
198        if ( $rev && $rev->getParentId() ) {
199            // Make sure $prev->getContent() is done post-send if possible
200            DeferredUpdates::addCallableUpdate( function () use ( $rev, $wikiPage, $user ) {
201                $prevRevRecord = $this->revisionLookup->getRevisionById( $rev->getParentId() );
202                if ( $prevRevRecord &&
203                    !$wikiPage->isRedirect() &&
204                    $prevRevRecord->getContent( SlotRecord::MAIN )->isRedirect()
205                ) {
206                    // Add item to queue, if it's not already there.
207                    self::addToPageTriageQueue(
208                        $wikiPage->getId(),
209                        $wikiPage->getTitle(),
210                        $user
211                    );
212                }
213            } );
214        }
215    }
216
217    /** @inheritDoc */
218    public function onPageSaveComplete(
219        $wikiPage,
220        $user,
221        $summary,
222        $flags,
223        $revisionRecord,
224        $editResult
225    ) {
226        // When a new article is created, insert it into PageTriage Queue and compile metadata.
227        // Page saved, flush cache
228        self::flushUserStatusCache( $wikiPage );
229
230        if ( !( $flags & EDIT_NEW ) ) {
231            // Don't add to queue if it is not a new page
232            return;
233        }
234
235        // Don't add to queue if not in a namespace of interest.
236        if ( !in_array( $wikiPage->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
237            return;
238        }
239
240        // Add item to queue. Metadata compilation will get triggered in the LinksUpdate hook.
241        self::addToPageTriageQueue(
242            $wikiPage->getId(),
243            $wikiPage->getTitle(),
244            $user
245        );
246    }
247
248    /** @inheritDoc */
249    public function onLinksUpdateComplete( $linksUpdate, $ticket ) {
250        if ( !in_array( $linksUpdate->getTitle()->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
251            return;
252        }
253
254        // Update metadata when link information is updated.
255        // This is also run after every page save.
256        // Note that this hook can be triggered by a GET request (rollback action, until T88044 is
257        // sorted out), in which case master DB connections and writes on GET request can occur.
258        DeferredUpdates::addCallableUpdate( static function () use ( $linksUpdate ) {
259            // Validate the page ID from DB_PRIMARY, compile metadata from DB_PRIMARY and return.
260            $acp = ArticleCompileProcessor::newFromPageId(
261                [ $linksUpdate->getTitle()->getArticleID() ],
262                false,
263                DB_PRIMARY
264            );
265            if ( $acp ) {
266                $acp->registerLinksUpdate( $linksUpdate );
267                $acp->compileMetadata();
268            }
269        } );
270    }
271
272    /**
273     * Add page to page triage queue, check for autopatrol right if reviewed is not set
274     *
275     * This method should only be called from this class and its closures
276     *
277     * @param int $pageId
278     * @param Title $title
279     * @param UserIdentity|null $userIdentity
280     * @return bool
281     * @throws MWPageTriageMissingRevisionException
282     */
283    public static function addToPageTriageQueue( $pageId, $title, $userIdentity = null ): bool {
284        global $wgUseRCPatrol, $wgUseNPPatrol;
285
286        // Get draft information.
287        $draftNsId = MediaWikiServices::getInstance()
288            ->getMainConfig()
289            ->get( 'PageTriageDraftNamespaceId' );
290        $isDraft = $draftNsId !== false && $title->inNamespace( $draftNsId );
291
292        // Draft redirects are not patrolled or reviewed.
293        if ( $isDraft && $title->isRedirect() ) {
294            return false;
295        }
296
297        $pageTriage = new PageTriage( $pageId );
298
299        // action taken by system
300        if ( $userIdentity === null ) {
301            return $pageTriage->addToPageTriageQueue();
302        // action taken by a user
303        } else {
304            // set reviewed if it's not set yet
305            $user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $userIdentity );
306            $permissionErrors = MediaWikiServices::getInstance()->getPermissionManager()
307                ->getPermissionErrors( 'autopatrol', $user, $title );
308            $isAutopatrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
309                !count( $permissionErrors );
310            if ( $isAutopatrolled && !$isDraft ) {
311                // Set as reviewed if the user has the autopatrol right,
312                // and they're not creating a Draft.
313                return $pageTriage->addToPageTriageQueue(
314                    QueueRecord::REVIEW_STATUS_AUTOPATROLLED,
315                    $userIdentity
316                );
317            }
318            // If they have no autopatrol right and are not making an explicit review,
319            // set to unreviewed (as the system would, in this situation).
320            return $pageTriage->addToPageTriageQueue();
321        }
322    }
323
324    /**
325     * Flush user page/user talk page existence status, this function should
326     * be called when a page gets created/deleted/moved/restored
327     *
328     * @param PageIdentity $pageIdentity
329     */
330    private static function flushUserStatusCache( PageIdentity $pageIdentity ): void {
331        if ( in_array( $pageIdentity->getNamespace(), [ NS_USER, NS_USER_TALK ] ) ) {
332            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
333            $cache->delete( PageTriageUtil::userStatusKey( $pageIdentity->getDBkey() ) );
334        }
335    }
336
337    /**
338     * Determines whether to set noindex for the article specified
339     *
340     * The NOINDEX logic is explained at:
341     * https://www.mediawiki.org/wiki/Extension:PageTriage#NOINDEX
342     *
343     * @param Article $article
344     * @return bool
345     */
346    private static function shouldShowNoIndex( Article $article ) {
347        $page = $article->getPage();
348
349        if ( self::shouldNoIndexForNewArticleReasons( $page ) ) {
350            return true;
351        }
352
353        $wikitextHasNoIndexMagicWord = $article->mParserOutput instanceof ParserOutput
354            && $article->mParserOutput->getPageProperty( 'noindex' ) !== null;
355
356        return $wikitextHasNoIndexMagicWord && self::shouldNoIndexForMagicWordReasons( $page );
357    }
358
359    /**
360     * Calculate whether we should show NOINDEX, based on criteria related to whether
361     * the page is reviewed.
362     *
363     * The NOINDEX logic is explained at:
364     * https://www.mediawiki.org/wiki/Extension:PageTriage#NOINDEX
365     *
366     * Note that we always check the age of the page last since that is potentially the
367     * most expensive check (if the data isn't cached). Performance is important because
368     * this code is run on every page.
369     *
370     * @param WikiPage $page
371     * @return bool
372     */
373    private static function shouldNoIndexForNewArticleReasons( WikiPage $page ) {
374        global $wgPageTriageNoIndexUnreviewedNewArticles, $wgPageTriageMaxAge;
375
376        if ( !$wgPageTriageNoIndexUnreviewedNewArticles ) {
377            return false;
378        } elseif ( !PageTriageUtil::isPageUnreviewed( $page ) ) {
379            return false;
380        } elseif ( !self::isNewEnoughToNoIndex( $page, $wgPageTriageMaxAge ) ) {
381            return false;
382        } else {
383            return true;
384        }
385    }
386
387    /**
388     * Calculate whether we should show NOINDEX, based on criteria related to whether the
389     * page contains a __NOINDEX__ magic word.
390     *
391     * The NOINDEX logic is explained at:
392     * https://www.mediawiki.org/wiki/Extension:PageTriage#NOINDEX
393     *
394     * @param WikiPage $page
395     * @return bool
396     */
397    private static function shouldNoIndexForMagicWordReasons( WikiPage $page ) {
398        global $wgPageTriageMaxNoIndexAge;
399
400        return self::isNewEnoughToNoIndex( $page, $wgPageTriageMaxNoIndexAge );
401    }
402
403    /**
404     * Checks to see if an article is new, i.e. less than the supplied $maxAgeInDays
405     *
406     * Look in cache for the creation date. If not found, query the replica for the value
407     * of ptrp_created.
408     *
409     * @param WikiPage $wikiPage WikiPage to check
410     * @param int|null|false $maxAgeInDays How many days old an article has to be to be
411     * considered "not new".
412     * @return bool
413     */
414    private static function isNewEnoughToNoIndex( WikiPage $wikiPage, $maxAgeInDays ) {
415        $pageId = $wikiPage->getId();
416        if ( !$pageId ) {
417            return false;
418        }
419
420        // Allow disabling the age threshold for noindex by setting maxAge to null, 0, or false
421        if ( !$maxAgeInDays ) {
422            return true;
423        }
424
425        // Check cache for creation date
426        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
427        $pageCreationDateTime = $cache->getWithSetCallback(
428            $cache->makeKey( 'pagetriage-page-created', $pageId ),
429            $cache::TTL_DAY,
430            static function ( $oldValue, &$ttl, array &$setOpts ) use ( $pageId ) {
431                // The ptrp_created field is equivalent to creation_date
432                // property set during article metadata compilation.
433                $dbr = PageTriageUtil::getReplicaConnection();
434                $setOpts += Database::getCacheSetOptions( $dbr );
435                $queueLookup = PageTriageServices::wrap( MediaWikiServices::getInstance() )
436                    ->getQueueLookup();
437                $queueRecord = $queueLookup->getByPageId( $pageId );
438                return $queueRecord instanceof QueueRecord ? $queueRecord->getCreatedTimestamp() : false;
439            },
440            [ 'version' => PageTriage::CACHE_VERSION ]
441        );
442
443        // If still not found, return false.
444        if ( !$pageCreationDateTime ) {
445            return false;
446        }
447
448        // Get the age of the article in days
449        $timestamp = new MWTimestamp( $pageCreationDateTime );
450        $dateInterval = $timestamp->diff( new MWTimestamp() );
451        $articleDaysOld = $dateInterval->format( '%a' );
452
453        // If it's younger than the maximum age, return true.
454        return $articleDaysOld < $maxAgeInDays;
455    }
456
457    /** @inheritDoc */
458    public function onArticleViewFooter( $article, $patrolFooterShown ) {
459        // Handler for hook ArticleViewFooter, this will determine whether to load
460        // curation toolbar or 'mark as reviewed'/'reviewed' text
461        $wikiPage = $article->getPage();
462        $title = $wikiPage->getTitle();
463        $context = $article->getContext();
464        $user = $context->getUser();
465        $outputPage = $context->getOutput();
466        $request = $context->getRequest();
467
468        // Overwrite the noindex rule defined in Article::view(), this also affects main namespace
469        if ( self::shouldShowNoIndex( $article ) ) {
470            $outputPage->setRobotPolicy( 'noindex,nofollow' );
471            $this->statsdDataFactory->increment(
472                'extension.PageTriage.by_wiki.' . WikiMap::getCurrentWikiId() . '.noindex'
473            );
474        }
475
476        // Only named users can review
477        if ( !$user->isNamed() ) {
478            return;
479        }
480
481        // Only show in defined namespaces
482        if ( !in_array( $title->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
483            return;
484        }
485
486        // Don't do anything if it's coming from Special:NewPages
487        if ( $request->getVal( 'patrolpage' ) ) {
488            return;
489        }
490
491        $userCanPatrol = $this->permissionManager->quickUserCan( 'patrol', $user, $title );
492        $userCanAutoPatrol = $this->permissionManager->userHasRight( $user, 'autopatrol' );
493
494        $outputPage->addJsConfigVars( [
495            'wgPageTriageUserCanPatrol' => $userCanPatrol,
496            'wgPageTriageUserCanAutoPatrol' => $userCanAutoPatrol
497        ] );
498
499        // Don't show anything for user with no patrol right
500        if ( !$userCanPatrol ) {
501
502            // Maybe allow autopatrolled users to unpatrol their article
503            $this->maybeShowUnpatrolLink( $wikiPage, $user, $outputPage );
504            return;
505        }
506
507        // See if the page is in the PageTriage page queue
508        // If it isn't, $needsReview will be null
509        // Also, users without the autopatrol right can't review their own pages
510        $needsReview = PageTriageUtil::isPageUnreviewed( $wikiPage );
511
512        if ( $needsReview !== null
513            && (
514                !$user->equals( $this->revisionStore->getFirstRevision( $title )->getUser( RevisionRecord::RAW ) )
515                || $userCanAutoPatrol
516            )
517        ) {
518            if ( $this->config->get( 'PageTriageEnableCurationToolbar' ) ||
519                $request->getVal( 'curationtoolbar' ) === 'true' ) {
520                // Load the JavaScript for the curation toolbar
521                $outputPage->addModules( 'ext.pageTriage.toolbarStartup' );
522                $outputPage->addModuleStyles( [ 'mediawiki.interface.helpers.styles' ] );
523            } else {
524                if ( $needsReview ) {
525                    // show 'Mark as reviewed' link
526                    $msg = $context->msg( 'pagetriage-markpatrolled' )->text();
527                    $msg = Html::element(
528                        'a',
529                        [ 'href' => '#', 'class' => 'mw-pagetriage-markpatrolled-link' ],
530                        $msg
531                    );
532                } else {
533                    // show 'Reviewed' text
534                    $msg = $context->msg( 'pagetriage-reviewed' )->escaped();
535                }
536                $outputPage->addModules( [ 'ext.pageTriage.article' ] );
537                $html = Html::rawElement( 'div', [ 'class' => 'mw-pagetriage-markpatrolled' ], $msg );
538                $outputPage->addHTML( $html );
539            }
540        } elseif ( $needsReview === null && !$title->isMainPage() ) {
541            // Page is potentially usable, but not in the queue, allow users to add it manually
542            // Option is not shown if the article is the main page
543            $outputPage->addModules( 'ext.pageTriage.sidebarLink' );
544        }
545    }
546
547    /**
548     * Show a link to autopatrolled users without the 'patrol'
549     * userright that allows them to unreview a specific page iff
550     * the page is autopatrolled && they are the page's creator
551     *
552     * @param WikiPage $wikiPage Wikipage being viewed
553     * @param User $user Current user
554     * @param OutputPage $out Output of current page
555     */
556    private function maybeShowUnpatrolLink( WikiPage $wikiPage, User $user, OutputPage $out ): void {
557        $reviewStatus = PageTriageUtil::getStatus( $wikiPage );
558        $articleIsNotAutoPatrolled = $reviewStatus !== QueueRecord::REVIEW_STATUS_AUTOPATROLLED;
559        if ( $articleIsNotAutoPatrolled ) {
560            return;
561        }
562
563        $isAutopatrolled = $this->permissionManager->userHasRight( $user, 'autopatrol' );
564
565        if ( !$isAutopatrolled ) {
566            return;
567        }
568
569        $pageCreator = $this->revisionStore->getFirstRevision( $wikiPage )->getUser( RevisionRecord::RAW );
570        $isPageCreator = $pageCreator->equals( $user );
571
572        if ( $isPageCreator ) {
573            $out->addModules( 'ext.pageTriage.sidebarLink' );
574        }
575    }
576
577    /** @inheritDoc */
578    public function onMarkPatrolledComplete( $rcid, $user, $wcOnlySysopsCanPatrol, $auto ) {
579        // Sync records from patrol queue to triage queue
580        $rc = RecentChange::newFromId( $rcid );
581
582        if ( $rc ) {
583            if ( !in_array( $rc->getPage()->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
584                return;
585            }
586
587            $pt = new PageTriage( $rc->getAttribute( 'rc_cur_id' ) );
588            if ( $pt->addToPageTriageQueue( QueueRecord::REVIEW_STATUS_PATROLLED, $user, true ) ) {
589                // Compile metadata for new page triage record.
590                $acp = ArticleCompileProcessor::newFromPageId( [ $rc->getAttribute( 'rc_cur_id' ) ] );
591                if ( $acp ) {
592                    // Page was just inserted into PageTriage queue, so we need to compile BasicData
593                    // from DB_PRIMARY, since that component accesses the pagetriage_page table.
594                    $acp->configComponentDb(
595                        ArticleCompileProcessor::getSafeComponentDbConfigForCompilation()
596                    );
597                    $acp->compileMetadata();
598                }
599            }
600            $title = $this->titleFactory->newFromID( $rc->getAttribute( 'rc_cur_id' ) );
601            if ( $title ) {
602                PageTriageUtil::createNotificationEvent(
603                    $title,
604                    $user,
605                    'pagetriage-mark-as-reviewed'
606                );
607            }
608        }
609    }
610
611    /** @inheritDoc */
612    public function onBlockIpComplete( $block, $performer, $priorBlock ) {
613        // Update Article metadata when a user gets blocked.
614        PageTriageUtil::updateMetadataOnBlockChange( $block, (int)$block->isSitewide() );
615    }
616
617    /** @inheritDoc */
618    public function onUnblockUserComplete( $block, $performer ) {
619        // Update Article metadata when a user gets unblocked.
620        PageTriageUtil::updateMetadataOnBlockChange( $block, 0 );
621    }
622
623    /** @inheritDoc */
624    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
625        $pageTriageDraftNamespaceId = $config->get( 'PageTriageDraftNamespaceId' );
626        $vars['pageTriageNamespaces'] = PageTriageUtil::getNamespaces( $config );
627        $vars['wgPageTriageDraftNamespaceId'] = $pageTriageDraftNamespaceId;
628    }
629
630    /**
631     * Generates messages for toolbar
632     *
633     * @param Context $context
634     * @param Config $config
635     * @return array
636     */
637    public static function toolbarContentLanguageMessages( Context $context, Config $config ) {
638        $keys = array_merge(
639            [
640                'pagetriage-mark-mark-talk-page-notify-topic-title',
641                'pagetriage-mark-unmark-talk-page-notify-topic-title',
642                'pagetriage-feedback-from-new-page-review-process-title',
643                'pagetriage-feedback-from-new-page-review-process-message',
644                'pagetriage-note-sent-talk-page-notify-topic-title',
645                'pagetriage-note-sent-talk-page-notify-topic-title-reviewer',
646                'pagetriage-tags-talk-page-notify-topic-title'
647            ],
648            $config->get( 'PageTriageDeletionTagsOptionsContentLanguageMessages' )
649        );
650        $messages = [];
651        foreach ( $keys as $key ) {
652            $messages[$key] = $context->msg( $key )->inContentLanguage()->plain();
653        }
654        return $messages;
655    }
656
657    /**
658     * Generates messages for toolbar
659     *
660     * @param Context $context
661     * @param Config $config
662     * @return array
663     */
664    public static function toolbarConfig( Context $context, Config $config ) {
665        $pageTriageCurationModules = $config->get( 'PageTriageCurationModules' );
666        $pageTriageCurationDependencies = [];
667        if ( ExtensionRegistry::getInstance()->isLoaded( 'WikiLove' ) ) {
668            $pageTriageCurationModules['wikiLove'] = [
669                // depends on WikiLove extension
670                'helplink' => '//en.wikipedia.org/wiki/Wikipedia:Page_Curation/Help#WikiLove',
671                'namespace' => [ NS_MAIN, NS_USER ],
672            ];
673            $pageTriageCurationDependencies[] = 'ext.wikiLove.init';
674        }
675        return [
676            'PageTriageCurationDependencies' => $pageTriageCurationDependencies,
677            'PageTriageCurationModules' => $pageTriageCurationModules,
678            'PageTriageEnableCopyvio' => $config->get( 'PageTriageEnableCopyvio' ),
679            'PageTriageEnableOresFilters' => $config->get( 'PageTriageEnableOresFilters' ),
680            'PageTriageEnableExtendedFeatures' =>
681                $config->get( 'PageTriageEnableExtendedFeatures' ),
682            'TalkPageNoteTemplate' => $config->get( 'TalkPageNoteTemplate' ),
683        ];
684    }
685
686    /**
687     * Add PageTriage events to Echo
688     *
689     * @param array &$notifications array a list of enabled echo events
690     * @param array &$notificationCategories array details for echo events
691     * @param array &$icons array of icon details
692     * @return bool
693     */
694    public static function onBeforeCreateEchoEvent(
695        &$notifications, &$notificationCategories, &$icons
696    ) {
697        global $wgPageTriageEnabledEchoEvents;
698
699        if ( $wgPageTriageEnabledEchoEvents ) {
700            $notificationCategories['page-review'] = [
701                'priority' => 8,
702                'tooltip' => 'echo-pref-tooltip-page-review',
703            ];
704        }
705
706        if ( in_array( 'pagetriage-mark-as-reviewed', $wgPageTriageEnabledEchoEvents ) ) {
707            $notifications['pagetriage-mark-as-reviewed'] = [
708                'presentation-model' => PageTriageMarkAsReviewedPresentationModel::class,
709                'category' => 'page-review',
710                'group' => 'neutral',
711                'section' => 'message',
712                'user-locators' => [ [ self::class . '::locateUsersForNotification' ] ],
713            ];
714        }
715        if ( in_array( 'pagetriage-add-maintenance-tag', $wgPageTriageEnabledEchoEvents ) ) {
716            $notifications['pagetriage-add-maintenance-tag'] = [
717                'presentation-model' => PageTriageAddMaintenanceTagPresentationModel::class,
718                'category' => 'page-review',
719                'group' => 'neutral',
720                'section' => 'alert',
721                'user-locators' => [ [ self::class . '::locateUsersForNotification' ] ],
722            ];
723        }
724        if ( in_array( 'pagetriage-add-deletion-tag', $wgPageTriageEnabledEchoEvents ) ) {
725            $notifications['pagetriage-add-deletion-tag'] = [
726                'presentation-model' => PageTriageAddDeletionTagPresentationModel::class,
727                'category' => 'page-review',
728                'group' => 'negative',
729                'section' => 'alert',
730                'user-locators' => [ [ self::class . '::locateUsersForNotification' ] ],
731            ];
732            $icons['trash'] = [
733                'path' => 'PageTriage/echo-icons/trash.svg'
734            ];
735        }
736
737        return true;
738    }
739
740    /**
741     * For locating users to be notifies of an Echo Event.
742     * @param Event $event
743     * @return array
744     */
745    public static function locateUsersForNotification( Event $event ) {
746        if ( !$event->getTitle() ) {
747            return [];
748        }
749
750        $pageId = $event->getTitle()->getArticleID();
751
752        $articleMetadata = new ArticleMetadata( [ $pageId ], false, DB_REPLICA );
753        $metaData = $articleMetadata->getMetadata();
754
755        if ( !$metaData ) {
756            return [];
757        }
758
759        $users = [];
760        if ( $metaData[$pageId]['user_id'] ) {
761            $users[$metaData[$pageId]['user_id']] = User::newFromId( $metaData[$pageId]['user_id'] );
762        }
763        return $users;
764    }
765
766    /** @inheritDoc */
767    public function onLocalUserCreated( $user, $autocreated ) {
768        // New users get echo preferences set that are not the default settings for existing users.
769        // Specifically, new users are opted into email notifications for page reviews.
770        if ( !$autocreated ) {
771            $this->userOptionsManager->setOption( $user, 'echo-subscriptions-email-page-review', true );
772        }
773    }
774
775    /**
776     * @param RecentChange $rc
777     * @param array &$models Models names to score
778     */
779    public static function onORESCheckModels( RecentChange $rc, &$models ) {
780        if ( !in_array( $rc->getAttribute( 'rc_type' ), [ RC_NEW, RC_EDIT ] ) ) {
781            return;
782        }
783
784        if ( !ArticleMetadata::validatePageIds(
785            [ (int)$rc->getPage()->getDBkey() ], DB_REPLICA
786        ) ) {
787            return;
788        }
789
790        // Ensure all pages in the PageTriage queue
791        // are scored for both models regardless of namespace.
792        foreach ( [ 'articlequality', 'draftquality' ] as $model ) {
793            if ( !in_array( $model, $models ) ) {
794                $models[] = $model;
795            }
796        }
797    }
798
799    /** @inheritDoc */
800    public function onListDefinedTags( &$tags ) {
801        $tags[] = self::TAG_NAME;
802    }
803
804    /** @inheritDoc */
805    public function onChangeTagsAllowedAdd( &$allowedTags, $addTags, $user ) {
806        $allowedTags[] = self::TAG_NAME;
807    }
808
809    /** @inheritDoc */
810    public function onChangeTagsListActive( &$tags ) {
811        $tags[] = self::TAG_NAME;
812    }
813
814    // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
815
816    /** @inheritDoc */
817    public function onApiMain__moduleManager( $moduleManager ) {
818        // phpcs:enable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
819        if ( !$this->config->get( 'PageTriageEnableExtendedFeatures' ) ) {
820            $moduleManager->addModule(
821                'pagetriagetagging',
822                'action',
823                ApiDisabled::class
824            );
825        }
826    }
827
828    /** @inheritDoc */
829    public function onPageDeleteComplete(
830        ProperPageIdentity $page,
831        Authority $deleter,
832        string $reason,
833        int $pageID,
834        RevisionRecord $deletedRev,
835        ManualLogEntry $logEntry,
836        int $archivedRevisionCount
837    ) {
838        if ( $this->queueManager->isPageTriageNamespace( $page->getNamespace() ) ) {
839            // TODO: Factor the user status cache into another service.
840            self::flushUserStatusCache( $page );
841            $this->queueManager->deleteByPageId( $pageID );
842        }
843    }
844
845    /** @inheritDoc */
846    public function onPageUndeleteComplete(
847        ProperPageIdentity $page,
848        Authority $restorer,
849        string $reason,
850        RevisionRecord $restoredRev,
851        ManualLogEntry $logEntry,
852        int $restoredRevisionCount,
853        bool $created,
854        array $restoredPageIds
855    ): void {
856        if ( !$created ) {
857            // not interested in revdel actions
858            return;
859        }
860
861        if ( !in_array( $page->getNamespace(), PageTriageUtil::getNamespaces() ) ) {
862            // don't queue pages in namespaces where PageTriage is disabled
863            return;
864        }
865
866        $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
867        self::addToPageTriageQueue( $wikiPage->getId(), $wikiPage->getTitle() );
868    }
869}