Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 228
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
HookUtils
0.00% covered (danger)
0.00%
0 / 228
0.00% covered (danger)
0.00%
0 / 12
11130
0.00% covered (danger)
0.00%
0 / 1
 hasPagePropCached
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 parseRevisionParsoidHtml
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
42
 featureConflictsWithGadget
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 isFeatureAvailableToUser
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 isFeatureEnabledForUser
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 isAvailableForTitle
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
210
 isFeatureEnabledForOutput
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
506
 shouldShowNewSectionTab
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 shouldOpenNewTopicTool
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
110
 shouldDisplayEmptyState
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
132
 pageSubjectExists
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 shouldAddAutoSubscription
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * DiscussionTools extension hooks
4 *
5 * @file
6 * @ingroup Extensions
7 * @license MIT
8 */
9
10namespace MediaWiki\Extension\DiscussionTools\Hooks;
11
12use LqtDispatch;
13use MediaWiki\Context\IContextSource;
14use MediaWiki\Context\RequestContext;
15use MediaWiki\Extension\DiscussionTools\CommentParser;
16use MediaWiki\Extension\DiscussionTools\CommentUtils;
17use MediaWiki\Extension\DiscussionTools\ContentThreadItemSetStatus;
18use MediaWiki\Linker\LinkTarget;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Output\OutputPage;
21use MediaWiki\Page\ParserOutputAccess;
22use MediaWiki\Parser\ParserOptions;
23use MediaWiki\Registration\ExtensionRegistry;
24use MediaWiki\Revision\RevisionRecord;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27use MediaWiki\Title\TitleValue;
28use MediaWiki\User\UserIdentity;
29use Wikimedia\Assert\Assert;
30use Wikimedia\NormalizedException\NormalizedException;
31use Wikimedia\Parsoid\Core\DOMCompat;
32use Wikimedia\Parsoid\Ext\DOMUtils;
33use Wikimedia\Rdbms\IDBAccessObject;
34
35class HookUtils {
36
37    public const REPLYTOOL = 'replytool';
38    public const NEWTOPICTOOL = 'newtopictool';
39    public const SOURCEMODETOOLBAR = 'sourcemodetoolbar';
40    public const TOPICSUBSCRIPTION = 'topicsubscription';
41    public const AUTOTOPICSUB = 'autotopicsub';
42    public const VISUALENHANCEMENTS = 'visualenhancements';
43
44    /**
45     * @var string[] List of all sub-features. Will be used to generate:
46     *  - Body class: ext-discussiontools-FEATURE-enabled
47     *  - User option: discussiontools-FEATURE
48     */
49    public const FEATURES = [
50        // Can't use static:: in compile-time constants
51        self::REPLYTOOL,
52        self::NEWTOPICTOOL,
53        self::SOURCEMODETOOLBAR,
54        self::TOPICSUBSCRIPTION,
55        self::AUTOTOPICSUB,
56        self::VISUALENHANCEMENTS
57    ];
58
59    /**
60     * @var string[] List of configurable sub-features, used to generate:
61     *  - Feature override global: $wgDiscussionTools_FEATURE
62     */
63    public const CONFIGS = [
64        self::VISUALENHANCEMENTS
65    ];
66
67    public const FEATURES_CONFLICT_WITH_GADGET = [
68        self::REPLYTOOL,
69        self::TOPICSUBSCRIPTION,
70    ];
71
72    public const FEATURES_DEPENDENCIES = [
73        self::SOURCEMODETOOLBAR => [
74            self::REPLYTOOL,
75            self::NEWTOPICTOOL,
76        ],
77        self::AUTOTOPICSUB => [
78            self::TOPICSUBSCRIPTION,
79        ]
80    ];
81
82    private const CACHED_PAGE_PROPS = [
83        'newsectionlink',
84        'nonewsectionlink',
85        'notalk',
86        'archivedtalk',
87    ];
88    private static array $propCache = [];
89
90    /**
91     * Check if a title has a page prop, and use an in-memory cache to avoid extra queries
92     *
93     * @param Title $title Title
94     * @param string $prop Page property
95     * @return bool Title has page property
96     */
97    public static function hasPagePropCached( Title $title, string $prop ): bool {
98        Assert::parameter(
99            in_array( $prop, self::CACHED_PAGE_PROPS, true ),
100            '$prop',
101            'must be one of the cached properties'
102        );
103        $id = $title->getArticleId();
104        if ( !isset( self::$propCache[ $id ] ) ) {
105            $services = MediaWikiServices::getInstance();
106            // Always fetch all of our properties, we need to check several of them on most requests
107            $pagePropsPerId = $services->getPageProps()->getProperties( $title, self::CACHED_PAGE_PROPS );
108            if ( $pagePropsPerId ) {
109                self::$propCache += $pagePropsPerId;
110            } else {
111                self::$propCache[ $id ] = [];
112            }
113        }
114        return isset( self::$propCache[ $id ][ $prop ] );
115    }
116
117    /**
118     * Parse a revision by using the discussion parser on the HTML provided by Parsoid.
119     *
120     * @param RevisionRecord $revRecord
121     * @param string|false $updateParserCacheFor Whether the parser cache should be updated on cache miss.
122     *        May be set to false for batch operations to avoid flooding the cache.
123     *        Otherwise, it should be set to the name of the calling method (__METHOD__),
124     *        so we can track what is causing parser cache writes.
125     */
126    public static function parseRevisionParsoidHtml(
127        RevisionRecord $revRecord,
128        $updateParserCacheFor
129    ): ContentThreadItemSetStatus {
130        $services = MediaWikiServices::getInstance();
131        $mainConfig = $services->getMainConfig();
132        $parserOutputAccess = $services->getParserOutputAccess();
133
134        // Look up the page by ID in master. If we just used $revRecord->getPage(),
135        // ParserOutputAccess would look it up by namespace+title in replica.
136        $pageRecord = $services->getPageStore()->getPageById( $revRecord->getPageId() ) ?:
137            $services->getPageStore()->getPageById( $revRecord->getPageId(), IDBAccessObject::READ_LATEST );
138        if ( !$pageRecord ) {
139            throw new NormalizedException(
140                "PageRecord for page {page} revision {revision} not found",
141                [
142                    'page' => $revRecord->getPageId(),
143                    'revision' => $revRecord->getId(),
144                ]
145            );
146        }
147
148        $parserOptions = ParserOptions::newFromAnon();
149        $parserOptions->setUseParsoid();
150
151        if ( $updateParserCacheFor ) {
152            // $updateParserCache contains the name of the calling method
153            $parserOptions->setRenderReason( $updateParserCacheFor );
154        }
155
156        $status = $parserOutputAccess->getParserOutput(
157            $pageRecord,
158            $parserOptions,
159            $revRecord,
160            // Don't flood the parser cache
161            [ ParserOutputAccess::OPT_NO_UPDATE_CACHE => !$updateParserCacheFor ],
162        );
163
164        if ( !$status->isOK() ) {
165            // This is currently the only expected failure, make the caller handle it
166            if ( $status->hasMessage( 'parsoid-resource-limit-exceeded' ) ) {
167                return ContentThreadItemSetStatus::wrap( $status );
168            }
169            // Any other failures indicate a software bug, so throw an exception
170            throw new NormalizedException( ...Status::wrap( $status )->getPsr3MessageAndContext() );
171        }
172
173        $parserOutput = $status->getValue();
174        $html = $parserOutput->getRawText();
175
176        // Run the discussion parser on it
177        $doc = DOMUtils::parseHTML( $html );
178        $container = DOMCompat::getBody( $doc );
179
180        // Unwrap sections, so that transclusions overlapping section boundaries don't cause all
181        // comments in the sections to be treated as transcluded from another page.
182        CommentUtils::unwrapParsoidSections( $container );
183
184        /** @var CommentParser $parser */
185        $parser = $services->getService( 'DiscussionTools.CommentParser' );
186        $title = TitleValue::newFromPage( $revRecord->getPage() );
187        return ContentThreadItemSetStatus::newGood( $parser->parse( $container, $title ) );
188    }
189
190    /**
191     * @param UserIdentity $user
192     * @param string $feature Feature to check for
193     * @return bool
194     */
195    public static function featureConflictsWithGadget( UserIdentity $user, string $feature ) {
196        $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()
197            ->makeConfig( 'discussiontools' );
198        $gadgetName = $dtConfig->get( 'DiscussionToolsConflictingGadgetName' );
199        if ( !$gadgetName ) {
200            return false;
201        }
202
203        if ( !in_array( $feature, static::FEATURES_CONFLICT_WITH_GADGET, true ) ) {
204            return false;
205        }
206
207        $extensionRegistry = ExtensionRegistry::getInstance();
208        if ( $extensionRegistry->isLoaded( 'Gadgets' ) ) {
209            $gadgetsRepo = MediaWikiServices::getInstance()->getService( 'GadgetsRepo' );
210            $match = array_search( $gadgetName, $gadgetsRepo->getGadgetIds(), true );
211            if ( $match !== false ) {
212                try {
213                    return $gadgetsRepo->getGadget( $gadgetName )
214                        ->isEnabled( $user );
215                } catch ( \InvalidArgumentException ) {
216                    return false;
217                }
218            }
219        }
220        return false;
221    }
222
223    /**
224     * Check if a DiscussionTools feature is available to this user
225     *
226     * @param UserIdentity $user
227     * @param string|null $feature Feature to check for (one of static::FEATURES)
228     *  Null will check for any DT feature.
229     */
230    public static function isFeatureAvailableToUser( UserIdentity $user, ?string $feature = null ): bool {
231        $services = MediaWikiServices::getInstance();
232        $dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
233
234        $userIdentityUtils = $services->getUserIdentityUtils();
235        if (
236            ( $feature === static::TOPICSUBSCRIPTION || $feature === static::AUTOTOPICSUB ) &&
237            // Users must be logged in to use topic subscription, and Echo must be installed (T322498)
238            ( !$user->isRegistered() || $userIdentityUtils->isTemp( $user ) ||
239                !ExtensionRegistry::getInstance()->isLoaded( 'Echo' ) )
240        ) {
241            return false;
242        }
243
244        $optionsLookup = $services->getUserOptionsLookup();
245
246        if ( $feature ) {
247            // Feature-specific override
248            if ( !in_array( $feature, static::CONFIGS, true ) ) {
249                // Feature is not configurable, always available
250                return true;
251            }
252            if ( $dtConfig->get( 'DiscussionTools_' . $feature ) !== 'default' ) {
253                // Feature setting can be 'available' or 'unavailable', overriding any BetaFeatures settings
254                return $dtConfig->get( 'DiscussionTools_' . $feature ) === 'available';
255            }
256        } else {
257            // Some features are always available, so if no feature is
258            // specified (i.e. checking for any feature), always return true.
259            return true;
260        }
261
262        // No feature-specific override found.
263
264        if ( $dtConfig->get( 'DiscussionToolsBeta' ) ) {
265            $betaenabled = $optionsLookup->getOption( $user, 'discussiontools-betaenable', 0 );
266            return (bool)$betaenabled;
267        }
268
269        return true;
270    }
271
272    /**
273     * Check if a DiscussionTools feature is enabled by this user
274     *
275     * @param UserIdentity $user
276     * @param string|null $feature Feature to check for (one of static::FEATURES)
277     *  Null will check for any DT feature.
278     */
279    public static function isFeatureEnabledForUser( UserIdentity $user, ?string $feature = null ): bool {
280        if ( !static::isFeatureAvailableToUser( $user, $feature ) ) {
281            return false;
282        }
283        $services = MediaWikiServices::getInstance();
284        $optionsLookup = $services->getUserOptionsLookup();
285        if ( $feature ) {
286            if ( static::featureConflictsWithGadget( $user, $feature ) ) {
287                return false;
288            }
289            // Check for a specific feature
290            $enabled = $optionsLookup->getOption( $user, 'discussiontools-' . $feature );
291            // `null` means there is no user option for this feature, so it must be enabled
292            return $enabled === null ? true : $enabled;
293        } else {
294            // Check for any feature
295            foreach ( static::FEATURES as $feat ) {
296                if ( $optionsLookup->getOption( $user, 'discussiontools-' . $feat ) ) {
297                    return true;
298                }
299            }
300            return false;
301        }
302    }
303
304    /**
305     * Check if the tools are available for a given title
306     *
307     * Keep in sync with SQL conditions in persistRevisionThreadItems.php.
308     *
309     * @param Title $title
310     * @param string|null $feature Feature to check for (one of static::FEATURES)
311     *  Null will check for any DT feature.
312     */
313    public static function isAvailableForTitle( Title $title, ?string $feature = null ): bool {
314        // Only wikitext pages (e.g. not Flow boards, special pages)
315        if ( $title->getContentModel() !== CONTENT_MODEL_WIKITEXT ) {
316            return false;
317        }
318        // LiquidThreads needs a separate check, since it predates content models other than wikitext (T329423)
319        // @phan-suppress-next-line PhanUndeclaredClassMethod
320        if ( ExtensionRegistry::getInstance()->isLoaded( 'Liquid Threads' ) && LqtDispatch::isLqtPage( $title ) ) {
321            return false;
322        }
323        if ( !$title->canExist() ) {
324            return false;
325        }
326
327        // ARCHIVEDTALK/NOTALK magic words
328        if ( static::hasPagePropCached( $title, 'notalk' ) ) {
329            return false;
330        }
331        if (
332            $feature === static::REPLYTOOL &&
333            static::hasPagePropCached( $title, 'archivedtalk' )
334        ) {
335            return false;
336        }
337
338        $services = MediaWikiServices::getInstance();
339
340        if ( $feature === static::VISUALENHANCEMENTS ) {
341            $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
342            // Visual enhancements are only enabled on talk namespaces (T325417) ...
343            return $title->isTalkPage() || (
344                // ... or __NEWSECTIONLINK__ (T331635) or __ARCHIVEDTALK__ (T374198) pages
345                (
346                    static::hasPagePropCached( $title, 'newsectionlink' ) ||
347                    static::hasPagePropCached( $title, 'archivedtalk' )
348                ) &&
349                // excluding the main namespace, unless it has been configured for signatures
350                (
351                    !$title->inNamespace( NS_MAIN ) ||
352                    $services->getNamespaceInfo()->wantSignatures( $title->getNamespace() )
353                )
354            );
355        }
356
357        // Check that the page supports discussions.
358        return (
359            // Talk namespaces, and other namespaces where the signature button is shown in wikitext
360            // editor using $wgExtraSignatureNamespaces (T249036)
361            $services->getNamespaceInfo()->wantSignatures( $title->getNamespace() ) ||
362            // Treat pages with __NEWSECTIONLINK__ as talk pages (T245890)
363            static::hasPagePropCached( $title, 'newsectionlink' )
364        );
365    }
366
367    /**
368     * Check if the tool is available on a given page
369     *
370     * @param OutputPage $output
371     * @param string|null $feature Feature to check for (one of static::FEATURES)
372     *  Null will check for any DT feature.
373     */
374    public static function isFeatureEnabledForOutput( OutputPage $output, ?string $feature = null ): bool {
375        // Only show on normal page views (not history etc.), and in edit mode for previews
376        if (
377            // Don't try to call $output->getActionName if testing for NEWTOPICTOOL as we use
378            // the hook onGetActionName to override the action for the tool on empty pages.
379            // If we tried to call it here it would set up infinite recursion (T312689)
380            $feature !== static::NEWTOPICTOOL && !(
381                in_array( $output->getActionName(), [ 'view', 'edit', 'submit' ], true ) ||
382                // Subscriptions (specifically page-level subscriptions) are available on history pages (T345096)
383                (
384                    $output->getActionName() === 'history' &&
385                    $feature === static::TOPICSUBSCRIPTION
386                )
387            )
388        ) {
389            return false;
390        }
391
392        $title = $output->getTitle();
393        // Don't show on pages without a Title
394        if ( !$title ) {
395            return false;
396        }
397
398        // Topic subscription is not available on your own talk page, as you will
399        // get 'edit-user-talk' notifications already. (T276996)
400        if (
401            ( $feature === static::TOPICSUBSCRIPTION || $feature === static::AUTOTOPICSUB ) &&
402            $title->equals( $output->getUser()->getTalkPage() )
403        ) {
404            return false;
405        }
406
407        // ?dtenable=1 overrides all user and title checks
408        $queryEnable = $output->getRequest()->getRawVal( 'dtenable' ) ?:
409            // Extra hack for parses from API, where this parameter isn't passed to derivative requests
410            RequestContext::getMain()->getRequest()->getRawVal( 'dtenable' );
411
412        if ( $queryEnable ) {
413            return true;
414        }
415
416        if ( $queryEnable === '0' ) {
417            // ?dtenable=0 forcibly disables the feature regardless of any other checks (T285578)
418            return false;
419        }
420
421        if ( !static::isAvailableForTitle( $title, $feature ) ) {
422            return false;
423        }
424
425        $isMobile = false;
426        if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) {
427            $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' );
428            $isMobile = $mobFrontContext->shouldDisplayMobileView();
429        }
430        $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
431
432        if ( $isMobile ) {
433            return $feature === null ||
434                $feature === static::REPLYTOOL ||
435                $feature === static::NEWTOPICTOOL ||
436                $feature === static::SOURCEMODETOOLBAR ||
437                // Even though mobile ignores user preferences, TOPICSUBSCRIPTION must
438                // still be disabled if the user isn't registered, or
439                // if Echo is disabled
440                (
441                    $feature === static::TOPICSUBSCRIPTION &&
442                    $output->getUser()->isNamed() &&
443                    ExtensionRegistry::getInstance()->isLoaded( 'Echo' )
444                ) ||
445                $feature === static::VISUALENHANCEMENTS;
446        }
447
448        return static::isFeatureEnabledForUser( $output->getUser(), $feature );
449    }
450
451    /**
452     * Check if the "New section" tab would be shown in a normal skin.
453     */
454    public static function shouldShowNewSectionTab( IContextSource $context ): bool {
455        $title = $context->getTitle();
456        $output = $context->getOutput();
457
458        // Match the logic in MediaWiki core (as defined in SkinTemplate::buildContentNavigationUrlsInternal):
459        // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/add6d0a0e38167a710fb47fac97ff3004451494c/includes/skins/SkinTemplate.php#1317
460        // * __NONEWSECTIONLINK__ is not present (OutputPage::forceHideNewSectionLink) and...
461        //   - This is the current revision of a non-redirect in a talk namespace or...
462        //   - __NEWSECTIONLINK__ is present (OutputPage::showNewSectionLink)
463        return (
464            !static::hasPagePropCached( $title, 'nonewsectionlink' ) &&
465            ( ( $title->isTalkPage() && !$title->isRedirect() && $output->isRevisionCurrent() ) ||
466                static::hasPagePropCached( $title, 'newsectionlink' ) )
467        );
468    }
469
470    /**
471     * Check if this page view should open the new topic tool on page load.
472     */
473    public static function shouldOpenNewTopicTool( IContextSource $context ): bool {
474        $req = $context->getRequest();
475        $out = $context->getOutput();
476        $hasPreload = $req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) ||
477            $req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) ||
478            // Switching or previewing from an external tool (T316333)
479            $req->getCheck( 'wpTextbox1' );
480
481        return (
482            // ?title=...&action=edit&section=new
483            // ?title=...&veaction=editsource&section=new
484            ( $req->getRawVal( 'action' ) === 'edit' || $req->getRawVal( 'veaction' ) === 'editsource' ) &&
485            $req->getRawVal( 'section' ) === 'new' &&
486            // Handle new topic with preloaded text only when requested (T269310)
487            ( $req->getCheck( 'dtpreload' ) || !$hasPreload ) &&
488            // User has new topic tool enabled (and not using &dtenable=0)
489            static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL )
490        );
491    }
492
493    /**
494     * Check if this page view should display the "empty state" message for empty talk pages.
495     */
496    public static function shouldDisplayEmptyState( IContextSource $context ): bool {
497        $req = $context->getRequest();
498        $out = $context->getOutput();
499        $user = $context->getUser();
500        $title = $context->getTitle();
501
502        $optionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
503
504        return (
505            (
506                // When following a red link from another page (but not when clicking the 'Edit' tab)
507                (
508                    $req->getRawVal( 'action' ) === 'edit' && $req->getRawVal( 'redlink' ) === '1' &&
509                    // …if not disabled by the user
510                    $optionsLookup->getOption( $user, 'discussiontools-newtopictool-createpage' )
511                ) ||
512                // When the new topic tool will be opened (usually when clicking the 'Add topic' tab)
513                static::shouldOpenNewTopicTool( $context ) ||
514                // In read mode (accessible for non-existent pages by clicking 'Cancel' in editor)
515                ( $req->getRawVal( 'action' ) ?? 'view' ) === 'view'
516            ) &&
517            // Only in talk namespaces, not including other namespaces that isAvailableForTitle() allows
518            $title->isTalkPage() &&
519            // Only if the subject page or the user exists (T288319, T312560)
520            ( $title->exists() || static::pageSubjectExists( $title ) ) &&
521            // The default display will probably be more useful for links to old revisions of deleted
522            // pages (existing pages are already excluded in shouldShowNewSectionTab())
523            $req->getIntOrNull( 'oldid' ) === null &&
524            // Only if "New section" tab would be shown by the skin.
525            // If the page doesn't exist, this only happens in talk namespaces.
526            // If the page exists, it also considers magic words on the page.
527            static::shouldShowNewSectionTab( $context ) &&
528            // User has new topic tool enabled (and not using &dtenable=0)
529            static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL )
530        );
531    }
532
533    /**
534     * Return whether the corresponding subject page exists, or (if the page is a user talk page,
535     * excluding subpages) whether the user is registered or a valid IP address.
536     */
537    private static function pageSubjectExists( LinkTarget $talkPage ): bool {
538        $services = MediaWikiServices::getInstance();
539        $namespaceInfo = $services->getNamespaceInfo();
540        Assert::precondition( $namespaceInfo->isTalk( $talkPage->getNamespace() ), "Page is a talk page" );
541
542        if ( $talkPage->inNamespace( NS_USER_TALK ) && !str_contains( $talkPage->getText(), '/' ) ) {
543            if ( $services->getUserNameUtils()->isIP( $talkPage->getText() ) ) {
544                return true;
545            }
546            $subjectUser = $services->getUserFactory()->newFromName( $talkPage->getText() );
547            if ( $subjectUser && $subjectUser->isRegistered() ) {
548                return true;
549            }
550            return false;
551        } else {
552            $subjectPage = $namespaceInfo->getSubjectPage( $talkPage );
553            return $services->getPageStore()->getPageForLink( $subjectPage )->exists();
554        }
555    }
556
557    /**
558     * Check if we should be adding automatic topic subscriptions for this user on this page.
559     */
560    public static function shouldAddAutoSubscription( UserIdentity $user, Title $title ): bool {
561        // This duplicates the logic from isFeatureEnabledForOutput(),
562        // because we don't have access to the request or the output here.
563
564        // Topic subscription is not available on your own talk page, as you will
565        // get 'edit-user-talk' notifications already. (T276996)
566        // (can't use User::getTalkPage() to check because this is a UserIdentity)
567        if ( $title->inNamespace( NS_USER_TALK ) && $title->getText() === $user->getName() ) {
568            return false;
569        }
570
571        // Users flagged as bots shouldn't be autosubscribed. They can
572        // manually subscribe if it becomes relevant. (T301933)
573        $user = MediaWikiServices::getInstance()
574            ->getUserFactory()
575            ->newFromUserIdentity( $user );
576        if ( $user->isBot() ) {
577            return false;
578        }
579
580        // Check if the user has automatic subscriptions enabled, and the tools are enabled on the page.
581        return static::isAvailableForTitle( $title ) &&
582            static::isFeatureEnabledForUser( $user, static::AUTOTOPICSUB );
583    }
584}