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