Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
96.45% |
190 / 197 |
|
50.00% |
6 / 12 |
CRAP | |
0.00% |
0 / 1 |
| CommentParser | |
96.45% |
190 / 197 |
|
50.00% |
6 / 12 |
53 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| preprocess | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| preprocessUnsafe | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| finalize | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
| preprocessInternal | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| doSectionLinks | |
97.62% |
41 / 42 |
|
0.00% |
0 / 1 |
8 | |||
| makeSectionLink | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
| doWikiLinks | |
98.46% |
64 / 65 |
|
0.00% |
0 / 1 |
17 | |||
| addLinkMarker | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
| addPageLink | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
9 | |||
| addFileLink | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| flushLinkBatches | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\CommentFormatter; |
| 4 | |
| 5 | use MediaWiki\Cache\LinkBatch; |
| 6 | use MediaWiki\Cache\LinkBatchFactory; |
| 7 | use MediaWiki\Cache\LinkCache; |
| 8 | use MediaWiki\FileRepo\File\File; |
| 9 | use MediaWiki\FileRepo\RepoGroup; |
| 10 | use MediaWiki\HookContainer\HookContainer; |
| 11 | use MediaWiki\HookContainer\HookRunner; |
| 12 | use MediaWiki\Html\Html; |
| 13 | use MediaWiki\Language\Language; |
| 14 | use MediaWiki\Linker\Linker; |
| 15 | use MediaWiki\Linker\LinkRenderer; |
| 16 | use MediaWiki\Linker\LinkTarget; |
| 17 | use MediaWiki\Parser\Parser; |
| 18 | use MediaWiki\Parser\Sanitizer; |
| 19 | use MediaWiki\SpecialPage\SpecialPage; |
| 20 | use MediaWiki\Title\MalformedTitleException; |
| 21 | use MediaWiki\Title\NamespaceInfo; |
| 22 | use MediaWiki\Title\Title; |
| 23 | use MediaWiki\Title\TitleParser; |
| 24 | use MediaWiki\Title\TitleValue; |
| 25 | use MediaWiki\WikiMap\WikiMap; |
| 26 | use Wikimedia\HtmlArmor\HtmlArmor; |
| 27 | use Wikimedia\StringUtils\StringUtils; |
| 28 | |
| 29 | /** |
| 30 | * The text processing backend for CommentFormatter. |
| 31 | * |
| 32 | * CommentParser objects should be discarded after the comment batch is |
| 33 | * complete, in order to reduce memory usage. |
| 34 | * |
| 35 | * @internal |
| 36 | */ |
| 37 | class CommentParser { |
| 38 | /** @var LinkRenderer */ |
| 39 | private $linkRenderer; |
| 40 | /** @var LinkBatchFactory */ |
| 41 | private $linkBatchFactory; |
| 42 | /** @var RepoGroup */ |
| 43 | private $repoGroup; |
| 44 | /** @var Language */ |
| 45 | private $userLang; |
| 46 | /** @var Language */ |
| 47 | private $contLang; |
| 48 | /** @var TitleParser */ |
| 49 | private $titleParser; |
| 50 | /** @var NamespaceInfo */ |
| 51 | private $namespaceInfo; |
| 52 | /** @var HookRunner */ |
| 53 | private $hookRunner; |
| 54 | /** @var LinkCache */ |
| 55 | private $linkCache; |
| 56 | |
| 57 | /** @var callable[] */ |
| 58 | private $links = []; |
| 59 | /** @var LinkBatch|null */ |
| 60 | private $linkBatch; |
| 61 | |
| 62 | /** @var array Input to RepoGroup::findFiles() */ |
| 63 | private $fileBatch; |
| 64 | /** @var File[] Resolved File objects indexed by DB key */ |
| 65 | private $files = []; |
| 66 | |
| 67 | /** @var int The maximum number of digits in a marker ID */ |
| 68 | private const MAX_ID_SIZE = 7; |
| 69 | /** @var string Prefix for marker. ' and " included to break attributes (T355538) */ |
| 70 | private const MARKER_PREFIX = "\x1B\"'"; |
| 71 | |
| 72 | /** |
| 73 | * @param LinkRenderer $linkRenderer |
| 74 | * @param LinkBatchFactory $linkBatchFactory |
| 75 | * @param LinkCache $linkCache |
| 76 | * @param RepoGroup $repoGroup |
| 77 | * @param Language $userLang |
| 78 | * @param Language $contLang |
| 79 | * @param TitleParser $titleParser |
| 80 | * @param NamespaceInfo $namespaceInfo |
| 81 | * @param HookContainer $hookContainer |
| 82 | */ |
| 83 | public function __construct( |
| 84 | LinkRenderer $linkRenderer, |
| 85 | LinkBatchFactory $linkBatchFactory, |
| 86 | LinkCache $linkCache, |
| 87 | RepoGroup $repoGroup, |
| 88 | Language $userLang, |
| 89 | Language $contLang, |
| 90 | TitleParser $titleParser, |
| 91 | NamespaceInfo $namespaceInfo, |
| 92 | HookContainer $hookContainer |
| 93 | ) { |
| 94 | $this->linkRenderer = $linkRenderer; |
| 95 | $this->linkBatchFactory = $linkBatchFactory; |
| 96 | $this->linkCache = $linkCache; |
| 97 | $this->repoGroup = $repoGroup; |
| 98 | $this->userLang = $userLang; |
| 99 | $this->contLang = $contLang; |
| 100 | $this->titleParser = $titleParser; |
| 101 | $this->namespaceInfo = $namespaceInfo; |
| 102 | $this->hookRunner = new HookRunner( $hookContainer ); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Convert a comment to HTML, but replace links with markers which are |
| 107 | * resolved later. |
| 108 | * |
| 109 | * @param string $comment |
| 110 | * @param LinkTarget|null $selfLinkTarget |
| 111 | * @param bool $samePage |
| 112 | * @param string|false|null $wikiId |
| 113 | * @param bool $enableSectionLinks |
| 114 | * @return string |
| 115 | */ |
| 116 | public function preprocess( string $comment, ?LinkTarget $selfLinkTarget = null, |
| 117 | $samePage = false, $wikiId = false, $enableSectionLinks = true |
| 118 | ) { |
| 119 | return $this->preprocessInternal( $comment, false, $selfLinkTarget, |
| 120 | $samePage, $wikiId, $enableSectionLinks ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Convert a comment in pseudo-HTML format to HTML, replacing links with markers. |
| 125 | * |
| 126 | * @param string $comment |
| 127 | * @param LinkTarget|null $selfLinkTarget |
| 128 | * @param bool $samePage |
| 129 | * @param string|false|null $wikiId |
| 130 | * @param bool $enableSectionLinks |
| 131 | * @return string |
| 132 | */ |
| 133 | public function preprocessUnsafe( $comment, ?LinkTarget $selfLinkTarget = null, |
| 134 | $samePage = false, $wikiId = false, $enableSectionLinks = true |
| 135 | ) { |
| 136 | return $this->preprocessInternal( $comment, true, $selfLinkTarget, |
| 137 | $samePage, $wikiId, $enableSectionLinks ); |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * Execute pending batch queries and replace markers in the specified |
| 142 | * string(s) with actual links. |
| 143 | * |
| 144 | * @param string|string[] $comments |
| 145 | * @return string|string[] |
| 146 | */ |
| 147 | public function finalize( $comments ) { |
| 148 | $this->flushLinkBatches(); |
| 149 | return preg_replace_callback( |
| 150 | '/' . self::MARKER_PREFIX . '([0-9]{' . self::MAX_ID_SIZE . '})/', |
| 151 | function ( $m ) { |
| 152 | $callback = $this->links[(int)$m[1]] ?? null; |
| 153 | if ( $callback ) { |
| 154 | return $callback(); |
| 155 | } else { |
| 156 | return '<!-- MISSING -->'; |
| 157 | } |
| 158 | }, |
| 159 | $comments |
| 160 | ); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * @param string $comment |
| 165 | * @param bool $unsafe |
| 166 | * @param LinkTarget|null $selfLinkTarget |
| 167 | * @param bool $samePage |
| 168 | * @param string|false|null $wikiId |
| 169 | * @param bool $enableSectionLinks |
| 170 | * @return string |
| 171 | */ |
| 172 | private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId, |
| 173 | $enableSectionLinks |
| 174 | ) { |
| 175 | // Sanitize text a bit |
| 176 | // \x1b needs to be stripped because it is used for link markers |
| 177 | $comment = strtr( $comment, "\n\x1b", " " ); |
| 178 | // Allow HTML entities (for T15815) |
| 179 | if ( !$unsafe ) { |
| 180 | $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); |
| 181 | } |
| 182 | if ( $enableSectionLinks ) { |
| 183 | $comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId ); |
| 184 | } |
| 185 | return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId ); |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Converts C-style comments in edit summaries into section links. |
| 190 | * |
| 191 | * Too many things are called "comments", so these are mostly now called |
| 192 | * section links rather than autocomments. |
| 193 | * |
| 194 | * We look for all comments, match any text before and after the comment, |
| 195 | * add a separator where needed and format the comment itself with CSS. |
| 196 | * |
| 197 | * @param string $comment Comment text |
| 198 | * @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections |
| 199 | * @param bool $samePage Whether section links should refer to local page |
| 200 | * @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki), |
| 201 | * as used by WikiMap. |
| 202 | * @return string Preprocessed comment |
| 203 | */ |
| 204 | private function doSectionLinks( |
| 205 | $comment, |
| 206 | $selfLinkTarget = null, |
| 207 | $samePage = false, |
| 208 | $wikiId = false |
| 209 | ) { |
| 210 | $comment = preg_replace_callback( |
| 211 | // To detect the presence of content before or after the |
| 212 | // auto-comment, we use capturing groups inside optional zero-width |
| 213 | // assertions. But older versions of PCRE can not directly make |
| 214 | // zero-width assertions optional, so wrap them in a non-capturing |
| 215 | // group. |
| 216 | '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!', |
| 217 | function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) { |
| 218 | $pre = ( $match[1] ?? '' ) !== ''; |
| 219 | $section = ( $match[2] ?? '' ); |
| 220 | $post = ( $match[3] ?? '' ) !== ''; |
| 221 | $comment = null; |
| 222 | |
| 223 | $this->hookRunner->onFormatAutocomments( |
| 224 | $comment, $pre, $section, $post, |
| 225 | Title::castFromLinkTarget( $selfLinkTarget ), |
| 226 | $samePage, |
| 227 | $wikiId ); |
| 228 | if ( $comment !== null ) { |
| 229 | return $comment; |
| 230 | } |
| 231 | |
| 232 | // HTML has already been escaped in preprocessInternal(), so treat this as HTML from this point |
| 233 | $parsedSection = $section; |
| 234 | |
| 235 | if ( $selfLinkTarget ) { |
| 236 | $decodedSection = substr( Parser::guessSectionNameFromStrippedText( |
| 237 | // Remove links that a user may have manually put in the autosummary |
| 238 | // This could be improved by copying as much of Parser::stripSectionName as desired. |
| 239 | str_replace( [ '[[:', '[[', ']]' ], '', $section ) |
| 240 | ), 1 ); |
| 241 | if ( $decodedSection !== '' ) { |
| 242 | if ( $samePage ) { |
| 243 | $targetWithSection = new TitleValue( NS_MAIN, '', $decodedSection ); |
| 244 | } else { |
| 245 | $targetWithSection = $selfLinkTarget->createFragmentTarget( $decodedSection ); |
| 246 | } |
| 247 | $parsedSection = $this->makeSectionLink( |
| 248 | $targetWithSection, |
| 249 | $this->userLang->getArrow() . |
| 250 | Html::rawElement( 'bdi', [ 'dir' => $this->userLang->getDir() ], $parsedSection ), |
| 251 | $wikiId, |
| 252 | $selfLinkTarget |
| 253 | ); |
| 254 | } |
| 255 | } |
| 256 | if ( $post ) { |
| 257 | # autocomment $postsep written summary (/* section */ summary) |
| 258 | $parsedSection .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped(); |
| 259 | } |
| 260 | if ( $parsedSection ) { |
| 261 | $parsedSection = Html::rawElement( 'span', [ 'class' => 'autocomment' ], $parsedSection ); |
| 262 | } |
| 263 | if ( $pre ) { |
| 264 | # written summary $presep autocomment (summary /* section */) |
| 265 | $parsedSection = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped() |
| 266 | . $parsedSection; |
| 267 | } |
| 268 | |
| 269 | // Make sure any brackets (which the user could have input in the edit summary) |
| 270 | // in the generated autocomment HTML don't trigger additional link processing (T406664). |
| 271 | return str_replace( [ '[', ']' ], [ '[', ']' ], $parsedSection ); |
| 272 | }, |
| 273 | $comment |
| 274 | ); |
| 275 | return $comment; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Make a section link. These don't need to go into the LinkBatch, since |
| 280 | * the link class does not depend on whether the link is known. |
| 281 | * |
| 282 | * @param LinkTarget $target |
| 283 | * @param string $text |
| 284 | * @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki), |
| 285 | * as used by WikiMap. |
| 286 | * @param LinkTarget $contextTitle |
| 287 | * |
| 288 | * @return string HTML link |
| 289 | */ |
| 290 | private function makeSectionLink( |
| 291 | LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle |
| 292 | ) { |
| 293 | if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) { |
| 294 | return $this->linkRenderer->makeExternalLink( |
| 295 | WikiMap::getForeignURL( |
| 296 | $wikiId, |
| 297 | $target->getNamespace() === 0 |
| 298 | ? $target->getDBkey() |
| 299 | : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) . |
| 300 | ':' . $target->getDBkey(), |
| 301 | $target->getFragment() |
| 302 | ), |
| 303 | new HtmlArmor( $text ), // Already escaped |
| 304 | $contextTitle |
| 305 | ); |
| 306 | } |
| 307 | return $this->linkRenderer->makePreloadedLink( $target, new HtmlArmor( $text ), '' ); |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Formats wiki links and media links in text; all other wiki formatting |
| 312 | * is ignored |
| 313 | * |
| 314 | * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser |
| 315 | * |
| 316 | * @param string $comment Text to format links in. WARNING! Since the output of this |
| 317 | * function is html, $comment must be sanitized for use as html. You probably want |
| 318 | * to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling |
| 319 | * this function. |
| 320 | * as used by WikiMap. |
| 321 | * @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections |
| 322 | * @param bool $samePage Whether section links should refer to local page |
| 323 | * @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki), |
| 324 | * as used by WikiMap. |
| 325 | * |
| 326 | * @return string HTML |
| 327 | */ |
| 328 | private function doWikiLinks( $comment, $selfLinkTarget = null, $samePage = false, $wikiId = false ) { |
| 329 | return preg_replace_callback( |
| 330 | '/ |
| 331 | \[\[ |
| 332 | \s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking |
| 333 | :? # ignore optional leading colon |
| 334 | ([^[\]|]+) # 1. link target; page names cannot include [, ] or | |
| 335 | (?:\| |
| 336 | # 2. link text |
| 337 | # Stop matching at ]] without relying on backtracking. |
| 338 | ((?:]?[^\]])*+) |
| 339 | )? |
| 340 | \]\] |
| 341 | ([^[]*) # 3. link trail (the text up until the next link) |
| 342 | /x', |
| 343 | function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) { |
| 344 | $medians = '(?:'; |
| 345 | $medians .= preg_quote( |
| 346 | $this->namespaceInfo->getCanonicalName( NS_MEDIA ), '/' ); |
| 347 | $medians .= '|'; |
| 348 | $medians .= preg_quote( |
| 349 | $this->contLang->getNsText( NS_MEDIA ), |
| 350 | '/' |
| 351 | ) . '):'; |
| 352 | |
| 353 | $comment = $match[0]; |
| 354 | |
| 355 | // Fix up urlencoded title texts (copied from Parser::replaceInternalLinks) |
| 356 | if ( str_contains( $match[1], '%' ) ) { |
| 357 | $match[1] = strtr( |
| 358 | rawurldecode( $match[1] ), |
| 359 | [ '<' => '<', '>' => '>' ] |
| 360 | ); |
| 361 | } |
| 362 | |
| 363 | // Handle link renaming [[foo|text]] will show link as "text" |
| 364 | if ( $match[2] != "" ) { |
| 365 | $text = $match[2]; |
| 366 | } else { |
| 367 | $text = $match[1]; |
| 368 | } |
| 369 | $submatch = []; |
| 370 | $linkMarker = null; |
| 371 | if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) { |
| 372 | // Media link; trail not supported. |
| 373 | $linkRegexp = '/\[\[(.*?)\]\]/'; |
| 374 | $linkTarget = $this->titleParser->makeTitleValueSafe( NS_FILE, $submatch[1] ); |
| 375 | if ( $linkTarget ) { |
| 376 | $linkMarker = $this->addFileLink( $linkTarget, $text ); |
| 377 | } |
| 378 | } else { |
| 379 | // Other kind of link |
| 380 | // Make sure its target is non-empty |
| 381 | if ( isset( $match[1][0] ) && $match[1][0] == ':' ) { |
| 382 | $match[1] = substr( $match[1], 1 ); |
| 383 | } |
| 384 | if ( $match[1] !== false && $match[1] !== null && $match[1] !== '' ) { |
| 385 | if ( preg_match( |
| 386 | $this->contLang->linkTrail(), |
| 387 | $match[3], |
| 388 | $submatch |
| 389 | ) ) { |
| 390 | $trail = $submatch[1]; |
| 391 | } else { |
| 392 | $trail = ""; |
| 393 | } |
| 394 | $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/'; |
| 395 | [ $inside, $trail ] = Linker::splitTrail( $trail ); |
| 396 | |
| 397 | $linkText = $text; |
| 398 | $linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText ); |
| 399 | |
| 400 | try { |
| 401 | $target = $this->titleParser->parseTitle( $linkTarget ); |
| 402 | |
| 403 | if ( $target->getText() == '' && !$target->isExternal() |
| 404 | && !$samePage && $selfLinkTarget |
| 405 | ) { |
| 406 | $target = $selfLinkTarget->createFragmentTarget( $target->getFragment() ); |
| 407 | } |
| 408 | |
| 409 | // We should deprecate `null` as a valid value for |
| 410 | // $selfLinkTarget to ensure that we can use it as |
| 411 | // the title context for the external link. |
| 412 | // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle |
| 413 | global $wgTitle; |
| 414 | $linkMarker = $this->addPageLink( |
| 415 | $target, |
| 416 | $linkText . $inside, |
| 417 | $wikiId, |
| 418 | $selfLinkTarget ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' ) |
| 419 | ); |
| 420 | $linkMarker .= $trail; |
| 421 | } catch ( MalformedTitleException ) { |
| 422 | // Fall through |
| 423 | } |
| 424 | } |
| 425 | } |
| 426 | if ( $linkMarker ) { |
| 427 | // If the link is still valid, go ahead and replace it in! |
| 428 | $comment = preg_replace( |
| 429 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable linkRegexp set when used |
| 430 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal linkRegexp set when used |
| 431 | $linkRegexp, |
| 432 | StringUtils::escapeRegexReplacement( $linkMarker ), |
| 433 | $comment, |
| 434 | 1 |
| 435 | ); |
| 436 | } |
| 437 | |
| 438 | return $comment; |
| 439 | }, |
| 440 | $comment |
| 441 | ); |
| 442 | } |
| 443 | |
| 444 | /** |
| 445 | * Add a deferred link to the list and return its marker. |
| 446 | * |
| 447 | * @param callable $callback |
| 448 | * @return string |
| 449 | */ |
| 450 | private function addLinkMarker( $callback ) { |
| 451 | $nextId = count( $this->links ); |
| 452 | if ( strlen( (string)$nextId ) > self::MAX_ID_SIZE ) { |
| 453 | throw new \RuntimeException( 'Too many links in comment batch' ); |
| 454 | } |
| 455 | $this->links[] = $callback; |
| 456 | return sprintf( self::MARKER_PREFIX . "%0" . self::MAX_ID_SIZE . 'd', $nextId ); |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Link to a LinkTarget. Return either HTML or a marker depending on whether |
| 461 | * existence checks are deferred. |
| 462 | * |
| 463 | * @param LinkTarget $target |
| 464 | * @param string $text |
| 465 | * @param string|false|null $wikiId |
| 466 | * @param LinkTarget $contextTitle |
| 467 | * @return string |
| 468 | */ |
| 469 | private function addPageLink( LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle ) { |
| 470 | if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) { |
| 471 | // Handle links from a foreign wiki ID |
| 472 | return $this->linkRenderer->makeExternalLink( |
| 473 | WikiMap::getForeignURL( |
| 474 | $wikiId, |
| 475 | $target->getNamespace() === 0 |
| 476 | ? $target->getDBkey() |
| 477 | : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) . |
| 478 | ':' . $target->getDBkey(), |
| 479 | $target->getFragment() |
| 480 | ), |
| 481 | new HtmlArmor( $text ), // Already escaped |
| 482 | $contextTitle |
| 483 | ); |
| 484 | } elseif ( $this->linkCache->getGoodLinkID( $target ) || |
| 485 | Title::newFromLinkTarget( $target )->isAlwaysKnown() |
| 486 | ) { |
| 487 | // Already known |
| 488 | return $this->linkRenderer->makeKnownLink( $target, new HtmlArmor( $text ) ); |
| 489 | } elseif ( $this->linkCache->isBadLink( $target ) ) { |
| 490 | // Already cached as unknown |
| 491 | return $this->linkRenderer->makeBrokenLink( $target, new HtmlArmor( $text ) ); |
| 492 | } |
| 493 | |
| 494 | // Defer page link |
| 495 | if ( !$this->linkBatch ) { |
| 496 | $this->linkBatch = $this->linkBatchFactory->newLinkBatch(); |
| 497 | $this->linkBatch->setCaller( __METHOD__ ); |
| 498 | } |
| 499 | $this->linkBatch->addObj( $target ); |
| 500 | return $this->addLinkMarker( function () use ( $target, $text ) { |
| 501 | return $this->linkRenderer->makeLink( $target, new HtmlArmor( $text ) ); |
| 502 | } ); |
| 503 | } |
| 504 | |
| 505 | /** |
| 506 | * Link to a file, returning a marker. |
| 507 | * |
| 508 | * @param LinkTarget $target The name of the file. |
| 509 | * @param string $html The inner HTML of the link |
| 510 | * @return string |
| 511 | */ |
| 512 | private function addFileLink( LinkTarget $target, $html ) { |
| 513 | $this->fileBatch[] = [ |
| 514 | 'title' => $target |
| 515 | ]; |
| 516 | return $this->addLinkMarker( function () use ( $target, $html ) { |
| 517 | return Linker::makeMediaLinkFile( |
| 518 | $target, |
| 519 | $this->files[$target->getDBkey()] ?? false, |
| 520 | $html |
| 521 | ); |
| 522 | } ); |
| 523 | } |
| 524 | |
| 525 | /** |
| 526 | * Execute any pending link batch or file batch |
| 527 | */ |
| 528 | private function flushLinkBatches() { |
| 529 | if ( $this->linkBatch ) { |
| 530 | $this->linkBatch->execute(); |
| 531 | $this->linkBatch = null; |
| 532 | } |
| 533 | if ( $this->fileBatch ) { |
| 534 | $this->files += $this->repoGroup->findFiles( $this->fileBatch ); |
| 535 | $this->fileBatch = []; |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | } |