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