MediaWiki master
CommentParser.php
Go to the documentation of this file.
1<?php
2
4
29
40 private $linkRenderer;
42 private $linkBatchFactory;
44 private $repoGroup;
46 private $userLang;
48 private $contLang;
50 private $titleParser;
52 private $namespaceInfo;
54 private $hookRunner;
56 private $linkCache;
57 private LinkAlwaysKnownLookup $linkAlwaysKnownLookup;
58
60 private $links = [];
62 private $linkBatch;
63
65 private $fileBatch;
67 private $files = [];
68
70 private const MAX_ID_SIZE = 7;
72 private const MARKER_PREFIX = "\x1B\"'";
73
85 public function __construct(
86 LinkRenderer $linkRenderer,
87 LinkBatchFactory $linkBatchFactory,
88 LinkCache $linkCache,
89 RepoGroup $repoGroup,
90 Language $userLang,
91 Language $contLang,
92 TitleParser $titleParser,
93 NamespaceInfo $namespaceInfo,
94 HookContainer $hookContainer,
95 LinkAlwaysKnownLookup $linkAlwaysKnownLookup,
96 ) {
97 $this->linkRenderer = $linkRenderer;
98 $this->linkBatchFactory = $linkBatchFactory;
99 $this->linkCache = $linkCache;
100 $this->repoGroup = $repoGroup;
101 $this->userLang = $userLang;
102 $this->contLang = $contLang;
103 $this->titleParser = $titleParser;
104 $this->namespaceInfo = $namespaceInfo;
105 $this->hookRunner = new HookRunner( $hookContainer );
106 $this->linkAlwaysKnownLookup = $linkAlwaysKnownLookup;
107 }
108
120 public function preprocess( string $comment, ?LinkTarget $selfLinkTarget = null,
121 $samePage = false, $wikiId = false, $enableSectionLinks = true
122 ) {
123 return $this->preprocessInternal( $comment, false, $selfLinkTarget,
124 $samePage, $wikiId, $enableSectionLinks );
125 }
126
137 public function preprocessUnsafe( $comment, ?LinkTarget $selfLinkTarget = null,
138 $samePage = false, $wikiId = false, $enableSectionLinks = true
139 ) {
140 return $this->preprocessInternal( $comment, true, $selfLinkTarget,
141 $samePage, $wikiId, $enableSectionLinks );
142 }
143
151 public function finalize( $comments ) {
152 $this->flushLinkBatches();
153 return preg_replace_callback(
154 '/' . self::MARKER_PREFIX . '([0-9]{' . self::MAX_ID_SIZE . '})/',
155 function ( $m ) {
156 $callback = $this->links[(int)$m[1]] ?? null;
157 if ( $callback ) {
158 return $callback();
159 } else {
160 return '<!-- MISSING -->';
161 }
162 },
163 $comments
164 );
165 }
166
176 private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId,
177 $enableSectionLinks
178 ) {
179 // Sanitize text a bit
180 // \x1b needs to be stripped because it is used for link markers
181 $comment = strtr( $comment, "\n\x1b", " " );
182 // Allow HTML entities (for T15815)
183 if ( !$unsafe ) {
184 $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
185 }
186 if ( $enableSectionLinks ) {
187 $comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
188 }
189 return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
190 }
191
208 private function doSectionLinks(
209 $comment,
210 $selfLinkTarget = null,
211 $samePage = false,
212 $wikiId = false
213 ) {
214 $comment = preg_replace_callback(
215 // To detect the presence of content before or after the
216 // auto-comment, we use capturing groups inside optional zero-width
217 // assertions. But older versions of PCRE can not directly make
218 // zero-width assertions optional, so wrap them in a non-capturing
219 // group.
220 '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
221 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
222 $pre = ( $match[1] ?? '' ) !== '';
223 $section = ( $match[2] ?? '' );
224 $post = ( $match[3] ?? '' ) !== '';
225 $comment = null;
226
227 $this->hookRunner->onFormatAutocomments(
228 $comment, $pre, $section, $post,
229 Title::castFromLinkTarget( $selfLinkTarget ),
230 $samePage,
231 $wikiId );
232 if ( $comment !== null ) {
233 return $comment;
234 }
235
236 // HTML has already been escaped in preprocessInternal(), so treat this as HTML from this point
237 $parsedSection = $section;
238
239 if ( $selfLinkTarget ) {
240 $decodedSection = substr( Parser::guessSectionNameFromStrippedText(
241 // Remove links that a user may have manually put in the autosummary
242 // This could be improved by copying as much of Parser::stripSectionName as desired.
243 str_replace( [ '[[:', '[[', ']]' ], '', $section )
244 ), 1 );
245 if ( $decodedSection !== '' || $section === '' ) {
246 if ( $samePage ) {
247 $targetWithSection = new TitleValue( NS_MAIN, '', $decodedSection );
248 } else {
249 $targetWithSection = $selfLinkTarget->createFragmentTarget( $decodedSection );
250 }
251 if ( $section === '' ) {
252 // Special case for edits to the zeroth section (T412472).
253 $linkHtml = wfMessage( 'autocomment-top' )->inLanguage( $this->userLang )->escaped();
254 } else {
255 $linkHtml = $parsedSection;
256 }
257 $parsedSection = $this->makeSectionLink(
258 $targetWithSection,
259 $this->userLang->getArrow() .
260 Html::rawElement( 'bdi', [ 'dir' => $this->userLang->getDir() ], $linkHtml ),
261 $wikiId,
262 $selfLinkTarget
263 );
264 }
265 }
266 if ( $post ) {
267 # autocomment $postsep written summary (/* section */ summary)
268 $parsedSection .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
269 }
270 if ( $parsedSection ) {
271 $parsedSection = Html::rawElement( 'span', [ 'class' => 'autocomment' ], $parsedSection );
272 }
273 if ( $pre ) {
274 # written summary $presep autocomment (summary /* section */)
275 $parsedSection = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped()
276 . $parsedSection;
277 }
278
279 // Make sure any brackets (which the user could have input in the edit summary)
280 // in the generated autocomment HTML don't trigger additional link processing (T406664).
281 return str_replace( [ '[', ']' ], [ '&#91;', '&#93;' ], $parsedSection );
282 },
283 $comment
284 );
285 return $comment;
286 }
287
300 private function makeSectionLink(
301 LinkTarget $target, $html, $wikiId, LinkTarget $contextTitle
302 ) {
303 if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
304 return $this->linkRenderer->makeExternalLink(
305 WikiMap::getForeignURL(
306 $wikiId,
307 $target->getNamespace() === 0
308 ? $target->getDBkey()
309 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
310 ':' . $target->getDBkey(),
311 $target->getFragment()
312 ),
313 new HtmlArmor( $html ), // Already escaped
314 $contextTitle
315 );
316 } elseif ( !$target->hasFragment() && $target->getText() === '' && $target->getNamespace() === NS_MAIN ) {
317 // Special case for edits to the zeroth section (T412472) when linking within the same page.
318 // All of the LinkTarget classes treat this as a link to the main page.
319 // We want to link to the top of the current page.
320 return Html::rawElement( 'a', [
321 'href' => '#',
322 ], $html );
323 }
324 return $this->linkRenderer->makePreloadedLink( $target, new HtmlArmor( $html ), '' );
325 }
326
345 private function doWikiLinks( $comment, $selfLinkTarget = null, $samePage = false, $wikiId = false ) {
346 return preg_replace_callback(
347 '/
348 \[\[
349 \s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
350 :? # ignore optional leading colon
351 ([^[\]|]+) # 1. link target; page names cannot include [, ] or |
352 (?:\|
353 # 2. link text
354 # Stop matching at ]] without relying on backtracking.
355 ((?:]?[^\]])*+)
356 )?
357 \]\]
358 ([^[]*) # 3. link trail (the text up until the next link)
359 /x',
360 function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
361 $medians = '(?:';
362 $medians .= preg_quote(
363 $this->namespaceInfo->getCanonicalName( NS_MEDIA ), '/' );
364 $medians .= '|';
365 $medians .= preg_quote(
366 $this->contLang->getNsText( NS_MEDIA ),
367 '/'
368 ) . '):';
369
370 $comment = $match[0];
371
372 // Fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
373 if ( str_contains( $match[1], '%' ) ) {
374 $match[1] = strtr(
375 rawurldecode( $match[1] ),
376 [ '<' => '&lt;', '>' => '&gt;' ]
377 );
378 }
379
380 // Handle link renaming [[foo|text]] will show link as "text"
381 if ( $match[2] != "" ) {
382 $text = $match[2];
383 } else {
384 $text = $match[1];
385 }
386 $submatch = [];
387 $linkMarker = null;
388 if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
389 // Media link; trail not supported.
390 $linkRegexp = '/\[\[(.*?)\]\]/';
391 $linkTarget = $this->titleParser->makeTitleValueSafe( NS_FILE, $submatch[1] );
392 if ( $linkTarget ) {
393 $linkMarker = $this->addFileLink( $linkTarget, $text );
394 }
395 } else {
396 // Other kind of link
397 // Make sure its target is non-empty
398 if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
399 $match[1] = substr( $match[1], 1 );
400 }
401 if ( $match[1] !== false && $match[1] !== null && $match[1] !== '' ) {
402 if ( preg_match(
403 $this->contLang->linkTrail(),
404 $match[3],
405 $submatch
406 ) ) {
407 $trail = $submatch[1];
408 } else {
409 $trail = "";
410 }
411 $linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
412 [ $inside, $trail ] = Linker::splitTrail( $trail );
413
414 $linkText = $text;
415 $linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText );
416
417 try {
418 $target = $this->titleParser->parseTitle( $linkTarget );
419
420 if ( $target->getText() == '' && !$target->isExternal()
421 && !$samePage && $selfLinkTarget
422 ) {
423 $target = $selfLinkTarget->createFragmentTarget( $target->getFragment() );
424 }
425
426 // We should deprecate `null` as a valid value for
427 // $selfLinkTarget to ensure that we can use it as
428 // the title context for the external link.
429 // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
430 global $wgTitle;
431 $linkMarker = $this->addPageLink(
432 $target,
433 $linkText . $inside,
434 $wikiId,
435 $selfLinkTarget ?? $wgTitle ?? SpecialPage::getTitleFor( 'Badtitle' )
436 );
437 $linkMarker .= $trail;
438 } catch ( MalformedTitleException ) {
439 // Fall through
440 }
441 }
442 }
443 if ( $linkMarker ) {
444 // If the link is still valid, go ahead and replace it in!
445 $comment = preg_replace(
446 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable linkRegexp set when used
447 $linkRegexp,
449 $comment,
450 1
451 );
452 }
453
454 return $comment;
455 },
456 $comment
457 );
458 }
459
466 private function addLinkMarker( $callback ) {
467 $nextId = count( $this->links );
468 if ( strlen( (string)$nextId ) > self::MAX_ID_SIZE ) {
469 throw new \RuntimeException( 'Too many links in comment batch' );
470 }
471 $this->links[] = $callback;
472 return sprintf( self::MARKER_PREFIX . "%0" . self::MAX_ID_SIZE . 'd', $nextId );
473 }
474
485 private function addPageLink( LinkTarget $target, $text, $wikiId, LinkTarget $contextTitle ) {
486 if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
487 // Handle links from a foreign wiki ID
488 return $this->linkRenderer->makeExternalLink(
489 WikiMap::getForeignURL(
490 $wikiId,
491 $target->getNamespace() === 0
492 ? $target->getDBkey()
493 : $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
494 ':' . $target->getDBkey(),
495 $target->getFragment()
496 ),
497 new HtmlArmor( $text ), // Already escaped
498 $contextTitle
499 );
500 } elseif ( $this->linkCache->getGoodLinkID( $target ) ||
501 $this->linkAlwaysKnownLookup->isAlwaysKnown( $target )
502 ) {
503 // Already known
504 return $this->linkRenderer->makeKnownLink( $target, new HtmlArmor( $text ) );
505 } elseif ( $this->linkCache->isBadLink( $target ) ) {
506 // Already cached as unknown
507 return $this->linkRenderer->makeBrokenLink( $target, new HtmlArmor( $text ) );
508 }
509
510 // Defer page link
511 if ( !$this->linkBatch ) {
512 $this->linkBatch = $this->linkBatchFactory->newLinkBatch();
513 $this->linkBatch->setCaller( __METHOD__ );
514 }
515 $this->linkBatch->addObj( $target );
516 return $this->addLinkMarker( function () use ( $target, $text ) {
517 return $this->linkRenderer->makeLink( $target, new HtmlArmor( $text ) );
518 } );
519 }
520
528 private function addFileLink( LinkTarget $target, $html ) {
529 $this->fileBatch[] = [
530 'title' => $target
531 ];
532 return $this->addLinkMarker( function () use ( $target, $html ) {
533 return Linker::makeMediaLinkFile(
534 $target,
535 $this->files[$target->getDBkey()] ?? false,
536 $html
537 );
538 } );
539 }
540
544 private function flushLinkBatches() {
545 if ( $this->linkBatch ) {
546 $this->linkBatch->execute();
547 $this->linkBatch = null;
548 }
549 if ( $this->fileBatch ) {
550 $this->files += $this->repoGroup->findFiles( $this->fileBatch );
551 $this->fileBatch = [];
552 }
553 }
554
555}
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_MEDIA
Definition Defines.php:39
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(MW_ENTRY_POINT==='index') if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli') global $wgTitle
Definition Setup.php:529
The text processing backend for CommentFormatter.
finalize( $comments)
Execute pending batch queries and replace markers in the specified string(s) with actual links.
preprocessUnsafe( $comment, ?LinkTarget $selfLinkTarget=null, $samePage=false, $wikiId=false, $enableSectionLinks=true)
Convert a comment in pseudo-HTML format to HTML, replacing links with markers.
__construct(LinkRenderer $linkRenderer, LinkBatchFactory $linkBatchFactory, LinkCache $linkCache, RepoGroup $repoGroup, Language $userLang, Language $contLang, TitleParser $titleParser, NamespaceInfo $namespaceInfo, HookContainer $hookContainer, LinkAlwaysKnownLookup $linkAlwaysKnownLookup,)
preprocess(string $comment, ?LinkTarget $selfLinkTarget=null, $samePage=false, $wikiId=false, $enableSectionLinks=true)
Convert a comment to HTML, but replace links with markers which are resolved later.
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:80
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
Base class for language-specific code.
Definition Language.php:65
Class that generates HTML for internal links.
Some internal bits split of from Skin.php.
Definition Linker.php:48
Factory for LinkBatch objects to batch query page metadata.
Batch query for page metadata and feed to LinkCache.
Definition LinkBatch.php:36
Page existence and metadata cache.
Definition LinkCache.php:52
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:139
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
A title parser service for MediaWiki.
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
A collection of static methods to play with strings.
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
Represents the target of a wiki link.