Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 228 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
| HookUtils | |
0.00% |
0 / 228 |
|
0.00% |
0 / 12 |
11130 | |
0.00% |
0 / 1 |
| hasPagePropCached | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| parseRevisionParsoidHtml | |
0.00% |
0 / 35 |
|
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 / 42 |
|
0.00% |
0 / 1 |
506 | |||
| 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\Core\DOMCompat; |
| 32 | use Wikimedia\Parsoid\Ext\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 | |
| 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§ion=new |
| 483 | // ?title=...&veaction=editsource§ion=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 | } |