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