Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 230 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
HookUtils | |
0.00% |
0 / 230 |
|
0.00% |
0 / 13 |
12882 | |
0.00% |
0 / 1 |
hasPagePropCached | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
parseRevisionParsoidHtml | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
42 | |||
featureConflictsWithGadget | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
isFeatureAvailableToUser | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
132 | |||
isFeatureEnabledForUser | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 | |||
determineUserABTestBucket | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
isAvailableForTitle | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
182 | |||
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 / 20 |
|
0.00% |
0 / 1 |
110 | |||
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 ExtensionRegistry; |
13 | use IContextSource; |
14 | use IDBAccessObject; |
15 | use LqtDispatch; |
16 | use MediaWiki\Extension\DiscussionTools\CommentParser; |
17 | use MediaWiki\Extension\DiscussionTools\CommentUtils; |
18 | use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet; |
19 | use MediaWiki\Extension\Gadgets\GadgetRepo; |
20 | use MediaWiki\Linker\LinkTarget; |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Output\OutputPage; |
23 | use MediaWiki\Page\ParserOutputAccess; |
24 | use MediaWiki\Revision\RevisionRecord; |
25 | use MediaWiki\Title\Title; |
26 | use MediaWiki\Title\TitleValue; |
27 | use MediaWiki\User\UserIdentity; |
28 | use ParserOptions; |
29 | use RequestContext; |
30 | use RuntimeException; |
31 | use Wikimedia\Assert\Assert; |
32 | use Wikimedia\Parsoid\Core\ResourceLimitExceededException; |
33 | use Wikimedia\Parsoid\Utils\DOMCompat; |
34 | use Wikimedia\Parsoid\Utils\DOMUtils; |
35 | |
36 | class HookUtils { |
37 | |
38 | public const REPLYTOOL = 'replytool'; |
39 | public const NEWTOPICTOOL = 'newtopictool'; |
40 | public const SOURCEMODETOOLBAR = 'sourcemodetoolbar'; |
41 | public const TOPICSUBSCRIPTION = 'topicsubscription'; |
42 | public const AUTOTOPICSUB = 'autotopicsub'; |
43 | public const VISUALENHANCEMENTS = 'visualenhancements'; |
44 | public const VISUALENHANCEMENTS_REPLY = 'visualenhancements_reply'; |
45 | public const VISUALENHANCEMENTS_PAGEFRAME = 'visualenhancements_pageframe'; |
46 | |
47 | /** |
48 | * @var string[] List of all sub-features. Will be used to generate: |
49 | * - Body class: ext-discussiontools-FEATURE-enabled |
50 | * - User option: discussiontools-FEATURE |
51 | */ |
52 | public const FEATURES = [ |
53 | // Can't use static:: in compile-time constants |
54 | self::REPLYTOOL, |
55 | self::NEWTOPICTOOL, |
56 | self::SOURCEMODETOOLBAR, |
57 | self::TOPICSUBSCRIPTION, |
58 | self::AUTOTOPICSUB, |
59 | self::VISUALENHANCEMENTS, |
60 | self::VISUALENHANCEMENTS_REPLY, |
61 | self::VISUALENHANCEMENTS_PAGEFRAME, |
62 | ]; |
63 | |
64 | /** |
65 | * @var string[] List of configurable sub-features, used to generate: |
66 | * - Feature override global: $wgDiscussionTools_FEATURE |
67 | */ |
68 | public const CONFIGS = [ |
69 | self::VISUALENHANCEMENTS, |
70 | self::VISUALENHANCEMENTS_REPLY, |
71 | self::VISUALENHANCEMENTS_PAGEFRAME, |
72 | ]; |
73 | |
74 | public const FEATURES_CONFLICT_WITH_GADGET = [ |
75 | self::REPLYTOOL, |
76 | ]; |
77 | |
78 | private const CACHED_PAGE_PROPS = [ |
79 | 'newsectionlink', |
80 | 'nonewsectionlink', |
81 | 'notalk', |
82 | 'archivedtalk', |
83 | ]; |
84 | private static array $propCache = []; |
85 | |
86 | /** |
87 | * Check if a title has a page prop, and use an in-memory cache to avoid extra queries |
88 | * |
89 | * @param Title $title Title |
90 | * @param string $prop Page property |
91 | * @return bool Title has page property |
92 | */ |
93 | public static function hasPagePropCached( Title $title, string $prop ): bool { |
94 | Assert::parameter( |
95 | in_array( $prop, static::CACHED_PAGE_PROPS, true ), |
96 | '$prop', |
97 | 'must be one of the cached properties' |
98 | ); |
99 | $id = $title->getArticleId(); |
100 | if ( !isset( static::$propCache[ $id ] ) ) { |
101 | $services = MediaWikiServices::getInstance(); |
102 | // Always fetch all of our properties, we need to check several of them on most requests |
103 | static::$propCache += |
104 | $services->getPageProps()->getProperties( $title, static::CACHED_PAGE_PROPS ); |
105 | } |
106 | return isset( static::$propCache[ $id ][ $prop ] ); |
107 | } |
108 | |
109 | /** |
110 | * Parse a revision by using the discussion parser on the HTML provided by Parsoid. |
111 | * |
112 | * @param RevisionRecord $revRecord |
113 | * @param string|false $updateParserCacheFor Whether the parser cache should be updated on cache miss. |
114 | * May be set to false for batch operations to avoid flooding the cache. |
115 | * Otherwise, it should be set to the name of the calling method (__METHOD__), |
116 | * so we can track what is causing parser cache writes. |
117 | * |
118 | * @return ContentThreadItemSet |
119 | * @throws ResourceLimitExceededException |
120 | */ |
121 | public static function parseRevisionParsoidHtml( |
122 | RevisionRecord $revRecord, |
123 | $updateParserCacheFor |
124 | ): ContentThreadItemSet { |
125 | $services = MediaWikiServices::getInstance(); |
126 | $mainConfig = $services->getMainConfig(); |
127 | $parserOutputAccess = $services->getParserOutputAccess(); |
128 | |
129 | // Look up the page by ID in master. If we just used $revRecord->getPage(), |
130 | // ParserOutputAccess would look it up by namespace+title in replica. |
131 | $pageRecord = $services->getPageStore()->getPageById( $revRecord->getPageId() ) ?: |
132 | $services->getPageStore()->getPageById( $revRecord->getPageId(), IDBAccessObject::READ_LATEST ); |
133 | Assert::postcondition( $pageRecord !== null, 'Revision had no page' ); |
134 | |
135 | $parserOptions = ParserOptions::newFromAnon(); |
136 | $parserOptions->setUseParsoid(); |
137 | |
138 | if ( $updateParserCacheFor ) { |
139 | // $updateParserCache contains the name of the calling method |
140 | $parserOptions->setRenderReason( $updateParserCacheFor ); |
141 | } |
142 | |
143 | $status = $parserOutputAccess->getParserOutput( |
144 | $pageRecord, |
145 | $parserOptions, |
146 | $revRecord, |
147 | // Don't flood the parser cache |
148 | $updateParserCacheFor ? 0 : ParserOutputAccess::OPT_NO_UPDATE_CACHE |
149 | ); |
150 | |
151 | if ( !$status->isOK() ) { |
152 | [ 'message' => $key, 'params' => $params ] = $status->getErrors()[0]; |
153 | // XXX: HtmlOutputRendererHelper checks for a resource limit exceeded message as well, |
154 | // maybe we shouldn't be catching that so low down. Re-throw for callers. |
155 | if ( $status->hasMessage( 'parsoid-resource-limit-exceeded' ) ) { |
156 | throw new ResourceLimitExceededException( $params[0] ?? '' ); |
157 | } |
158 | $message = wfMessage( $key, ...$params ); |
159 | throw new RuntimeException( $message->inLanguage( 'en' )->useDatabase( false )->text() ); |
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 $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 = GadgetRepo::singleton(); |
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 | // Being in the "test" group for this feature means it's enabled. This |
253 | // overrules the wiki's beta feature setting. (However, a user who's |
254 | // in the control group can still bypass this and enable the feature |
255 | // normally.) |
256 | $abtest = static::determineUserABTestBucket( $user, $feature ); |
257 | if ( $abtest === 'test' ) { |
258 | return true; |
259 | } |
260 | |
261 | // No feature-specific override found. |
262 | |
263 | if ( $dtConfig->get( 'DiscussionToolsBeta' ) ) { |
264 | $betaenabled = $optionsLookup->getOption( $user, 'discussiontools-betaenable', 0 ); |
265 | return (bool)$betaenabled; |
266 | } |
267 | |
268 | return true; |
269 | } |
270 | |
271 | /** |
272 | * Check if a DiscussionTools feature is enabled by this user |
273 | * |
274 | * @param UserIdentity $user |
275 | * @param string|null $feature Feature to check for (one of static::FEATURES) |
276 | * Null will check for any DT feature. |
277 | * @return bool |
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 | * Work out the A/B test bucket for the current user |
306 | * |
307 | * Currently this just checks whether the user is logged in, and assigns |
308 | * them to a consistent bucket based on their ID. |
309 | * |
310 | * @param UserIdentity $user |
311 | * @param string|null $feature Feature to check for (one of static::FEATURES) |
312 | * Null will check for any DT feature. |
313 | * @return string 'test' if in the test group, 'control' if in the control group, or '' if |
314 | * they're not in the test |
315 | */ |
316 | public static function determineUserABTestBucket( UserIdentity $user, ?string $feature = null ): string { |
317 | $services = MediaWikiServices::getInstance(); |
318 | $dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' ); |
319 | |
320 | $abtest = $dtConfig->get( 'DiscussionToolsABTest' ); |
321 | if ( !$abtest ) { |
322 | return ''; |
323 | } |
324 | |
325 | if ( |
326 | ( $feature ? in_array( $feature, (array)$abtest, true ) : (bool)$abtest ) && |
327 | $user->isRegistered() |
328 | ) { |
329 | return $user->getId() % 2 === 0 ? 'test' : 'control'; |
330 | } |
331 | return ''; |
332 | } |
333 | |
334 | /** |
335 | * Check if the tools are available for a given title |
336 | * |
337 | * Keep in sync with SQL conditions in persistRevisionThreadItems.php. |
338 | * |
339 | * @param Title $title |
340 | * @param string|null $feature Feature to check for (one of static::FEATURES) |
341 | * Null will check for any DT feature. |
342 | * @return bool |
343 | */ |
344 | public static function isAvailableForTitle( Title $title, ?string $feature = null ): bool { |
345 | // Only wikitext pages (e.g. not Flow boards, special pages) |
346 | if ( $title->getContentModel() !== CONTENT_MODEL_WIKITEXT ) { |
347 | return false; |
348 | } |
349 | // LiquidThreads needs a separate check, since it predates content models other than wikitext (T329423) |
350 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
351 | if ( ExtensionRegistry::getInstance()->isLoaded( 'Liquid Threads' ) && LqtDispatch::isLqtPage( $title ) ) { |
352 | return false; |
353 | } |
354 | if ( !$title->canExist() ) { |
355 | return false; |
356 | } |
357 | |
358 | // ARCHIVEDTALK/NOTALK magic words |
359 | if ( self::hasPagePropCached( $title, 'notalk' ) ) { |
360 | return false; |
361 | } |
362 | if ( |
363 | $feature === static::REPLYTOOL && |
364 | self::hasPagePropCached( $title, 'archivedtalk' ) |
365 | ) { |
366 | return false; |
367 | } |
368 | |
369 | $services = MediaWikiServices::getInstance(); |
370 | |
371 | if ( $feature === static::VISUALENHANCEMENTS ) { |
372 | $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' ); |
373 | // Visual enhancements are only enabled on talk namespaces (T325417) ... |
374 | return $title->isTalkPage() || ( |
375 | // ... or __NEWSECTIONLINK__ pages (T331635) |
376 | static::hasPagePropCached( $title, 'newsectionlink' ) && |
377 | // excluding the main namespace, unless it has been configured for signatures |
378 | ( |
379 | !$title->inNamespace( NS_MAIN ) || |
380 | $services->getNamespaceInfo()->wantSignatures( $title->getNamespace() ) |
381 | ) |
382 | ); |
383 | } |
384 | |
385 | // Check that the page supports discussions. |
386 | return ( |
387 | // Talk namespaces, and other namespaces where the signature button is shown in wikitext |
388 | // editor using $wgExtraSignatureNamespaces (T249036) |
389 | $services->getNamespaceInfo()->wantSignatures( $title->getNamespace() ) || |
390 | // Treat pages with __NEWSECTIONLINK__ as talk pages (T245890) |
391 | static::hasPagePropCached( $title, 'newsectionlink' ) |
392 | ); |
393 | } |
394 | |
395 | /** |
396 | * Check if the tool is available on a given page |
397 | * |
398 | * @param OutputPage $output |
399 | * @param string|null $feature Feature to check for (one of static::FEATURES) |
400 | * Null will check for any DT feature. |
401 | * @return bool |
402 | */ |
403 | public static function isFeatureEnabledForOutput( OutputPage $output, ?string $feature = null ): bool { |
404 | // Only show on normal page views (not history etc.), and in edit mode for previews |
405 | if ( |
406 | // Don't try to call $output->getActionName if testing for NEWTOPICTOOL as we use |
407 | // the hook onGetActionName to override the action for the tool on empty pages. |
408 | // If we tried to call it here it would set up infinite recursion (T312689) |
409 | $feature !== static::NEWTOPICTOOL && !( |
410 | in_array( $output->getActionName(), [ 'view', 'edit', 'submit' ], true ) || |
411 | // Subscriptions (specifically page-level subscriptions) are available on history pages (T345096) |
412 | ( |
413 | $output->getActionName() === 'history' && |
414 | $feature === static::TOPICSUBSCRIPTION |
415 | ) |
416 | ) |
417 | ) { |
418 | return false; |
419 | } |
420 | |
421 | $title = $output->getTitle(); |
422 | // Don't show on pages without a Title |
423 | if ( !$title ) { |
424 | return false; |
425 | } |
426 | |
427 | // Topic subscription is not available on your own talk page, as you will |
428 | // get 'edit-user-talk' notifications already. (T276996) |
429 | if ( |
430 | ( $feature === static::TOPICSUBSCRIPTION || $feature === static::AUTOTOPICSUB ) && |
431 | $title->equals( $output->getUser()->getTalkPage() ) |
432 | ) { |
433 | return false; |
434 | } |
435 | |
436 | // Subfeatures are disabled if the main feature is disabled |
437 | if ( ( |
438 | $feature === static::VISUALENHANCEMENTS_REPLY || |
439 | $feature === static::VISUALENHANCEMENTS_PAGEFRAME |
440 | ) && !self::isFeatureEnabledForOutput( $output, static::VISUALENHANCEMENTS ) ) { |
441 | return false; |
442 | } |
443 | |
444 | // ?dtenable=1 overrides all user and title checks |
445 | $queryEnable = $output->getRequest()->getRawVal( 'dtenable' ) ?: |
446 | // Extra hack for parses from API, where this parameter isn't passed to derivative requests |
447 | RequestContext::getMain()->getRequest()->getRawVal( 'dtenable' ); |
448 | |
449 | if ( $queryEnable ) { |
450 | return true; |
451 | } |
452 | |
453 | if ( $queryEnable === '0' ) { |
454 | // ?dtenable=0 forcibly disables the feature regardless of any other checks (T285578) |
455 | return false; |
456 | } |
457 | |
458 | if ( !static::isAvailableForTitle( $title, $feature ) ) { |
459 | return false; |
460 | } |
461 | |
462 | $isMobile = false; |
463 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { |
464 | $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
465 | $isMobile = $mobFrontContext->shouldDisplayMobileView(); |
466 | } |
467 | $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' ); |
468 | |
469 | if ( $isMobile ) { |
470 | return $feature === null || |
471 | $feature === static::REPLYTOOL || |
472 | $feature === static::NEWTOPICTOOL || |
473 | $feature === static::SOURCEMODETOOLBAR || |
474 | // Even though mobile ignores user preferences, TOPICSUBSCRIPTION must |
475 | // still be disabled if the user isn't registered. |
476 | ( $feature === static::TOPICSUBSCRIPTION && $output->getUser()->isNamed() ) || |
477 | $feature === static::VISUALENHANCEMENTS || |
478 | $feature === static::VISUALENHANCEMENTS_REPLY || |
479 | $feature === static::VISUALENHANCEMENTS_PAGEFRAME; |
480 | } |
481 | |
482 | return static::isFeatureEnabledForUser( $output->getUser(), $feature ); |
483 | } |
484 | |
485 | /** |
486 | * Check if the "New section" tab would be shown in a normal skin. |
487 | */ |
488 | public static function shouldShowNewSectionTab( IContextSource $context ): bool { |
489 | $title = $context->getTitle(); |
490 | $output = $context->getOutput(); |
491 | |
492 | // Match the logic in MediaWiki core (as defined in SkinTemplate::buildContentNavigationUrlsInternal): |
493 | // https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/add6d0a0e38167a710fb47fac97ff3004451494c/includes/skins/SkinTemplate.php#1317 |
494 | // * __NONEWSECTIONLINK__ is not present (OutputPage::forceHideNewSectionLink) and... |
495 | // - This is the current revision of a non-redirect in a talk namespace or... |
496 | // - __NEWSECTIONLINK__ is present (OutputPage::showNewSectionLink) |
497 | return ( |
498 | !static::hasPagePropCached( $title, 'nonewsectionlink' ) && |
499 | ( ( $title->isTalkPage() && !$title->isRedirect() && $output->isRevisionCurrent() ) || |
500 | static::hasPagePropCached( $title, 'newsectionlink' ) ) |
501 | ); |
502 | } |
503 | |
504 | /** |
505 | * Check if this page view should open the new topic tool on page load. |
506 | */ |
507 | public static function shouldOpenNewTopicTool( IContextSource $context ): bool { |
508 | $req = $context->getRequest(); |
509 | $out = $context->getOutput(); |
510 | $hasPreload = $req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) || |
511 | $req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) || |
512 | // Switching or previewing from an external tool (T316333) |
513 | $req->getCheck( 'wpTextbox1' ); |
514 | |
515 | return ( |
516 | // ?title=...&action=edit§ion=new |
517 | // ?title=...&veaction=editsource§ion=new |
518 | ( $req->getRawVal( 'action' ) === 'edit' || $req->getRawVal( 'veaction' ) === 'editsource' ) && |
519 | $req->getRawVal( 'section' ) === 'new' && |
520 | // Handle new topic with preloaded text only when requested (T269310) |
521 | ( $req->getCheck( 'dtpreload' ) || !$hasPreload ) && |
522 | // User has new topic tool enabled (and not using &dtenable=0) |
523 | static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL ) |
524 | ); |
525 | } |
526 | |
527 | /** |
528 | * Check if this page view should display the "empty state" message for empty talk pages. |
529 | */ |
530 | public static function shouldDisplayEmptyState( IContextSource $context ): bool { |
531 | $req = $context->getRequest(); |
532 | $out = $context->getOutput(); |
533 | $user = $context->getUser(); |
534 | $title = $context->getTitle(); |
535 | |
536 | $optionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
537 | |
538 | return ( |
539 | ( |
540 | // When following a red link from another page (but not when clicking the 'Edit' tab) |
541 | ( |
542 | $req->getRawVal( 'action' ) === 'edit' && $req->getRawVal( 'redlink' ) === '1' && |
543 | // …if not disabled by the user |
544 | $optionsLookup->getOption( $user, 'discussiontools-newtopictool-createpage' ) |
545 | ) || |
546 | // When the new topic tool will be opened (usually when clicking the 'Add topic' tab) |
547 | static::shouldOpenNewTopicTool( $context ) || |
548 | // In read mode (accessible for non-existent pages by clicking 'Cancel' in editor) |
549 | $req->getRawVal( 'action', 'view' ) === 'view' |
550 | ) && |
551 | // Only in talk namespaces, not including other namespaces that isAvailableForTitle() allows |
552 | $title->isTalkPage() && |
553 | // Only if the subject page or the user exists (T288319, T312560) |
554 | static::pageSubjectExists( $title ) && |
555 | // The default display will probably be more useful for links to old revisions of deleted |
556 | // pages (existing pages are already excluded in shouldShowNewSectionTab()) |
557 | $req->getIntOrNull( 'oldid' ) === null && |
558 | // Only if "New section" tab would be shown by the skin. |
559 | // If the page doesn't exist, this only happens in talk namespaces. |
560 | // If the page exists, it also considers magic words on the page. |
561 | static::shouldShowNewSectionTab( $context ) && |
562 | // User has new topic tool enabled (and not using &dtenable=0) |
563 | static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL ) |
564 | ); |
565 | } |
566 | |
567 | /** |
568 | * Return whether the corresponding subject page exists, or (if the page is a user talk page, |
569 | * excluding subpages) whether the user is registered or a valid IP address. |
570 | */ |
571 | private static function pageSubjectExists( LinkTarget $talkPage ): bool { |
572 | $services = MediaWikiServices::getInstance(); |
573 | $namespaceInfo = $services->getNamespaceInfo(); |
574 | Assert::precondition( $namespaceInfo->isTalk( $talkPage->getNamespace() ), "Page is a talk page" ); |
575 | |
576 | if ( $talkPage->inNamespace( NS_USER_TALK ) && !str_contains( $talkPage->getText(), '/' ) ) { |
577 | if ( $services->getUserNameUtils()->isIP( $talkPage->getText() ) ) { |
578 | return true; |
579 | } |
580 | $subjectUser = $services->getUserFactory()->newFromName( $talkPage->getText() ); |
581 | if ( $subjectUser && $subjectUser->isRegistered() ) { |
582 | return true; |
583 | } |
584 | return false; |
585 | } else { |
586 | $subjectPage = $namespaceInfo->getSubjectPage( $talkPage ); |
587 | return $services->getPageStore()->getPageForLink( $subjectPage )->exists(); |
588 | } |
589 | } |
590 | |
591 | /** |
592 | * Check if we should be adding automatic topic subscriptions for this user on this page. |
593 | */ |
594 | public static function shouldAddAutoSubscription( UserIdentity $user, Title $title ): bool { |
595 | // This duplicates the logic from isFeatureEnabledForOutput(), |
596 | // because we don't have access to the request or the output here. |
597 | |
598 | // Topic subscription is not available on your own talk page, as you will |
599 | // get 'edit-user-talk' notifications already. (T276996) |
600 | // (can't use User::getTalkPage() to check because this is a UserIdentity) |
601 | if ( $title->inNamespace( NS_USER_TALK ) && $title->getText() === $user->getName() ) { |
602 | return false; |
603 | } |
604 | |
605 | // Users flagged as bots shouldn't be autosubscribed. They can |
606 | // manually subscribe if it becomes relevant. (T301933) |
607 | $user = MediaWikiServices::getInstance() |
608 | ->getUserFactory() |
609 | ->newFromUserIdentity( $user ); |
610 | if ( $user->isBot() ) { |
611 | return false; |
612 | } |
613 | |
614 | // Check if the user has automatic subscriptions enabled, and the tools are enabled on the page. |
615 | return static::isAvailableForTitle( $title ) && |
616 | static::isFeatureEnabledForUser( $user, static::AUTOTOPICSUB ); |
617 | } |
618 | } |