Introduce CommentFormatter

CommentParser:

* Move comment formatting backend from Linker to a CommentParser service.
  Allow link existence and file existence to be batched.
* Rename $local to $samePage since I think that is clearer.
* Rename $title to $selfLinkTarget since it was unclear what the title
  was used for.
* Rename the "autocomment" concept to "section link" in public
  interfaces, although the old term remains in CSS classes.
* Keep unsafe HTML pass-through in separate "unsafe" methods, for easier
  static analysis and code review.

CommentFormatter:

* Add CommentFormatter and RowCommentFormatter services as a usable
  frontend for comment batches, and to replace the Linker static methods.
* Provide fluent and parametric interfaces.

Linker:

* Remove Linker::makeCommentLink() without deprecation -- nothing calls
  it and it is obviously an internal helper.
* Soft-deprecate Linker methods formatComment(), formatLinksInComment(),
  commentBlock() and revComment().

Caller migration:

* CommentFormatter single: Linker, RollbackAction, ApiComparePages,
  ApiParse
* CommentFormatter parametric batch: ImageHistoryPseudoPager
* CommentFormatter fluent batch: ApiQueryFilearchive
* RowCommentFormatter sequential: History feed, BlocklistPager,
  ProtectedPagesPager, ApiQueryProtectedTitles
* RowCommentFormatter with index: ChangesFeed, ChangesList,
  ApiQueryDeletedrevs, ApiQueryLogEvents, ApiQueryRecentChanges
* RevisionCommentBatch: HistoryPager, ContribsPager

Bug: T285917
Change-Id: Ia3fd50a4a13138ba5003d884962da24746d562d0
This commit is contained in:
Tim Starling 2021-07-01 16:55:03 +10:00 committed by Petr Pchelko
parent f3cf265e75
commit f7f84dddb3
55 changed files with 3658 additions and 461 deletions

View file

@ -142,6 +142,8 @@ because of Phabricator reports.
* PageProps::getInstance() has been deprecated. Use
MediaWikiServices::getPageProps() instead.
* User::setOption(), deprecated since 1.35, now emits deprecation warnings.
* Linker::formatComment(), ::formatLinksInComment(), ::commentBlock() and
revComment() were soft-deprecated. Use the new CommentFormatter service.
* Skin::getSkinStylePath has been hard deprecated. Direct string path
should be used instead.
* SkinTemplate::getPersonalToolsList(), deprecated since 1.35, now emits

View file

@ -0,0 +1,194 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
use Traversable;
/**
* This class provides a fluent interface for formatting a batch of comments.
*
* @since 1.38
*/
class CommentBatch {
/** @var CommentFormatter */
private $formatter;
/** @var iterable<CommentItem>|Traversable */
private $comments;
/** @var bool|null */
private $useBlock;
/** @var bool|null */
private $useParentheses;
/** @var LinkTarget|null */
private $selfLinkTarget;
/** @var bool|null */
private $samePage;
/** @var string|false|null */
private $wikiId;
/** @var bool|null */
private $enableSectionLinks;
/**
* @internal Use CommentFormatter::createBatch()
*
* @param CommentFormatter $formatter
*/
public function __construct( CommentFormatter $formatter ) {
$this->formatter = $formatter;
}
/**
* Set the comments to be formatted. This can be an array of CommentItem
* objects, or it can be an iterator which generates CommentItem objects.
*
* Theoretically iterable should imply Traversable, but PHPStorm gives an
* error when RowCommentIterator is passed as iterable<CommentItem>.
*
* @param iterable<CommentItem>|Traversable $comments
* @return $this
*/
public function comments( $comments ) {
$this->comments = $comments;
return $this;
}
/**
* Specify the comments to be formatted as an array of strings. This is a
* simplified wrapper for comments() which does not allow you to set options
* on a per-comment basis.
*
* $strings must be an array -- use comments() if you want to use an iterator.
*
* @param string[] $strings
* @return $this
*/
public function strings( array $strings ) {
$this->comments = new StringCommentIterator( $strings );
return $this;
}
/**
* Wrap each comment in standard punctuation and formatting if it's
* non-empty. Empty comments remain empty. This causes the batch to work
* like the old Linker::commentBlock().
*
* If this function is not called, the option is false.
*
* @param bool $useBlock
* @return $this
*/
public function useBlock( $useBlock = true ) {
$this->useBlock = $useBlock;
return $this;
}
/**
* Wrap each comment with parentheses. This has no effect if the useBlock
* option is not enabled.
*
* Unlike the legacy Linker::commentBlock(), this option defaults to false
* if this method is not called, since that better preserves the fluent
* style.
*
* @param bool $useParentheses
* @return $this
*/
public function useParentheses( $useParentheses = true ) {
$this->useParentheses = $useParentheses;
return $this;
}
/**
* Set the title to be used for self links in the comments. If there is no
* title specified either here or in the item, fragment links are not
* expanded.
*
* @param LinkTarget $selfLinkTarget
* @return $this
*/
public function selfLinkTarget( LinkTarget $selfLinkTarget ) {
$this->selfLinkTarget = $selfLinkTarget;
return $this;
}
/**
* Set the option to enable/disable section links formatted as C-style
* comments, as used in revision comments to indicate the section which
* was edited.
*
* If the method is not called, the option is true. Setting this to false
* approximately emulates Linker::formatLinksInComment() except that HTML
* in the input is escaped.
*
* @param bool $enable
* @return $this
*/
public function enableSectionLinks( $enable ) {
$this->enableSectionLinks = $enable;
return $this;
}
/**
* Disable section links formatted as C-style comments, as used in revision
* comments to indicate the section which was edited. Calling this
* approximately emulates Linker::formatLinksInComment() except that HTML
* in the input is escaped.
*
* @return $this
*/
public function disableSectionLinks() {
$this->enableSectionLinks = false;
return $this;
}
/**
* Set the same-page option. If this is true, section links and fragment-
* only wikilinks are rendered with an href that is a fragment-only URL.
* If it is false (the default), such links go to the self link title.
*
* This can also be set per-item using CommentItem::samePage().
*
* This is equivalent to $local in the old Linker methods.
*
* @param bool $samePage
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* ID of the wiki to link to (if not the local wiki), as used by WikiMap.
* This is used to render comments which are loaded from a foreign wiki.
* This only affects links which are syntactically internal -- it has no
* effect on interwiki links.
*
* This can also be set per-item using CommentItem::wikiId().
*
* @param string|false|null $wikiId
* @return $this
*/
public function wikiId( $wikiId ) {
$this->wikiId = $wikiId;
return $this;
}
/**
* Format the comments and produce an array of HTML fragments.
*
* @return string[]
*/
public function execute() {
return $this->formatter->formatItemsInternal(
$this->comments,
$this->selfLinkTarget,
$this->samePage,
$this->wikiId,
$this->enableSectionLinks,
$this->useBlock,
$this->useParentheses
);
}
}

View file

@ -0,0 +1,408 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RevisionRecord;
use Traversable;
/**
* This is the main service interface for converting single-line comments from
* various DB comment fields into HTML.
*
* @since 1.38
*/
class CommentFormatter {
/** @var CommentParserFactory */
protected $parserFactory;
/**
* @internal Use MediaWikiServices::getCommentFormatter()
*
* @param CommentParserFactory $parserFactory
*/
public function __construct( CommentParserFactory $parserFactory ) {
$this->parserFactory = $parserFactory;
}
/**
* Format comments using a fluent interface.
*
* @return CommentBatch
*/
public function createBatch() {
return new CommentBatch( $this );
}
/**
* Format a single comment. Similar to the old Linker::formatComment().
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function format( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
return $this->formatInternal( $comment, true, false, false,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Wrap a comment in standard punctuation and formatting if
* it's non-empty, otherwise return an empty string.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @param bool $useParentheses
* @return string
*/
public function formatBlock( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $useParentheses = true
) {
return $this->formatInternal( $comment, true, true, $useParentheses,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Format a comment, passing through HTML in the input to the output.
* This is unsafe and exists only for backwards compatibility with
* Linker::formatLinksInComment().
*
* In new code, use formatLinks() or createBatch()->disableSectionLinks().
*
* @internal
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function formatLinksUnsafe( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$preprocessed = $parser->preprocessUnsafe( $comment, $selfLinkTarget,
$samePage, $wikiId, false );
return $parser->finalize( $preprocessed );
}
/**
* Format links in a comment, ignoring section links in C-style comments.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function formatLinks( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
return $this->formatInternal( $comment, false, false, false,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Format a single comment with many ugly boolean parameters.
*
* @param string $comment
* @param bool $enableSectionLinks
* @param bool $useBlock
* @param bool $useParentheses
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string|string[]
*/
private function formatInternal( $comment, $enableSectionLinks, $useBlock, $useParentheses,
$selfLinkTarget = null, $samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$preprocessed = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId,
$enableSectionLinks );
$output = $parser->finalize( $preprocessed );
if ( $useBlock ) {
$output = $this->wrapCommentWithBlock( $output, $useParentheses );
}
return $output;
}
/**
* Format comments which are provided as strings and all have the same
* self-link target and other options.
*
* If you need a different title for each comment, use createBatch().
*
* @param string[] $strings
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string[]
*/
public function formatStrings( $strings, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $strings as $i => $comment ) {
$outputs[$i] = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId );
}
return $parser->finalize( $outputs );
}
/**
* Given an array of comments as strings which all have the same self link
* target, format the comments and wrap them in standard punctuation and
* formatting.
*
* If you need a different title for each comment, use createBatch().
*
* @param string[] $strings
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @param bool $useParentheses
* @return string[]
*/
public function formatStringsAsBlock( $strings, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $useParentheses = true
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $strings as $i => $comment ) {
$outputs[$i] = $this->wrapCommentWithBlock(
$parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId ),
$useParentheses );
}
return $parser->finalize( $outputs );
}
/**
* Wrap and format the given revision's comment block, if the specified
* user is allowed to view it.
*
* This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
*
* NOTE: revision comments are special. This is not the same as getting a
* revision comment as a string and then formatting it with format().
*
* @param RevisionRecord $revision The revision to extract the comment and
* title from. The title should always be populated, to avoid an additional
* DB query.
* @param Authority $authority The user viewing the comment
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param bool $isPublic Show only if all users can see it
* @param bool $useParentheses Whether the comment is wrapped in parentheses
* @return string
*/
public function formatRevision(
RevisionRecord $revision,
Authority $authority,
$samePage = false,
$isPublic = false,
$useParentheses = true
) {
$parser = $this->parserFactory->create();
return $parser->finalize( $this->preprocessRevComment(
$parser, $authority, $revision, $samePage, $isPublic, $useParentheses ) );
}
/**
* Format multiple revision comments.
*
* @see CommentFormatter::formatRevision()
*
* @param iterable<RevisionRecord> $revisions
* @param Authority $authority
* @param bool $samePage
* @param bool $isPublic
* @param bool $useParentheses
* @param bool $indexById
* @return string|string[]
*/
public function formatRevisions(
$revisions,
Authority $authority,
$samePage = false,
$isPublic = false,
$useParentheses = true,
$indexById = false
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $revisions as $i => $rev ) {
if ( $indexById ) {
$key = $rev->getId();
} else {
$key = $i;
}
$outputs[$key] = $this->preprocessRevComment(
$parser, $authority, $rev, $samePage, $isPublic, $useParentheses );
}
return $parser->finalize( $outputs );
}
/**
* Format a batch of revision comments using a fluent interface.
*
* @return RevisionCommentBatch
*/
public function createRevisionBatch() {
return new RevisionCommentBatch( $this );
}
/**
* Format an iterator over CommentItem objects
*
* A shortcut for createBatch()->comments()->execute() for when you
* need to pass no other options.
*
* @param iterable<CommentItem>|Traversable $items
* @return string[]
*/
public function formatItems( $items ) {
return $this->formatItemsInternal( $items );
}
/**
* @internal For use by CommentBatch
*
* Format comments with nullable batch options.
*
* @param iterable<CommentItem> $items
* @param LinkTarget|null $selfLinkTarget
* @param bool|null $samePage
* @param string|false|null $wikiId
* @param bool|null $enableSectionLinks
* @param bool|null $useBlock
* @param bool|null $useParentheses
* @return string[]
*/
public function formatItemsInternal( $items, $selfLinkTarget = null,
$samePage = null, $wikiId = null, $enableSectionLinks = null,
$useBlock = null, $useParentheses = null
) {
$outputs = [];
$parser = $this->parserFactory->create();
foreach ( $items as $index => $item ) {
$preprocessed = $parser->preprocess(
$item->comment,
$item->selfLinkTarget ?? $selfLinkTarget,
$item->samePage ?? $samePage ?? false,
$item->wikiId ?? $wikiId ?? false,
$enableSectionLinks ?? true
);
if ( $useBlock ?? false ) {
$preprocessed = $this->wrapCommentWithBlock(
$preprocessed,
$useParentheses ?? true
);
}
$outputs[$index] = $preprocessed;
}
return $parser->finalize( $outputs );
}
/**
* Wrap a comment in standard punctuation and formatting if
* it's non-empty, otherwise return empty string.
*
* @param string $formatted
* @param bool $useParentheses Whether the comment is wrapped in parentheses
*
* @return string
*/
protected function wrapCommentWithBlock(
$formatted, $useParentheses
) {
// '*' used to be the comment inserted by the software way back
// in antiquity in case none was provided, here for backwards
// compatibility, acc. to brion -ævar
if ( $formatted == '' || $formatted == '*' ) {
return '';
}
if ( $useParentheses ) {
$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
$classNames = 'comment';
} else {
$classNames = 'comment comment--without-parentheses';
}
return " <span class=\"$classNames\">$formatted</span>";
}
/**
* Preprocess and wrap a revision comment.
*
* @param CommentParser $parser
* @param Authority $authority
* @param RevisionRecord $revRecord
* @param bool $samePage Whether section links should refer to local page
* @param bool $isPublic Show only if all users can see it
* @param bool $useParentheses (optional) Wrap comments in parentheses where needed
* @return string HTML fragment with link markers
*/
private function preprocessRevComment(
CommentParser $parser,
Authority $authority,
RevisionRecord $revRecord,
$samePage = false,
$isPublic = false,
$useParentheses = true
) {
if ( $revRecord->getComment( RevisionRecord::RAW ) === null ) {
return "";
}
if ( $revRecord->audienceCan(
RevisionRecord::DELETED_COMMENT,
$isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
$authority )
) {
$comment = $revRecord->getComment( RevisionRecord::FOR_THIS_USER, $authority );
$block = $parser->preprocess(
$comment ? $comment->text : '',
$revRecord->getPageAsLinkTarget(),
$samePage,
null,
true
);
$block = $this->wrapCommentWithBlock( $block, $useParentheses );
} else {
$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
}
if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
$class = \Linker::getRevisionDeletedClass( $revRecord );
return " <span class=\"$class comment\">$block</span>";
}
return $block;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
/**
* An object to represent one of the inputs to a batch formatting operation.
*
* @since 1.38
* @newable
*/
class CommentItem {
/**
* @var string
* @internal
*/
public $comment;
/**
* @var LinkTarget|null
* @internal
*/
public $selfLinkTarget;
/**
* @var bool|null
* @internal
*/
public $samePage;
/**
* @var string|false|null
* @internal
*/
public $wikiId;
/**
* @param string $comment The comment to format
*/
public function __construct( string $comment ) {
$this->comment = $comment;
}
/**
* Set the self-link target.
*
* @param LinkTarget $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @return $this
*/
public function selfLinkTarget( LinkTarget $selfLinkTarget ) {
$this->selfLinkTarget = $selfLinkTarget;
return $this;
}
/**
* Set the same-page flag.
*
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* ID of the wiki to link to (if not the local wiki), as used by WikiMap.
* This is used to render comments which are loaded from a foreign wiki.
* This only affects links which are syntactically internal -- it has no
* effect on interwiki links.
*
* @param string|false|null $wikiId
* @return $this
*/
public function wikiId( $wikiId ) {
$this->wikiId = $wikiId;
return $this;
}
}

View file

@ -0,0 +1,528 @@
<?php
namespace MediaWiki\CommentFormatter;
use File;
use HtmlArmor;
use Language;
use LinkBatch;
use LinkCache;
use Linker;
use MalformedTitleException;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use NamespaceInfo;
use Parser;
use RepoGroup;
use Title;
use TitleParser;
use TitleValue;
/**
* The text processing backend for CommentFormatter.
*
* CommentParser objects should be discarded after the comment batch is
* complete, in order to reduce memory usage.
*
* @internal
*/
class CommentParser {
/** @var LinkRenderer */
private $linkRenderer;
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var RepoGroup */
private $repoGroup;
/** @var Language */
private $userLang;
/** @var Language */
private $contLang;
/** @var TitleParser */
private $titleParser;
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var HookRunner */
private $hookRunner;
/** @var LinkCache */
private $linkCache;
/** @var callable[] */
private $links = [];
/** @var LinkBatch|null */
private $linkBatch;
/** @var array Input to RepoGroup::findFiles() */
private $fileBatch;
/** @var File[] Resolved File objects indexed by DB key */
private $files = [];
/** @var int The maximum number of digits in a marker ID */
private const MAX_ID_SIZE = 7;
/**
* @param LinkRenderer $linkRenderer
* @param LinkBatchFactory $linkBatchFactory
* @param LinkCache $linkCache
* @param RepoGroup $repoGroup
* @param Language $userLang
* @param Language $contLang
* @param TitleParser $titleParser
* @param NamespaceInfo $namespaceInfo
* @param HookContainer $hookContainer
*/
public function __construct(
LinkRenderer $linkRenderer,
LinkBatchFactory $linkBatchFactory,
LinkCache $linkCache,
RepoGroup $repoGroup,
Language $userLang,
Language $contLang,
TitleParser $titleParser,
NamespaceInfo $namespaceInfo,
HookContainer $hookContainer
) {
$this->linkRenderer = $linkRenderer;
$this->linkBatchFactory = $linkBatchFactory;
$this->linkCache = $linkCache;
$this->repoGroup = $repoGroup;
$this->userLang = $userLang;
$this->contLang = $contLang;
$this->titleParser = $titleParser;
$this->namespaceInfo = $namespaceInfo;
$this->hookRunner = new HookRunner( $hookContainer );
}
/**
* Convert a comment to HTML, but replace links with markers which are
* resolved later.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
public function preprocess( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $enableSectionLinks = true
) {
return $this->preprocessInternal( $comment, false, $selfLinkTarget,
$samePage, $wikiId, $enableSectionLinks );
}
/**
* Convert a comment in pseudo-HTML format to HTML, replacing links with markers.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
public function preprocessUnsafe( $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $enableSectionLinks = true
) {
return $this->preprocessInternal( $comment, true, $selfLinkTarget,
$samePage, $wikiId, $enableSectionLinks );
}
/**
* Execute pending batch queries and replace markers in the specified
* string(s) with actual links.
*
* @param string|string[] $comments
* @return string|string[]
*/
public function finalize( $comments ) {
$this->flushLinkBatches();
return preg_replace_callback(
'/\x1b([0-9]{' . self::MAX_ID_SIZE . '})/',
function ( $m ) {
$callback = $this->links[(int)$m[1]] ?? null;
if ( $callback ) {
return $callback();
} else {
return '<!-- MISSING -->';
}
},
$comments
);
}
/**
* @param string $comment
* @param bool $unsafe
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId,
$enableSectionLinks
) {
// Sanitize text a bit
// \x1b needs to be stripped because it is used for link markers
$comment = strtr( $comment, "\n\x1b", " " );
// Allow HTML entities (for T15815)
if ( !$unsafe ) {
$comment = \Sanitizer::escapeHtmlAllowEntities( $comment );
}
if ( $enableSectionLinks ) {
$comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
}
return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
}
/**
* Converts C-style comments in edit summaries into section links.
*
* Too many things are called "comments", so these are mostly now called
* section links rather than autocomments.
*
* We look for all comments, match any text before and after the comment,
* add a separator where needed and format the comment itself with CSS.
*
* @param string $comment Comment text
* @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections
* @param bool $samePage Whether section links should refer to local page
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
* @return string Preprocessed comment
*/
private function doSectionLinks(
$comment,
$selfLinkTarget = null,
$samePage = false,
$wikiId = false
) {
// @todo $append here is something of a hack to preserve the status
// quo. Someone who knows more about bidi and such should decide
// (1) what sane rendering even *is* for an LTR edit summary on an RTL
// wiki, both when autocomments exist and when they don't, and
// (2) what markup will make that actually happen.
$append = '';
$comment = preg_replace_callback(
// To detect the presence of content before or after the
// auto-comment, we use capturing groups inside optional zero-width
// assertions. But older versions of PCRE can't directly make
// zero-width assertions optional, so wrap them in a non-capturing
// group.
'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
function ( $match ) use ( &$append, $selfLinkTarget, $samePage, $wikiId ) {
// Ensure all match positions are defined
$match += [ '', '', '', '' ];
$pre = $match[1] !== '';
$auto = $match[2];
$post = $match[3] !== '';
$comment = null;
$this->hookRunner->onFormatAutocomments(
$comment, $pre, $auto, $post,
Title::castFromLinkTarget( $selfLinkTarget ),
$samePage,
$wikiId );
if ( $comment !== null ) {
return $comment;
}
if ( $selfLinkTarget ) {
$section = $auto;
# Remove links that a user may have manually put in the autosummary
# This could be improved by copying as much of Parser::stripSectionName as desired.
$section = str_replace( [
'[[:',
'[[',
']]'
], '', $section );
// We don't want any links in the auto text to be linked, but we still
// want to show any [[ ]]
$sectionText = str_replace( '[[', '&#91;[', $auto );
$section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
if ( $section !== '' ) {
if ( $samePage ) {
$sectionTitle = new TitleValue( NS_MAIN, '', $section );
} else {
$sectionTitle = $selfLinkTarget->createFragmentTarget( $section );
}
$auto = $this->makeSectionLink(
$sectionTitle,
$this->userLang->getArrow() . $this->userLang->getDirMark() . $sectionText,
$wikiId
);
}
}
if ( $pre ) {
# written summary $presep autocomment (summary /* section */)
$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
}
if ( $post ) {
# autocomment $postsep written summary (/* section */ summary)
$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
}
if ( $auto ) {
$auto = '<span dir="auto"><span class="autocomment">' . $auto . '</span>';
$append .= '</span>';
}
$comment = $pre . $auto;
return $comment;
},
$comment
);
return $comment . $append;
}
/**
* Make a section link. These don't need to go into the LinkBatch, since
* the link class does not depend on whether the link is known.
*
* @param LinkTarget $target
* @param string $text
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
*
* @return string HTML link
*/
private function makeSectionLink(
LinkTarget $target, $text, $wikiId
) {
if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
return Linker::makeExternalLink(
\WikiMap::getForeignURL(
$wikiId,
$target->getNamespace() === 0
? $target->getDBkey()
: $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
':' . $target->getDBkey(),
$target->getFragment()
),
$text,
/* escape = */ false // Already escaped
);
}
return $this->linkRenderer->makePreloadedLink( $target, new HtmlArmor( $text ), '' );
}
/**
* Formats wiki links and media links in text; all other wiki formatting
* is ignored
*
* @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
*
* @param string $comment Text to format links in. WARNING! Since the output of this
* function is html, $comment must be sanitized for use as html. You probably want
* to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
* this function.
* as used by WikiMap.
* @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections
* @param bool $samePage Whether section links should refer to local page
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
*
* @return string HTML
*/
private function doWikiLinks( $comment, $selfLinkTarget = null, $samePage = false, $wikiId = false ) {
return preg_replace_callback(
'/
\[\[
\s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
:? # ignore optional leading colon
([^[\]|]+) # 1. link target; page names cannot include [, ] or |
(?:\|
# 2. link text
# Stop matching at ]] without relying on backtracking.
((?:]?[^\]])*+)
)?
\]\]
([^[]*) # 3. link trail (the text up until the next link)
/x',
function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
$medians = '(?:';
$medians .= preg_quote(
$this->namespaceInfo->getCanonicalName( NS_MEDIA ), '/' );
$medians .= '|';
$medians .= preg_quote(
$this->contLang->getNsText( NS_MEDIA ),
'/'
) . '):';
$comment = $match[0];
// Fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
if ( strpos( $match[1], '%' ) !== false ) {
$match[1] = strtr(
rawurldecode( $match[1] ),
[ '<' => '&lt;', '>' => '&gt;' ]
);
}
// Handle link renaming [[foo|text]] will show link as "text"
if ( $match[2] != "" ) {
$text = $match[2];
} else {
$text = $match[1];
}
$submatch = [];
$linkMarker = null;
if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
// Media link; trail not supported.
$linkRegexp = '/\[\[(.*?)\]\]/';
$linkTarget = $this->titleParser->makeTitleValueSafe( NS_FILE, $submatch[1] );
if ( $linkTarget ) {
$linkMarker = $this->addFileLink( $linkTarget, $text );
}
} else {
// Other kind of link
// Make sure its target is non-empty
if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
$match[1] = substr( $match[1], 1 );
}
if ( $match[1] !== false && $match[1] !== '' ) {
if ( preg_match(
$this->contLang->linkTrail(),
$match[3],
$submatch
) ) {
$trail = $submatch[1];
} else {
$trail = "";
}
$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
list( $inside, $trail ) = Linker::splitTrail( $trail );
$linkText = $text;
$linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText );
try {
$target = $this->titleParser->parseTitle( $linkTarget );
if ( $target->getText() == '' && !$target->isExternal()
&& !$samePage && $selfLinkTarget
) {
$target = $selfLinkTarget->createFragmentTarget( $target->getFragment() );
}
$linkMarker = $this->addPageLink( $target, $linkText . $inside, $wikiId );
$linkMarker .= $trail;
} catch ( MalformedTitleException $e ) {
// Fall through
}
}
}
if ( $linkMarker ) {
// If the link is still valid, go ahead and replace it in!
$comment = preg_replace(
$linkRegexp,
$linkMarker,
$comment,
1
);
}
return $comment;
},
$comment
);
}
/**
* Add a deferred link to the list and return its marker.
*
* @param callable $callback
* @return string
*/
private function addLinkMarker( $callback ) {
$nextId = count( $this->links );
if ( strlen( $nextId ) > self::MAX_ID_SIZE ) {
throw new \RuntimeException( 'Too many links in comment batch' );
}
$this->links[] = $callback;
return sprintf( "\x1b%0" . self::MAX_ID_SIZE . 'd', $nextId );
}
/**
* Link to a LinkTarget. Return either HTML or a marker depending on whether
* existence checks are deferred.
*
* @param LinkTarget $target
* @param string $text
* @param string|false|null $wikiId
* @return string
*/
private function addPageLink( LinkTarget $target, $text, $wikiId ) {
// Handle external links (not including interwiki links)
if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
return Linker::makeExternalLink(
\WikiMap::getForeignURL(
$wikiId,
$target->getNamespace() === 0
? $target->getDBkey()
: $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
':' . $target->getDBkey(),
$target->getFragment()
),
$text,
/* escape = */ false // Already escaped
);
}
if ( $this->linkCache->getGoodLinkID( $target ) ) {
// Already known
return $this->linkRenderer->makeKnownLink( $target, new HtmlArmor( $text ) );
} elseif ( $this->linkCache->isBadLink( $target ) ) {
// Already cached as unknown
return $this->linkRenderer->makeBrokenLink( $target, new HtmlArmor( $text ) );
}
// Defer page link
if ( !$this->linkBatch ) {
$this->linkBatch = $this->linkBatchFactory->newLinkBatch();
}
$this->linkBatch->addObj( $target );
return $this->addLinkMarker( function () use ( $target, $text ) {
return $this->linkRenderer->makeLink( $target, new HtmlArmor( $text ) );
} );
}
/**
* Link to a file, returning a marker.
*
* @param LinkTarget $target The name of the file.
* @param string $html The inner HTML of the link
* @return string
*/
private function addFileLink( LinkTarget $target, $html ) {
$this->fileBatch[] = [
'title' => $target
];
return $this->addLinkMarker( function () use ( $target, $html ) {
return Linker::makeMediaLinkFile(
$target,
$this->files[$target->getDBkey()] ?? false,
$html
);
} );
}
/**
* Execute any pending link batch or file batch
*/
private function flushLinkBatches() {
if ( $this->linkBatch ) {
$this->linkBatch->execute();
$this->linkBatch = null;
}
if ( $this->fileBatch ) {
$this->files += $this->repoGroup->findFiles( $this->fileBatch );
$this->fileBatch = [];
}
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace MediaWiki\CommentFormatter;
use Language;
use LinkCache;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Linker\LinkRenderer;
use NamespaceInfo;
use RepoGroup;
use TitleParser;
/**
* @internal
*/
class CommentParserFactory {
/** @var LinkRenderer */
private $linkRenderer;
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var LinkCache */
private $linkCache;
/** @var RepoGroup */
private $repoGroup;
/** @var Language */
private $userLang;
/** @var Language */
private $contLang;
/** @var TitleParser */
private $titleParser;
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var HookContainer */
private $hookContainer;
/**
* @param LinkRenderer $linkRenderer
* @param LinkBatchFactory $linkBatchFactory
* @param LinkCache $linkCache
* @param RepoGroup $repoGroup
* @param Language $userLang
* @param Language $contLang
* @param TitleParser $titleParser
* @param NamespaceInfo $namespaceInfo
* @param HookContainer $hookContainer
*/
public function __construct(
LinkRenderer $linkRenderer,
LinkBatchFactory $linkBatchFactory,
LinkCache $linkCache,
RepoGroup $repoGroup,
Language $userLang,
Language $contLang,
TitleParser $titleParser,
NamespaceInfo $namespaceInfo,
HookContainer $hookContainer
) {
$this->linkRenderer = $linkRenderer;
$this->linkBatchFactory = $linkBatchFactory;
$this->linkCache = $linkCache;
$this->repoGroup = $repoGroup;
$this->userLang = $userLang;
$this->contLang = $contLang;
$this->titleParser = $titleParser;
$this->namespaceInfo = $namespaceInfo;
$this->hookContainer = $hookContainer;
}
/**
* @return CommentParser
*/
public function create() {
return new CommentParser(
$this->linkRenderer,
$this->linkBatchFactory,
$this->linkCache,
$this->repoGroup,
$this->userLang,
$this->contLang,
$this->titleParser,
$this->namespaceInfo,
$this->hookContainer
);
}
}

View file

@ -0,0 +1,133 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RevisionRecord;
/**
* Fluent interface for revision comment batch inputs.
*
* @since 1.38
*/
class RevisionCommentBatch {
/** @var CommentFormatter */
private $formatter;
/** @var Authority|null */
private $authority;
/** @var iterable<RevisionRecord> */
private $revisions;
/** @var bool */
private $samePage = false;
/** @var bool */
private $isPublic = false;
/** @var bool */
private $useParentheses = false;
/** @var bool */
private $indexById = false;
/**
* @param CommentFormatter $formatter
*/
public function __construct( CommentFormatter $formatter ) {
$this->formatter = $formatter;
}
/**
* Set the authority to use for permission checks. This must be called
* prior to execute().
*
* @param Authority $authority
* @return $this
*/
public function authority( Authority $authority ) {
$this->authority = $authority;
return $this;
}
/**
* Set the revisions to extract comments from.
*
* @param iterable<RevisionRecord> $revisions
* @return $this
*/
public function revisions( $revisions ) {
$this->revisions = $revisions;
return $this;
}
/**
* Set the same-page option. If this is true, section links and fragment-
* only wikilinks are rendered with an href that is a fragment-only URL.
* If it is false (the default), such links go to the self link title.
*
* This is equivalent to $local in the old Linker methods.
*
* @param bool $samePage
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* Wrap the comment with parentheses. This has no effect if the useBlock
* option is not enabled.
*
* Unlike the legacy Linker::commentBlock(), this option defaults to false
* if this method is not called, since that better preserves the fluent
* style.
*
* @param bool $useParentheses
* @return $this
*/
public function useParentheses( $useParentheses = true ) {
$this->useParentheses = $useParentheses;
return $this;
}
/**
* If this is true, show the comment only if all users can see it.
*
* We'll call it hideIfDeleted() since public is a keyword and isPublic()
* has an inappropriate verb.
*
* @param bool $isPublic
* @return $this
*/
public function hideIfDeleted( $isPublic = true ) {
$this->isPublic = $isPublic;
return $this;
}
/**
* If this is true, the array keys in the return value will be the revision
* IDs instead of the keys from the input array.
*
* @param bool $indexById
* @return $this
*/
public function indexById( $indexById = true ) {
$this->indexById = $indexById;
return $this;
}
/**
* Format the comments.
*
* @return string[] Formatted comments. The array key is either the field
* value specified by indexField(), or if that was not called, it is the
* key from the array passed to revisions().
*/
public function execute() {
return $this->formatter->formatRevisions(
$this->revisions,
$this->authority,
$this->samePage,
$this->isPublic,
$this->useParentheses,
$this->indexById
);
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace MediaWiki\CommentFormatter;
use CommentStore;
use Traversable;
use Wikimedia\Rdbms\IResultWrapper;
/**
* This is basically a CommentFormatter with a CommentStore dependency, allowing
* it to retrieve comment texts directly from database result wrappers.
*
* @since 1.38
*/
class RowCommentFormatter extends CommentFormatter {
/** @var CommentStore */
private $commentStore;
/**
* @internal Use MediaWikiServices::getRowCommentFormatter()
*
* @param CommentParserFactory $commentParserFactory
* @param CommentStore $commentStore
*/
public function __construct(
CommentParserFactory $commentParserFactory,
CommentStore $commentStore
) {
parent::__construct( $commentParserFactory );
$this->commentStore = $commentStore;
}
/**
* Format DB rows using a fluent interface. Pass the return value of this
* function to CommentBatch::comments().
*
* Example:
* $comments = $rowCommentFormatter->createBatch()
* ->comments(
* $rowCommentFormatter->rows( $rows )
* ->commentField( 'img_comment' )
* )
* ->useBlock( true )
* ->execute();
*
* @param Traversable|array $rows
* @return RowCommentIterator
*/
public function rows( $rows ) {
return new RowCommentIterator( $this->commentStore, $rows );
}
/**
* Format DB rows using a parametric interface.
*
* @param iterable<\stdClass>|IResultWrapper $rows
* @param string $commentKey The comment key to pass through to CommentStore,
* typically a legacy field name.
* @param string|null $namespaceField The namespace field for the self-link
* target, or null to have no self-link target.
* @param string|null $titleField The title field for the self-link target,
* or null to have no self-link target.
* @param string|null $indexField The field to use for array keys in the
* result, or null to use the same keys as in the input $rows
* @param bool $useBlock Wrap the output in standard punctuation and
* formatting if it's non-empty.
* @param bool $useParentheses Wrap the output with parentheses. Has no
* effect if $useBlock is false.
* @return string[] The formatted comment. The key will be the value of the
* index field if an index field was specified, or the key from the
* corresponding element of $rows if no index field was specified.
*/
public function formatRows( $rows, $commentKey, $namespaceField = null, $titleField = null,
$indexField = null, $useBlock = false, $useParentheses = true
) {
return $this->createBatch()
->comments(
$this->rows( $rows )
->commentKey( $commentKey )
->namespaceField( $namespaceField )
->titleField( $titleField )
->indexField( $indexField )
)
->useBlock( $useBlock )
->useParentheses( $useParentheses )
->execute();
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace MediaWiki\CommentFormatter;
use ArrayIterator;
use CommentStore;
use IteratorIterator;
use TitleValue;
use Traversable;
/**
* An adaptor which converts a row iterator into a CommentItem iterator for
* batch formatting.
*
* Fluent-style mutators are provided to configure how comment text is extracted
* from rows.
*
* Using an iterator for this configuration, instead of putting the
* options in CommentBatch, allows CommentBatch to be a simple single
* class without a CommentStore dependency.
*
* @since 1.38
*/
class RowCommentIterator extends IteratorIterator {
/** @var CommentStore */
private $commentStore;
/** @var string|null */
private $commentKey;
/** @var string|null */
private $namespaceField;
/** @var string|null */
private $titleField;
/** @var string|null */
private $indexField;
/**
* @internal Use RowCommentFormatter::rows()
* @param CommentStore $commentStore
* @param Traversable|array $rows
*/
public function __construct( CommentStore $commentStore, $rows ) {
if ( is_array( $rows ) ) {
parent::__construct( new ArrayIterator( $rows ) );
} else {
parent::__construct( $rows );
}
$this->commentStore = $commentStore;
}
/**
* Set what CommentStore calls the key -- typically a legacy field name
* which once held a comment. This must be called before attempting
* iteration.
*
* @param string $key
* @return $this
*/
public function commentKey( $key ) {
$this->commentKey = $key;
return $this;
}
/**
* Set the namespace field. If this is not called, the item will not have
* a self-link target, although it may be provided by the batch.
*
* @param string $field
* @return $this
*/
public function namespaceField( $field ) {
$this->namespaceField = $field;
return $this;
}
/**
* Set the title field. If this is not called, the item will not have
* a self-link target, although it may be provided by the batch.
*
* @param string $field
* @return $this
*/
public function titleField( $field ) {
$this->titleField = $field;
return $this;
}
/**
* Set the index field. Values from this field will appear as array keys
* in the final formatted comment array. If unset, the array will be
* numerically indexed.
*
* @param string $field
* @return $this
*/
public function indexField( $field ) {
$this->indexField = $field;
return $this;
}
public function key() {
if ( $this->indexField ) {
return parent::current()->{$this->indexField};
} else {
return parent::key();
}
}
public function current() {
if ( $this->commentKey === null ) {
throw new \RuntimeException( __METHOD__ . ': commentKey must be specified' );
}
$row = parent::current();
$comment = $this->commentStore->getComment( $this->commentKey, $row );
$item = new CommentItem( $comment->text );
if ( $this->namespaceField && $this->titleField ) {
$item->selfLinkTarget( new TitleValue(
(int)$row->{$this->namespaceField},
$row->{$this->titleField}
) );
}
return $item;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\CommentFormatter;
use ArrayIterator;
/**
* An adaptor which converts an array of strings to an iterator of CommentItem
* objects.
*
* @since 1.38
*/
class StringCommentIterator extends ArrayIterator {
/**
* @internal Use CommentBatch::strings()
* @param string[] $strings
*/
public function __construct( $strings ) {
parent::__construct( $strings );
}
public function current() {
return new CommentItem( parent::current() );
}
}

View file

@ -283,20 +283,6 @@ class DummyLinker {
);
}
public function makeCommentLink(
Title $title,
$text,
$wikiId = null,
$options = []
) {
return Linker::makeCommentLink(
$title,
$text,
$wikiId,
$options
);
}
public function normalizeSubpageLink( $contextTitle, $target, &$text ) {
return Linker::normalizeSubpageLink(
$contextTitle,

View file

@ -68,9 +68,11 @@ class FeedUtils {
*
* @param stdClass $row Row from the recentchanges table, including fields as
* appropriate for CommentStore
* @param string|null $formattedComment rc_comment in HTML format, or null
* to format it on demand.
* @return string
*/
public static function formatDiff( $row ) {
public static function formatDiff( $row, $formattedComment = null ) {
$titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
$timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
$actiontext = '';
@ -78,12 +80,16 @@ class FeedUtils {
$rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows
$actiontext = LogFormatter::newFromRow( $rcRow )->getActionText();
}
return self::formatDiffRow( $titleObj,
if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
$formattedComment = wfMessage( 'rev-deleted-comment' )->escaped();
} elseif ( $formattedComment === null ) {
$formattedComment = Linker::formatComment(
CommentStore::getStore()->getComment( 'rc_comment', $row )->text );
}
return self::formatDiffRow2( $titleObj,
$row->rc_last_oldid, $row->rc_this_oldid,
$timestamp,
$row->rc_deleted & RevisionRecord::DELETED_COMMENT
? wfMessage( 'rev-deleted-comment' )->escaped()
: CommentStore::getStore()->getComment( 'rc_comment', $row )->text,
$formattedComment,
$actiontext
);
}
@ -91,6 +97,8 @@ class FeedUtils {
/**
* Really format a diff for the newsfeed
*
* @deprecated since 1.38 use formatDiffRow2
*
* @param Title $title
* @param int $oldid Old revision's id
* @param int $newid New revision's id
@ -101,13 +109,34 @@ class FeedUtils {
*/
public static function formatDiffRow( $title, $oldid, $newid, $timestamp,
$comment, $actiontext = ''
) {
$formattedComment = MediaWikiServices::getInstance()->getCommentFormatter()
->format( $comment );
return self::formatDiffRow2( $title, $oldid, $newid, $timestamp,
$formattedComment, $actiontext );
}
/**
* Really really format a diff for the newsfeed. Same as formatDiffRow()
* except with preformatted comments.
*
* @param Title $title
* @param int $oldid Old revision's id
* @param int $newid New revision's id
* @param int $timestamp New revision's timestamp
* @param string $formattedComment New revision's comment in HTML format
* @param string $actiontext Text of the action; in case of log event
* @return string
*/
public static function formatDiffRow2( $title, $oldid, $newid, $timestamp,
$formattedComment, $actiontext = ''
) {
global $wgFeedDiffCutoff;
// log entries
$unwrappedText = implode(
' ',
array_filter( [ $actiontext, Linker::formatComment( $comment ) ] )
array_filter( [ $actiontext, $formattedComment ] )
);
$completeText = Html::rawElement( 'p', [], $unwrappedText ) . "\n";

View file

@ -1357,8 +1357,8 @@ class Linker {
*
* This method produces HTML that can require CSS styles in mediawiki.interface.helpers.styles.
*
* @author Erik Moeller <moeller@scireview.de>
* @since 1.16.3. $wikiId added in 1.26
* @deprecated since 1.38 use CommentFormatter
*
* @param string $comment
* @param LinkTarget|null $title LinkTarget object (to generate link to the section in
@ -1372,113 +1372,8 @@ class Linker {
public static function formatComment(
$comment, $title = null, $local = false, $wikiId = null
) {
# Sanitize text a bit:
$comment = str_replace( "\n", " ", $comment );
# Allow HTML entities (for T15815)
$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
# Render autocomments and make links:
$comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
return self::formatLinksInComment( $comment, $title, $local, $wikiId );
}
/**
* Converts autogenerated comments in edit summaries into section links.
*
* The pattern for autogen comments is / * foo * /, which makes for
* some nasty regex.
* We look for all comments, match any text before and after the comment,
* add a separator where needed and format the comment itself with CSS
* Called by Linker::formatComment.
*
* @param string $comment Comment text
* @param LinkTarget|null $title An optional LinkTarget object used to links to sections
* @param bool $local Whether section links should refer to local page
* @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
*
* @return string Formatted comment (wikitext)
*/
private static function formatAutocomments(
$comment, $title = null, $local = false, $wikiId = null
) {
// @todo $append here is something of a hack to preserve the status
// quo. Someone who knows more about bidi and such should decide
// (1) what sane rendering even *is* for an LTR edit summary on an RTL
// wiki, both when autocomments exist and when they don't, and
// (2) what markup will make that actually happen.
$append = '';
$comment = preg_replace_callback(
// To detect the presence of content before or after the
// auto-comment, we use capturing groups inside optional zero-width
// assertions. But older versions of PCRE can't directly make
// zero-width assertions optional, so wrap them in a non-capturing
// group.
'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
static function ( $match ) use ( $title, $local, $wikiId, &$append ) {
global $wgLang;
// Ensure all match positions are defined
$match += [ '', '', '', '' ];
$pre = $match[1] !== '';
$auto = $match[2];
$post = $match[3] !== '';
$comment = null;
Hooks::runner()->onFormatAutocomments(
$comment, $pre, $auto, $post, Title::castFromLinkTarget( $title ), $local,
$wikiId );
if ( $comment === null ) {
if ( $title ) {
$section = $auto;
# Remove links that a user may have manually put in the autosummary
# This could be improved by copying as much of Parser::stripSectionName as desired.
$section = str_replace( [
'[[:',
'[[',
']]'
], '', $section );
// We don't want any links in the auto text to be linked, but we still
// want to show any [[ ]]
$sectionText = str_replace( '[[', '&#91;[', $auto );
$section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
if ( $section !== '' ) {
if ( $local ) {
$sectionTitle = new TitleValue( NS_MAIN, '', $section );
} else {
$sectionTitle = $title->createFragmentTarget( $section );
}
$auto = Linker::makeCommentLink(
$sectionTitle,
$wgLang->getArrow() . $wgLang->getDirMark() . $sectionText,
$wikiId,
'noclasses'
);
}
}
if ( $pre ) {
# written summary $presep autocomment (summary /* section */)
$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
}
if ( $post ) {
# autocomment $postsep written summary (/* section */ summary)
$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
}
if ( $auto ) {
$auto = '<span dir="auto"><span class="autocomment">' . $auto . '</span>';
$append .= '</span>';
}
$comment = $pre . $auto;
}
return $comment;
},
$comment
);
return $comment . $append;
$formatter = MediaWikiServices::getInstance()->getCommentFormatter();
return $formatter->format( $comment, $title, $local, $wikiId );
}
/**
@ -1486,7 +1381,7 @@ class Linker {
* is ignored
*
* @since 1.16.3. $wikiId added in 1.26
* @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
* @deprecated since 1.38 use CommentFormatter
*
* @param string $comment Text to format links in. WARNING! Since the output of this
* function is html, $comment must be sanitized for use as html. You probably want
@ -1503,146 +1398,8 @@ class Linker {
public static function formatLinksInComment(
$comment, $title = null, $local = false, $wikiId = null
) {
return preg_replace_callback(
'/
\[\[
\s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
:? # ignore optional leading colon
([^[\]|]+) # 1. link target; page names cannot include [, ] or |
(?:\|
# 2. link text
# Stop matching at ]] without relying on backtracking.
((?:]?[^\]])*+)
)?
\]\]
([^[]*) # 3. link trail (the text up until the next link)
/x',
static function ( $match ) use ( $title, $local, $wikiId ) {
$services = MediaWikiServices::getInstance();
$medians = '(?:';
$medians .= preg_quote(
$services->getNamespaceInfo()->getCanonicalName( NS_MEDIA ), '/' );
$medians .= '|';
$medians .= preg_quote(
$services->getContentLanguage()->getNsText( NS_MEDIA ),
'/'
) . '):';
$comment = $match[0];
# fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
if ( strpos( $match[1], '%' ) !== false ) {
$match[1] = strtr(
rawurldecode( $match[1] ),
[ '<' => '&lt;', '>' => '&gt;' ]
);
}
# Handle link renaming [[foo|text]] will show link as "text"
if ( $match[2] != "" ) {
$text = $match[2];
} else {
$text = $match[1];
}
$submatch = [];
$thelink = null;
if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
# Media link; trail not supported.
$linkRegexp = '/\[\[(.*?)\]\]/';
$title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
if ( $title ) {
$thelink = Linker::makeMediaLinkObj( $title, $text );
}
} else {
# Other kind of link
# Make sure its target is non-empty
if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
$match[1] = substr( $match[1], 1 );
}
if ( $match[1] !== false && $match[1] !== '' ) {
if ( preg_match(
$services->getContentLanguage()->linkTrail(),
$match[3],
$submatch
) ) {
$trail = $submatch[1];
} else {
$trail = "";
}
$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
list( $inside, $trail ) = Linker::splitTrail( $trail );
$linkText = $text;
$linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
try {
$target = $services->getTitleParser()->
parseTitle( $linkTarget );
if ( $target->getText() == '' && !$target->isExternal()
&& !$local && $title
) {
$target = $title->createFragmentTarget( $target->getFragment() );
}
$thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
} catch ( MalformedTitleException $e ) {
// Fall through
}
}
}
if ( $thelink ) {
// If the link is still valid, go ahead and replace it in!
$comment = preg_replace(
$linkRegexp,
StringUtils::escapeRegexReplacement( $thelink ),
$comment,
1
);
}
return $comment;
},
$comment
);
}
/**
* Generates a link to the given LinkTarget
*
* @note This is only public for technical reasons. It's not intended for use outside Linker.
*
* @param LinkTarget $linkTarget
* @param string $text
* @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
* @param string|string[] $options See the $options parameter in Linker::link.
*
* @return string HTML link
*/
public static function makeCommentLink(
LinkTarget $linkTarget, $text, $wikiId = null, $options = []
) {
if ( $wikiId !== null && !$linkTarget->isExternal() ) {
$link = self::makeExternalLink(
WikiMap::getForeignURL(
$wikiId,
$linkTarget->getNamespace() === 0
? $linkTarget->getDBkey()
: MediaWikiServices::getInstance()->getNamespaceInfo()->
getCanonicalName( $linkTarget->getNamespace() ) .
':' . $linkTarget->getDBkey(),
$linkTarget->getFragment()
),
$text,
/* escape = */ false // Already escaped
);
} else {
$link = self::link( $linkTarget, $text, [], [], $options );
}
return $link;
$formatter = MediaWikiServices::getInstance()->getCommentFormatter();
return $formatter->formatLinksUnsafe( $comment, $title, $local, $wikiId );
}
/**
@ -1736,6 +1493,8 @@ class Linker {
* This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
*
* @since 1.16.3. $wikiId added in 1.26
* @deprecated since 1.38 use CommentFormatter
*
* @param string $comment
* @param LinkTarget|null $title LinkTarget object (to generate link to section in autocomment)
* or null
@ -1749,20 +1508,8 @@ class Linker {
public static function commentBlock(
$comment, $title = null, $local = false, $wikiId = null, $useParentheses = true
) {
// '*' used to be the comment inserted by the software way back
// in antiquity in case none was provided, here for backwards
// compatibility, acc. to brion -ævar
if ( $comment == '' || $comment == '*' ) {
return '';
}
$formatted = self::formatComment( $comment, $title, $local, $wikiId );
if ( $useParentheses ) {
$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
$classNames = 'comment';
} else {
$classNames = 'comment comment--without-parentheses';
}
return " <span class=\"$classNames\">$formatted</span>";
return MediaWikiServices::getInstance()->getCommentFormatter()
->formatBlock( $comment, $title, $local, $wikiId, $useParentheses );
}
/**
@ -1772,6 +1519,7 @@ class Linker {
* This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
*
* @since 1.16.3
* @deprecated since 1.38 use CommentFormatter
* @param RevisionRecord $revRecord (Switched from the old Revision class to RevisionRecord
* since 1.35)
* @param bool $local Whether section links should refer to local page
@ -1785,31 +1533,9 @@ class Linker {
$isPublic = false,
$useParentheses = true
) {
// TODO inject authority
$authority = RequestContext::getMain()->getAuthority();
if ( $revRecord->getComment( RevisionRecord::RAW ) === null ) {
return "";
}
if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) && $isPublic ) {
$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
} elseif ( $revRecord->userCan( RevisionRecord::DELETED_COMMENT, $authority ) ) {
$comment = $revRecord->getComment( RevisionRecord::FOR_THIS_USER, $authority );
$block = self::commentBlock(
$comment ? $comment->text : null,
$revRecord->getPageAsLinkTarget(),
$local,
null,
$useParentheses
);
} else {
$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
}
if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
$class = self::getRevisionDeletedClass( $revRecord );
return " <span class=\"$class comment\">$block</span>";
}
return $block;
$formatter = MediaWikiServices::getInstance()->getCommentFormatter();
return $formatter->formatRevision( $revRecord, $authority, $local, $isPublic, $useParentheses );
}
/**

View file

@ -41,6 +41,8 @@ use MediaWiki\Block\UnblockUserFactory;
use MediaWiki\Cache\BacklinkCacheFactory;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Collation\CollationFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\Config\ConfigRepository;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Transform\ContentTransformer;
@ -756,6 +758,14 @@ class MediaWikiServices extends ServiceContainer {
return $this->getService( 'CollationFactory' );
}
/**
* @return CommentFormatter
* @since 1.38
*/
public function getCommentFormatter(): CommentFormatter {
return $this->getService( 'CommentFormatter' );
}
/**
* @since 1.31
* @return CommentStore
@ -1474,6 +1484,14 @@ class MediaWikiServices extends ServiceContainer {
return $this->getService( 'RollbackPageFactory' );
}
/**
* @since 1.38
* @return RowCommentFormatter
*/
public function getRowCommentFormatter(): RowCommentFormatter {
return $this->getService( 'RowCommentFormatter' );
}
/**
* @since 1.27
* @return SearchEngine

View file

@ -8,6 +8,7 @@ use ContribsPager;
use FauxRequest;
use IContextSource;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\Permissions\Authority;
@ -43,6 +44,9 @@ class ContributionsLookup {
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @param RevisionStore $revisionStore
* @param LinkRendererFactory $linkRendererFactory
@ -51,6 +55,7 @@ class ContributionsLookup {
* @param ILoadBalancer $loadBalancer
* @param ActorMigration $actorMigration
* @param NamespaceInfo $namespaceInfo
* @param CommentFormatter $commentFormatter
*/
public function __construct(
RevisionStore $revisionStore,
@ -59,7 +64,8 @@ class ContributionsLookup {
HookContainer $hookContainer,
ILoadBalancer $loadBalancer,
ActorMigration $actorMigration,
NamespaceInfo $namespaceInfo
NamespaceInfo $namespaceInfo,
CommentFormatter $commentFormatter
) {
$this->revisionStore = $revisionStore;
$this->linkRendererFactory = $linkRendererFactory;
@ -68,6 +74,7 @@ class ContributionsLookup {
$this->loadBalancer = $loadBalancer;
$this->actorMigration = $actorMigration;
$this->namespaceInfo = $namespaceInfo;
$this->commentFormatter = $commentFormatter;
}
/**
@ -272,7 +279,8 @@ class ContributionsLookup {
$this->actorMigration,
$this->revisionStore,
$this->namespaceInfo,
$targetUser
$targetUser,
$this->commentFormatter
);
}

View file

@ -61,6 +61,9 @@ use MediaWiki\Block\UserBlockCommandFactory;
use MediaWiki\Cache\BacklinkCacheFactory;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Collation\CollationFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\CommentParserFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\Config\ConfigRepository;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Content\ContentHandlerFactory;
@ -338,6 +341,21 @@ return [
);
},
'CommentFormatter' => static function ( MediaWikiServices $services ): CommentFormatter {
$parserFactory = new CommentParserFactory(
$services->getLinkRenderer(),
$services->getLinkBatchFactory(),
$services->getLinkCache(),
$services->getRepoGroup(),
RequestContext::getMain()->getLanguage(),
$services->getContentLanguage(),
$services->getTitleParser(),
$services->getNamespaceInfo(),
$services->getHookContainer()
);
return new CommentFormatter( $parserFactory );
},
'CommentStore' => static function ( MediaWikiServices $services ): CommentStore {
return new CommentStore(
$services->getContentLanguage(),
@ -404,7 +422,8 @@ return [
$services->getHookContainer(),
$services->getDBLoadBalancer(),
$services->getActorMigration(),
$services->getNamespaceInfo()
$services->getNamespaceInfo(),
$services->getCommentFormatter()
);
},
@ -1447,6 +1466,24 @@ return [
return $services->get( '_PageCommandFactory' );
},
'RowCommentFormatter' => static function ( MediaWikiServices $services ): RowCommentFormatter {
$parserFactory = new CommentParserFactory(
$services->getLinkRenderer(),
$services->getLinkBatchFactory(),
$services->getLinkCache(),
$services->getRepoGroup(),
RequestContext::getMain()->getLanguage(),
$services->getContentLanguage(),
$services->getTitleParser(),
$services->getNamespaceInfo(),
$services->getHookContainer()
);
return new RowCommentFormatter(
$parserFactory,
$services->getCommentStore()
);
},
'SearchEngineConfig' => static function ( MediaWikiServices $services ): SearchEngineConfig {
// @todo This should not take a Config object, but it's not so easy to remove because it
// exposes it in a getter, which is actually used.

View file

@ -174,6 +174,7 @@ class ActionFactory {
'RollbackPageFactory',
'UserOptionsLookup',
'WatchlistManager',
'CommentFormatter'
],
],
'unwatch' => [

View file

@ -312,7 +312,8 @@ class HistoryAction extends FormlessAction {
$conds,
$d,
$services->getLinkBatchFactory(),
$watchlistManager
$watchlistManager,
$services->getCommentFormatter()
);
$out->addHTML(
$pager->getNavigationBar() .
@ -405,11 +406,15 @@ class HistoryAction extends FormlessAction {
$items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT );
// Preload comments
$formattedComments = MediaWikiServices::getInstance()->getRowCommentFormatter()
->formatRows( $items, 'rev_comment' );
// Generate feed elements enclosed between header and footer.
$feed->outHeader();
if ( $items->numRows() ) {
foreach ( $items as $row ) {
$feed->outItem( $this->feedItem( $row ) );
foreach ( $items as $i => $row ) {
$feed->outItem( $this->feedItem( $row, $formattedComments[$i] ) );
}
} else {
$feed->outItem( $this->feedEmpty() );
@ -434,19 +439,20 @@ class HistoryAction extends FormlessAction {
* includes a diff to the previous revision (if any).
*
* @param stdClass|array $row Database row
* @param string $formattedComment The comment in HTML format
* @return FeedItem
*/
private function feedItem( $row ) {
private function feedItem( $row, $formattedComment ) {
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
$rev = $revisionStore->newRevisionFromRow( $row, 0, $this->getTitle() );
$prevRev = $revisionStore->getPreviousRevision( $rev );
$revComment = $rev->getComment() === null ? null : $rev->getComment()->text;
$text = FeedUtils::formatDiffRow(
$text = FeedUtils::formatDiffRow2(
$this->getTitle(),
$prevRev ? $prevRev->getId() : false,
$rev->getId(),
$rev->getTimestamp(),
$revComment
$formattedComment
);
$revUserText = $rev->getUser() ? $rev->getUser()->getName() : '';
if ( $revComment == '' ) {

View file

@ -20,6 +20,7 @@
* @ingroup Actions
*/
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Page\RollbackPageFactory;
use MediaWiki\Revision\RevisionRecord;
@ -46,6 +47,9 @@ class RollbackAction extends FormAction {
/** @var WatchlistManager */
private $watchlistManager;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @param Page $page
* @param IContextSource|null $context
@ -53,6 +57,7 @@ class RollbackAction extends FormAction {
* @param RollbackPageFactory $rollbackPageFactory
* @param UserOptionsLookup $userOptionsLookup
* @param WatchlistManager $watchlistManager
* @param CommentFormatter $commentFormatter
*/
public function __construct(
Page $page,
@ -60,13 +65,15 @@ class RollbackAction extends FormAction {
IContentHandlerFactory $contentHandlerFactory,
RollbackPageFactory $rollbackPageFactory,
UserOptionsLookup $userOptionsLookup,
WatchlistManager $watchlistManager
WatchlistManager $watchlistManager,
CommentFormatter $commentFormatter
) {
parent::__construct( $page, $context );
$this->contentHandlerFactory = $contentHandlerFactory;
$this->rollbackPageFactory = $rollbackPageFactory;
$this->userOptionsLookup = $userOptionsLookup;
$this->watchlistManager = $watchlistManager;
$this->commentFormatter = $commentFormatter;
}
public function getName() {
@ -188,9 +195,8 @@ class RollbackAction extends FormAction {
$this->getOutput()->addWikiMsg(
'editcomment',
Message::rawParam(
Linker::formatComment(
$current->getComment()->text
)
$this->commentFormatter
->format( $current->getComment()->text )
)
);
}

View file

@ -22,6 +22,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
@ -62,6 +63,19 @@ class HistoryPager extends ReverseChronologicalPager {
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @var RevisionRecord[] Revisions, with the key being their result offset
*/
private $revisions = [];
/**
* @var string[] Formatted comments, with the key being their result offset as for $revisions
*/
private $formattedComments = [];
/**
* @param HistoryAction $historyPage
* @param string $year
@ -71,6 +85,7 @@ class HistoryPager extends ReverseChronologicalPager {
* @param string $day
* @param LinkBatchFactory|null $linkBatchFactory
* @param WatchlistManager|null $watchlistManager
* @param CommentFormatter|null $commentFormatter
*/
public function __construct(
HistoryAction $historyPage,
@ -80,7 +95,8 @@ class HistoryPager extends ReverseChronologicalPager {
array $conds = [],
$day = '',
LinkBatchFactory $linkBatchFactory = null,
WatchlistManager $watchlistManager = null
WatchlistManager $watchlistManager = null,
CommentFormatter $commentFormatter = null
) {
parent::__construct( $historyPage->getContext() );
$this->historyPage = $historyPage;
@ -93,6 +109,7 @@ class HistoryPager extends ReverseChronologicalPager {
$this->linkBatchFactory = $linkBatchFactory ?? $services->getLinkBatchFactory();
$this->watchlistManager = $watchlistManager
?? $services->getWatchlistManager();
$this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
}
// For hook compatibility...
@ -148,7 +165,7 @@ class HistoryPager extends ReverseChronologicalPager {
*/
public function formatRow( $row ) {
if ( $this->lastRow ) {
$firstInList = $this->counter == 1;
$resultOffset = $this->counter - 1;
$this->counter++;
$notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' )
@ -156,7 +173,7 @@ class HistoryPager extends ReverseChronologicalPager {
->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
: false;
$s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
$s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, $resultOffset );
} else {
$s = '';
}
@ -171,9 +188,9 @@ class HistoryPager extends ReverseChronologicalPager {
}
# Do a link batch query
$this->mResult->seek( 0 );
$batch = $this->linkBatchFactory->newLinkBatch();
$revIds = [];
$title = $this->getTitle();
foreach ( $this->mResult as $row ) {
if ( $row->rev_parent_id ) {
$revIds[] = $row->rev_parent_id;
@ -185,9 +202,24 @@ class HistoryPager extends ReverseChronologicalPager {
$batch->add( NS_USER, $row->rev_user_text );
$batch->add( NS_USER_TALK, $row->rev_user_text );
}
$this->revisions[] = $this->revisionStore->newRevisionFromRow(
$row,
RevisionStore::READ_NORMAL,
$title
);
}
$this->parentLens = $this->revisionStore->getRevisionSizes( $revIds );
$batch->execute();
# The keys of $this->formattedComments will be the same as the keys of $this->revisions
$this->formattedComments = $this->commentFormatter->createRevisionBatch()
->revisions( $this->revisions )
->authority( $this->getAuthority() )
->samePage( false )
->hideIfDeleted( true )
->useParentheses( false )
->execute();
$this->mResult->seek( 0 );
}
@ -278,7 +310,7 @@ class HistoryPager extends ReverseChronologicalPager {
}
if ( $this->lastRow ) {
$firstInList = $this->counter == 1;
$resultOffset = $this->counter - 1;
if ( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
if ( $this->mOffset == '' ) {
@ -297,7 +329,7 @@ class HistoryPager extends ReverseChronologicalPager {
->getTitleNotificationTimestamp( $this->getUser(), $this->getTitle() )
: false;
$s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
$s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, $resultOffset );
} else {
$s = '';
}
@ -335,18 +367,12 @@ class HistoryPager extends ReverseChronologicalPager {
* @param mixed $next The database row corresponding to the next line
* (chronologically previous)
* @param bool|string $notificationtimestamp
* @param bool $dummy Unused.
* @param bool $firstInList Whether this row corresponds to the first
* displayed on this history page.
* @param int $resultOffset The offset into the current result set
* @return string HTML output for the row
*/
private function historyLine( $row, $next, $notificationtimestamp = false,
$dummy = false, $firstInList = false ) {
$revRecord = $this->revisionStore->newRevisionFromRow(
$row,
RevisionStore::READ_NORMAL,
$this->getTitle()
);
private function historyLine( $row, $next, $notificationtimestamp, $resultOffset ) {
$firstInList = $resultOffset === 0;
$revRecord = $this->revisions[$resultOffset];
if ( is_object( $next ) ) {
$previousRevRecord = $this->revisionStore->newRevisionFromRow(
@ -440,7 +466,7 @@ class HistoryPager extends ReverseChronologicalPager {
}
# Text following the character difference is added just before running hooks
$s2 = Linker::revComment( $revRecord, false, true, false );
$s2 = $this->formattedComments[$resultOffset];
if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) {
$s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>';

View file

@ -18,6 +18,7 @@
* @file
*/
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Transform\ContentTransformer;
use MediaWiki\Revision\MutableRevisionRecord;
@ -48,6 +49,9 @@ class ApiComparePages extends ApiBase {
/** @var ContentTransformer */
private $contentTransformer;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @param ApiMain $mainModule
* @param string $moduleName
@ -55,6 +59,7 @@ class ApiComparePages extends ApiBase {
* @param SlotRoleRegistry $slotRoleRegistry
* @param IContentHandlerFactory $contentHandlerFactory
* @param ContentTransformer $contentTransformer
* @param CommentFormatter $commentFormatter
*/
public function __construct(
ApiMain $mainModule,
@ -62,13 +67,15 @@ class ApiComparePages extends ApiBase {
RevisionStore $revisionStore,
SlotRoleRegistry $slotRoleRegistry,
IContentHandlerFactory $contentHandlerFactory,
ContentTransformer $contentTransformer
ContentTransformer $contentTransformer,
CommentFormatter $commentFormatter
) {
parent::__construct( $mainModule, $moduleName );
$this->revisionStore = $revisionStore;
$this->slotRoleRegistry = $slotRoleRegistry;
$this->contentHandlerFactory = $contentHandlerFactory;
$this->contentTransformer = $contentTransformer;
$this->commentFormatter = $commentFormatter;
}
public function execute() {
@ -639,7 +646,7 @@ class ApiComparePages extends ApiBase {
if ( isset( $this->props['comment'] ) ) {
$vals["{$prefix}comment"] = $comment->text;
}
$vals["{$prefix}parsedcomment"] = Linker::formatComment(
$vals["{$prefix}parsedcomment"] = $this->commentFormatter->format(
$comment->text, $title
);
}

View file

@ -22,6 +22,7 @@
use MediaWiki\Api\ApiHookRunner;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\ParamValidator\TypeDef\UserDef;
@ -64,6 +65,9 @@ class ApiFeedContributions extends ApiBase {
/** @var UserFactory */
private $userFactory;
/** @var CommentFormatter */
private $commentFormatter;
/** @var ApiHookRunner */
private $hookRunner;
@ -79,6 +83,7 @@ class ApiFeedContributions extends ApiBase {
* @param NamespaceInfo $namespaceInfo
* @param ActorMigration $actorMigration
* @param UserFactory $userFactory
* @param CommentFormatter $commentFormatter
*/
public function __construct(
ApiMain $main,
@ -91,7 +96,8 @@ class ApiFeedContributions extends ApiBase {
ILoadBalancer $loadBalancer,
NamespaceInfo $namespaceInfo,
ActorMigration $actorMigration,
UserFactory $userFactory
UserFactory $userFactory,
CommentFormatter $commentFormatter
) {
parent::__construct( $main, $action );
$this->revisionStore = $revisionStore;
@ -103,6 +109,7 @@ class ApiFeedContributions extends ApiBase {
$this->namespaceInfo = $namespaceInfo;
$this->actorMigration = $actorMigration;
$this->userFactory = $userFactory;
$this->commentFormatter = $commentFormatter;
$this->hookRunner = new ApiHookRunner( $hookContainer );
}
@ -178,7 +185,8 @@ class ApiFeedContributions extends ApiBase {
$this->actorMigration,
$this->revisionStore,
$this->namespaceInfo,
$targetUser
$targetUser,
$this->commentFormatter
);
$feedLimit = $this->getConfig()->get( 'FeedLimit' );

View file

@ -132,6 +132,7 @@ class ApiMain extends ApiBase {
'Parser',
'WikiPageFactory',
'ContentTransformer',
'CommentFormatter',
]
],
'stashedit' => [
@ -164,6 +165,7 @@ class ApiMain extends ApiBase {
'NamespaceInfo',
'ActorMigration',
'UserFactory',
'CommentFormatter',
]
],
'feedrecentchanges' => [
@ -200,6 +202,7 @@ class ApiMain extends ApiBase {
'SlotRoleRegistry',
'ContentHandlerFactory',
'ContentTransformer',
'CommentFormatter',
]
],
'checktoken' => [

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Transform\ContentTransformer;
use MediaWiki\Languages\LanguageNameUtils;
@ -73,6 +74,9 @@ class ApiParse extends ApiBase {
/** @var ContentTransformer */
private $contentTransformer;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @param ApiMain $main
* @param string $action
@ -85,6 +89,7 @@ class ApiParse extends ApiBase {
* @param Parser $parser
* @param WikiPageFactory $wikiPageFactory
* @param ContentTransformer $contentTransformer
* @param CommentFormatter $commentFormatter
*/
public function __construct(
ApiMain $main,
@ -97,7 +102,8 @@ class ApiParse extends ApiBase {
IContentHandlerFactory $contentHandlerFactory,
Parser $parser,
WikiPageFactory $wikiPageFactory,
ContentTransformer $contentTransformer
ContentTransformer $contentTransformer,
CommentFormatter $commentFormatter
) {
parent::__construct( $main, $action );
$this->revisionLookup = $revisionLookup;
@ -109,6 +115,7 @@ class ApiParse extends ApiBase {
$this->parser = $parser;
$this->wikiPageFactory = $wikiPageFactory;
$this->contentTransformer = $contentTransformer;
$this->commentFormatter = $commentFormatter;
}
private function getPoolKey(): string {
@ -792,7 +799,7 @@ class ApiParse extends ApiBase {
}
/**
* This mimicks the behavior of EditPage in formatting a summary
* This mimics the behavior of EditPage in formatting a summary
*
* @param Title $title of the page being parsed
* @param array $params The API parameters of the request
@ -812,7 +819,7 @@ class ApiParse extends ApiBase {
->inContentLanguage()->text();
}
}
return Linker::formatComment( $summary, $title, $this->section === 'new' );
return $this->commentFormatter->format( $summary, $title, $this->section === 'new' );
}
private function formatLangLinks( $links ) {

View file

@ -273,6 +273,7 @@ class ApiQuery extends ApiBase {
'class' => ApiQueryDeletedrevs::class,
'services' => [
'CommentStore',
'RowCommentFormatter',
'RevisionStore',
'ChangeTagDefStore',
'LinkBatchFactory',
@ -288,6 +289,7 @@ class ApiQuery extends ApiBase {
'class' => ApiQueryFilearchive::class,
'services' => [
'CommentStore',
'CommentFormatter',
],
],
'imageusage' => [
@ -303,6 +305,7 @@ class ApiQuery extends ApiBase {
'class' => ApiQueryLogEvents::class,
'services' => [
'CommentStore',
'RowCommentFormatter',
'ChangeTagDefStore',
],
],
@ -323,6 +326,7 @@ class ApiQuery extends ApiBase {
'class' => ApiQueryProtectedTitles::class,
'services' => [
'CommentStore',
'RowCommentFormatter'
],
],
'querypage' => [
@ -338,6 +342,7 @@ class ApiQuery extends ApiBase {
'class' => ApiQueryRecentChanges::class,
'services' => [
'CommentStore',
'RowCommentFormatter',
'ChangeTagDefStore',
'SlotRoleStore',
'SlotRoleRegistry',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
@ -41,6 +42,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
/** @var CommentStore */
private $commentStore;
/** @var RowCommentFormatter */
private $commentFormatter;
/** @var RevisionStore */
private $revisionStore;
@ -54,6 +58,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
* @param ApiQuery $query
* @param string $moduleName
* @param CommentStore $commentStore
* @param RowCommentFormatter $commentFormatter
* @param RevisionStore $revisionStore
* @param NameTableStore $changeTagDefStore
* @param LinkBatchFactory $linkBatchFactory
@ -62,12 +67,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
ApiQuery $query,
$moduleName,
CommentStore $commentStore,
RowCommentFormatter $commentFormatter,
RevisionStore $revisionStore,
NameTableStore $changeTagDefStore,
LinkBatchFactory $linkBatchFactory
) {
parent::__construct( $query, $moduleName, 'dr' );
$this->commentStore = $commentStore;
$this->commentFormatter = $commentFormatter;
$this->revisionStore = $revisionStore;
$this->changeTagDefStore = $changeTagDefStore;
$this->linkBatchFactory = $linkBatchFactory;
@ -295,6 +302,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->addWhereRange( 'ar_id', $dir, null, null );
}
$res = $this->select( __METHOD__ );
$formattedComments = [];
if ( $fld_parsedcomment ) {
$formattedComments = $this->commentFormatter->formatItems(
$this->commentFormatter->rows( $res )
->indexField( 'ar_id' )
->commentKey( 'ar_comment' )
->namespaceField( 'ar_namespace' )
->titleField( 'ar_title' )
);
}
$pageMap = []; // Maps ns&title to (fake) pageid
$count = 0;
$newPageID = 0;
@ -355,8 +374,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$rev['comment'] = $comment;
}
if ( $fld_parsedcomment ) {
$title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
$rev['parsedcomment'] = Linker::formatComment( $comment, $title );
$rev['parsedcomment'] = $formattedComments[$row->ar_id];
}
}
}

View file

@ -24,6 +24,8 @@
* @file
*/
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\CommentItem;
use MediaWiki\Revision\RevisionRecord;
/**
@ -36,18 +38,24 @@ class ApiQueryFilearchive extends ApiQueryBase {
/** @var CommentStore */
private $commentStore;
/** @var CommentFormatter */
private $commentFormatter;
/**
* @param ApiQuery $query
* @param string $moduleName
* @param CommentStore $commentStore
* @param CommentFormatter $commentFormatter
*/
public function __construct(
ApiQuery $query,
$moduleName,
CommentStore $commentStore
CommentStore $commentStore,
CommentFormatter $commentFormatter
) {
parent::__construct( $query, $moduleName, 'fa' );
$this->commentStore = $commentStore;
$this->commentFormatter = $commentFormatter;
}
public function execute() {
@ -63,6 +71,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
$fld_size = isset( $prop['size'] );
$fld_dimensions = isset( $prop['dimensions'] );
$fld_description = isset( $prop['description'] ) || isset( $prop['parseddescription'] );
$fld_parseddescription = isset( $prop['parseddescription'] );
$fld_mime = isset( $prop['mime'] );
$fld_mediatype = isset( $prop['mediatype'] );
$fld_metadata = isset( $prop['metadata'] );
@ -151,6 +160,22 @@ class ApiQueryFilearchive extends ApiQueryBase {
$res = $this->select( __METHOD__ );
// Format descriptions in a batch
$formattedDescriptions = [];
$descriptions = [];
if ( $fld_parseddescription ) {
$commentItems = [];
foreach ( $res as $row ) {
$desc = $this->commentStore->getComment( 'fa_description', $row )->text;
$descriptions[$row->fa_id] = $desc;
$commentItems[$row->fa_id] = ( new CommentItem( $desc ) )
->selfLinkTarget( new TitleValue( NS_FILE, $row->fa_name ) );
}
$formattedDescriptions = $this->commentFormatter->createBatch()
->comments( $commentItems )
->execute();
}
$count = 0;
$result = $this->getResult();
foreach ( $res as $row ) {
@ -174,10 +199,11 @@ class ApiQueryFilearchive extends ApiQueryBase {
if ( $fld_description &&
RevisionRecord::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user )
) {
$file['description'] = $this->commentStore->getComment( 'fa_description', $row )->text;
if ( isset( $prop['parseddescription'] ) ) {
$file['parseddescription'] = Linker::formatComment(
$file['description'], $title );
$file['parseddescription'] = $formattedDescriptions[$row->fa_id];
$file['description'] = $descriptions[$row->fa_id];
} else {
$file['description'] = $this->commentStore->getComment( 'fa_description', $row )->text;
}
}
if ( $fld_user &&

View file

@ -20,6 +20,8 @@
* @file
*/
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Storage\NameTableAccessException;
use MediaWiki\Storage\NameTableStore;
@ -34,23 +36,32 @@ class ApiQueryLogEvents extends ApiQueryBase {
/** @var CommentStore */
private $commentStore;
/** @var CommentFormatter */
private $commentFormatter;
/** @var NameTableStore */
private $changeTagDefStore;
/** @var string[]|null */
private $formattedComments;
/**
* @param ApiQuery $query
* @param string $moduleName
* @param CommentStore $commentStore
* @param RowCommentFormatter $commentFormatter
* @param NameTableStore $changeTagDefStore
*/
public function __construct(
ApiQuery $query,
$moduleName,
CommentStore $commentStore,
RowCommentFormatter $commentFormatter,
NameTableStore $changeTagDefStore
) {
parent::__construct( $query, $moduleName, 'le' );
$this->commentStore = $commentStore;
$this->commentFormatter = $commentFormatter;
$this->changeTagDefStore = $changeTagDefStore;
}
@ -254,6 +265,15 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( $this->fld_title ) {
$this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'log' );
}
if ( $this->fld_parsedcomment ) {
$this->formattedComments = $this->commentFormatter->formatItems(
$this->commentFormatter->rows( $res )
->commentKey( 'log_comment' )
->indexField( 'log_id' )
->namespaceField( 'log_namespace' )
->titleField( 'log_title' )
);
}
$result = $this->getResult();
foreach ( $res as $row ) {
@ -286,7 +306,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
$vals['logid'] = (int)$row->log_id;
}
if ( $this->fld_title || $this->fld_parsedcomment ) {
if ( $this->fld_title ) {
$title = Title::makeTitle( $row->log_namespace, $row->log_title );
}
@ -342,13 +362,13 @@ class ApiQueryLogEvents extends ApiQueryBase {
$anyHidden = true;
}
if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
$comment = $this->commentStore->getComment( 'log_comment', $row )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
$vals['comment'] = $this->commentStore->getComment( 'log_comment', $row )->text;
}
if ( $this->fld_parsedcomment ) {
$vals['parsedcomment'] = Linker::formatComment( $comment, $title );
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$vals['parsedcomment'] = $this->formattedComments[$row->log_id];
}
}
}

View file

@ -20,6 +20,8 @@
* @file
*/
use MediaWiki\CommentFormatter\RowCommentFormatter;
/**
* Query module to enumerate all create-protected pages.
*
@ -30,18 +32,24 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
/** @var CommentStore */
private $commentStore;
/** @var RowCommentFormatter */
private $commentFormatter;
/**
* @param ApiQuery $query
* @param string $moduleName
* @param CommentStore $commentStore
* @param RowCommentFormatter $commentFormatter
*/
public function __construct(
ApiQuery $query,
$moduleName,
CommentStore $commentStore
CommentStore $commentStore,
RowCommentFormatter $commentFormatter
) {
parent::__construct( $query, $moduleName, 'pt' );
$this->commentStore = $commentStore;
$this->commentFormatter = $commentFormatter;
}
public function execute() {
@ -112,6 +120,14 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
if ( $resultPageSet === null ) {
$this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'pt' );
if ( isset( $prop['parsedcomment'] ) ) {
$formattedComments = $this->commentFormatter->formatItems(
$this->commentFormatter->rows( $res )
->commentKey( 'pt_reason' )
->namespaceField( 'pt_namespace' )
->titleField( 'pt_title' )
);
}
}
$count = 0;
@ -119,7 +135,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
$titles = [];
foreach ( $res as $row ) {
foreach ( $res as $rowOffset => $row ) {
if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that there are
// additional pages to be had. Stop here...
@ -150,9 +166,8 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
}
if ( isset( $prop['parsedcomment'] ) ) {
$vals['parsedcomment'] = Linker::formatComment(
$this->commentStore->getComment( 'pt_reason', $row )->text
);
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
$vals['parsedcomment'] = $formattedComments[$rowOffset];
}
if ( isset( $prop['expiry'] ) ) {

View file

@ -20,6 +20,7 @@
* @file
*/
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRoleRegistry;
@ -37,6 +38,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/** @var CommentStore */
private $commentStore;
/** @var RowCommentFormatter */
private $commentFormatter;
/** @var NameTableStore */
private $changeTagDefStore;
@ -46,10 +50,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/** @var SlotRoleRegistry */
private $slotRoleRegistry;
private $formattedComments = [];
/**
* @param ApiQuery $query
* @param string $moduleName
* @param CommentStore $commentStore
* @param RowCommentFormatter $commentFormatter
* @param NameTableStore $changeTagDefStore
* @param NameTableStore $slotRoleStore
* @param SlotRoleRegistry $slotRoleRegistry
@ -58,12 +65,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
ApiQuery $query,
$moduleName,
CommentStore $commentStore,
RowCommentFormatter $commentFormatter,
NameTableStore $changeTagDefStore,
NameTableStore $slotRoleStore,
SlotRoleRegistry $slotRoleRegistry
) {
parent::__construct( $query, $moduleName, 'rc' );
$this->commentStore = $commentStore;
$this->commentFormatter = $commentFormatter;
$this->changeTagDefStore = $changeTagDefStore;
$this->slotRoleStore = $slotRoleStore;
$this->slotRoleRegistry = $slotRoleRegistry;
@ -413,9 +422,19 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
/* Perform the actual query. */
$res = $this->select( __METHOD__, [], $hookData );
// Do batch queries
if ( $this->fld_title && $resultPageSet === null ) {
$this->executeGenderCacheFromResultWrapper( $res, __METHOD__, 'rc' );
}
if ( $this->fld_parsedcomment ) {
$this->formattedComments = $this->commentFormatter->formatItems(
$this->commentFormatter->rows( $res )
->indexField( 'rc_id' )
->commentKey( 'rc_comment' )
->namespaceField( 'rc_namespace' )
->titleField( 'rc_title' )
);
}
$revids = [];
$titles = [];
@ -560,13 +579,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
if ( RevisionRecord::userCanBitfield(
$row->rc_deleted, RevisionRecord::DELETED_COMMENT, $user
) ) {
$comment = $this->commentStore->getComment( 'rc_comment', $row )->text;
if ( $this->fld_comment ) {
$vals['comment'] = $comment;
$vals['comment'] = $this->commentStore->getComment( 'rc_comment', $row )->text;
}
if ( $this->fld_parsedcomment ) {
$vals['parsedcomment'] = Linker::formatComment( $comment, $title );
$vals['parsedcomment'] = $this->formattedComments[$row->rc_id];
}
}
}

View file

@ -93,7 +93,15 @@ class ChangesFeed {
}
}
$nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
$services = MediaWikiServices::getInstance();
$commentFormatter = $services->getRowCommentFormatter();
$formattedComments = $commentFormatter->formatItems(
$commentFormatter->rows( $rows )
->commentKey( 'rc_comment' )
->indexField( 'rc_id' )
);
$nsInfo = $services->getNamespaceInfo();
foreach ( $sorted as $obj ) {
$title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
$talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace ) && $title->canExist()
@ -117,7 +125,7 @@ class ChangesFeed {
$items[] = new FeedItem(
$title->getPrefixedText(),
FeedUtils::formatDiff( $obj ),
FeedUtils::formatDiff( $obj, $formattedComments[$obj->rc_id] ),
$url,
$obj->rc_timestamp,
( $obj->rc_deleted & RevisionRecord::DELETED_USER )

View file

@ -22,6 +22,7 @@
* @file
*/
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MediaWikiServices;
@ -56,6 +57,16 @@ class ChangesList extends ContextSource {
*/
protected $linkRenderer;
/**
* @var RowCommentFormatter
*/
protected $commentFormatter;
/**
* @var string[] Comments indexed by rc_id
*/
protected $formattedComments;
/**
* @var ChangesListFilterGroup[]
*/
@ -69,8 +80,11 @@ class ChangesList extends ContextSource {
$this->setContext( $context );
$this->preCacheMessages();
$this->watchMsgCache = new MapCacheLRU( 50 );
$this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
$this->filterGroups = $filterGroups;
$services = MediaWikiServices::getInstance();
$this->linkRenderer = $services->getLinkRenderer();
$this->commentFormatter = $services->getRowCommentFormatter();
}
/**
@ -308,6 +322,16 @@ class ChangesList extends ContextSource {
*/
public function initChangesListRows( $rows ) {
$this->getHookRunner()->onChangesListInitRows( $this, $rows );
$this->formattedComments = $this->commentFormatter->createBatch()
->comments(
$this->commentFormatter->rows( $rows )
->commentKey( 'rc_comment' )
->namespaceField( 'rc_namespace' )
->titleField( 'rc_title' )
->indexField( 'rc_id' )
)
->useBlock()
->execute();
}
/**
@ -696,14 +720,21 @@ class ChangesList extends ContextSource {
}
return ' <span class="' . $deletedClass . ' comment">' .
$this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
} elseif ( isset( $rc->mAttribs['rc_id'] )
&& isset( $this->formattedComments[$rc->mAttribs['rc_id']] )
) {
return $this->formattedComments[$rc->mAttribs['rc_id']];
} else {
return Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle(),
return $this->commentFormatter->formatBlock(
$rc->mAttribs['rc_comment'],
$rc->getTitle(),
// Whether section links should refer to local page (using default false)
false,
// wikid to generate links for (using default null) */
null,
// whether parentheses should be rendered as part of the message
false );
false
);
}
}

View file

@ -111,11 +111,13 @@ class ImageHistoryList extends ContextSource {
}
/**
* @internal
* @param bool $iscur
* @param File $file
* @param string $formattedComment
* @return string
*/
public function imageHistoryLine( $iscur, $file ) {
public function imageHistoryLine( $iscur, $file, $formattedComment ) {
$user = $this->getUser();
$lang = $this->getLanguage();
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
@ -274,7 +276,7 @@ class ImageHistoryList extends ContextSource {
$row .= Html::rawElement(
'td',
[ 'dir' => $contLang->getDir() ],
Linker::formatComment( $description, $this->title )
$formattedComment
);
}

View file

@ -112,10 +112,12 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
public function getBody() {
$s = '';
$this->doQuery();
$formattedComments = [];
if ( count( $this->mHist ) ) {
if ( $this->mImg->isLocal() ) {
// Do a batch existence check for user pages and talkpages
// Do a batch existence check for user pages and talkpages. Format comments.
$linkBatch = $this->linkBatchFactory->newLinkBatch();
$comments = [];
for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
$file = $this->mHist[$i];
$uploader = $file->getUploader( File::FOR_THIS_USER, $this->getAuthority() );
@ -123,8 +125,11 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
$linkBatch->add( NS_USER, $uploader->getName() );
$linkBatch->add( NS_USER_TALK, $uploader->getName() );
}
$comments[$i] = $file->getDescription( File::FOR_THIS_USER, $this->getUser() );
}
$linkBatch->execute();
$formattedComments = MediaWikiServices::getInstance()->getCommentFormatter()
->formatStrings( $comments, $this->getTitle() );
}
$list = new ImageHistoryList( $this->mImagePage );
@ -134,7 +139,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
// Skip rows there just for paging links
for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) {
$file = $this->mHist[$i];
$s .= $list->imageHistoryLine( !$file->isOld(), $file );
$s .= $list->imageHistoryLine( !$file->isOld(), $file, $formattedComments[$i] );
}
$s .= $list->endImageHistoryList( $navLink );

View file

@ -306,6 +306,13 @@ abstract class IndexPager extends ContextSource implements Pager {
return $this->mResult;
}
/**
* @return int The current offset into the result. Valid during formatRow().
*/
public function getResultOffset() {
return $this->mResult->key();
}
/**
* Set the offset from an other source than the request
*

View file

@ -144,6 +144,7 @@ class SpecialPageFactory {
'DBLoadBalancer',
'CommentStore',
'UserCache',
'RowCommentFormatter',
]
],
'Protectedtitles' => [
@ -380,6 +381,7 @@ class SpecialPageFactory {
'CommentStore',
'BlockUtils',
'BlockActionInfo',
'RowCommentFormatter',
],
],
'AutoblockList' => [
@ -391,6 +393,7 @@ class SpecialPageFactory {
'CommentStore',
'BlockUtils',
'BlockActionInfo',
'RowCommentFormatter',
],
],
'ChangePassword' => [
@ -445,6 +448,7 @@ class SpecialPageFactory {
'UserNameUtils',
'UserNamePrefixSearch',
'UserOptionsLookup',
'CommentFormatter',
'UserFactory',
]
],

View file

@ -25,6 +25,7 @@ use MediaWiki\Block\BlockActionInfo;
use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Block\BlockUtils;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use Wikimedia\Rdbms\ILoadBalancer;
/**
@ -53,6 +54,9 @@ class SpecialAutoblockList extends SpecialPage {
/** @var BlockActionInfo */
private $blockActionInfo;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
/**
* @param LinkBatchFactory $linkBatchFactory
* @param BlockRestrictionStore $blockRestrictionStore
@ -60,6 +64,7 @@ class SpecialAutoblockList extends SpecialPage {
* @param CommentStore $commentStore
* @param BlockUtils $blockUtils
* @param BlockActionInfo $blockActionInfo
* @param RowCommentFormatter $rowCommentFormatter
*/
public function __construct(
LinkBatchFactory $linkBatchFactory,
@ -67,7 +72,8 @@ class SpecialAutoblockList extends SpecialPage {
ILoadBalancer $loadBalancer,
CommentStore $commentStore,
BlockUtils $blockUtils,
BlockActionInfo $blockActionInfo
BlockActionInfo $blockActionInfo,
RowCommentFormatter $rowCommentFormatter
) {
parent::__construct( 'AutoblockList' );
@ -77,6 +83,7 @@ class SpecialAutoblockList extends SpecialPage {
$this->commentStore = $commentStore;
$this->blockUtils = $blockUtils;
$this->blockActionInfo = $blockActionInfo;
$this->rowCommentFormatter = $rowCommentFormatter;
}
/**
@ -140,7 +147,8 @@ class SpecialAutoblockList extends SpecialPage {
$this->getSpecialPageFactory(),
$this->commentStore,
$this->blockUtils,
$this->blockActionInfo
$this->blockActionInfo,
$this->rowCommentFormatter
);
}

View file

@ -26,6 +26,7 @@ use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Block\BlockUtils;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
@ -60,13 +61,17 @@ class SpecialBlockList extends SpecialPage {
/** @var BlockActionInfo */
private $blockActionInfo;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
public function __construct(
LinkBatchFactory $linkBatchFactory,
BlockRestrictionStore $blockRestrictionStore,
ILoadBalancer $loadBalancer,
CommentStore $commentStore,
BlockUtils $blockUtils,
BlockActionInfo $blockActionInfo
BlockActionInfo $blockActionInfo,
RowCommentFormatter $rowCommentFormatter
) {
parent::__construct( 'BlockList' );
@ -76,6 +81,7 @@ class SpecialBlockList extends SpecialPage {
$this->commentStore = $commentStore;
$this->blockUtils = $blockUtils;
$this->blockActionInfo = $blockActionInfo;
$this->rowCommentFormatter = $rowCommentFormatter;
}
/**
@ -249,7 +255,8 @@ class SpecialBlockList extends SpecialPage {
$this->getSpecialPageFactory(),
$this->commentStore,
$this->blockUtils,
$this->blockActionInfo
$this->blockActionInfo,
$this->rowCommentFormatter
);
}

View file

@ -23,6 +23,7 @@
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
@ -69,6 +70,9 @@ class SpecialContributions extends IncludableSpecialPage {
/** @var UserOptionsLookup */
private $userOptionsLookup;
/** @var CommentFormatter */
private $commentFormatter;
/** @var UserFactory */
private $userFactory;
@ -85,6 +89,7 @@ class SpecialContributions extends IncludableSpecialPage {
* @param UserNameUtils|null $userNameUtils
* @param UserNamePrefixSearch|null $userNamePrefixSearch
* @param UserOptionsLookup|null $userOptionsLookup
* @param CommentFormatter|null $commentFormatter
* @param UserFactory|null $userFactory
*/
public function __construct(
@ -97,6 +102,7 @@ class SpecialContributions extends IncludableSpecialPage {
UserNameUtils $userNameUtils = null,
UserNamePrefixSearch $userNamePrefixSearch = null,
UserOptionsLookup $userOptionsLookup = null,
CommentFormatter $commentFormatter = null,
UserFactory $userFactory = null
) {
parent::__construct( 'Contributions' );
@ -111,6 +117,7 @@ class SpecialContributions extends IncludableSpecialPage {
$this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
$this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
$this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
$this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
$this->userFactory = $userFactory ?? $services->getUserFactory();
}
@ -858,7 +865,8 @@ class SpecialContributions extends IncludableSpecialPage {
$this->actorMigration,
$this->revisionStore,
$this->namespaceInfo,
$targetUser
$targetUser,
$this->commentFormatter
);
}

View file

@ -22,6 +22,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use Wikimedia\Rdbms\ILoadBalancer;
/**
@ -45,23 +46,29 @@ class SpecialProtectedpages extends SpecialPage {
/** @var UserCache */
private $userCache;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
/**
* @param LinkBatchFactory $linkBatchFactory
* @param ILoadBalancer $loadBalancer
* @param CommentStore $commentStore
* @param UserCache $userCache
* @param RowCommentFormatter $rowCommentFormatter
*/
public function __construct(
LinkBatchFactory $linkBatchFactory,
ILoadBalancer $loadBalancer,
CommentStore $commentStore,
UserCache $userCache
UserCache $userCache,
RowCommentFormatter $rowCommentFormatter
) {
parent::__construct( 'Protectedpages' );
$this->linkBatchFactory = $linkBatchFactory;
$this->loadBalancer = $loadBalancer;
$this->commentStore = $commentStore;
$this->userCache = $userCache;
$this->rowCommentFormatter = $rowCommentFormatter;
}
public function execute( $par ) {
@ -97,7 +104,8 @@ class SpecialProtectedpages extends SpecialPage {
$this->linkBatchFactory,
$this->loadBalancer,
$this->commentStore,
$this->userCache
$this->userCache,
$this->rowCommentFormatter
);
$this->getOutput()->addHTML( $this->showOptions(

View file

@ -27,6 +27,7 @@ use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\SpecialPage\SpecialPageFactory;
use MediaWiki\User\UserIdentity;
use Wikimedia\IPUtils;
@ -65,6 +66,12 @@ class BlockListPager extends TablePager {
/** @var BlockActionInfo */
private $blockActionInfo;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
/** @var string[] */
private $formattedComments = [];
/**
* @param SpecialPage $page
* @param array $conds
@ -75,6 +82,7 @@ class BlockListPager extends TablePager {
* @param CommentStore $commentStore
* @param BlockUtils $blockUtils
* @param BlockActionInfo $blockActionInfo
* @param RowCommentFormatter $rowCommentFormatter
*/
public function __construct(
$page,
@ -85,7 +93,8 @@ class BlockListPager extends TablePager {
SpecialPageFactory $specialPageFactory,
CommentStore $commentStore,
BlockUtils $blockUtils,
BlockActionInfo $blockActionInfo
BlockActionInfo $blockActionInfo,
RowCommentFormatter $rowCommentFormatter
) {
$this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
parent::__construct( $page->getContext(), $page->getLinkRenderer() );
@ -97,6 +106,7 @@ class BlockListPager extends TablePager {
$this->commentStore = $commentStore;
$this->blockUtils = $blockUtils;
$this->blockActionInfo = $blockActionInfo;
$this->rowCommentFormatter = $rowCommentFormatter;
}
protected function getFieldNames() {
@ -243,8 +253,7 @@ class BlockListPager extends TablePager {
break;
case 'ipb_reason':
$value = $this->commentStore->getComment( 'ipb_reason', $row )->text;
$formatted = Linker::formatComment( $value );
$formatted = $this->formattedComments[$this->getResultOffset()];
break;
case 'ipb_params':
@ -469,7 +478,7 @@ class BlockListPager extends TablePager {
* @param IResultWrapper $result
*/
public function preprocessResults( $result ) {
# Do a link batch query
// Do a link batch query
$lb = $this->linkBatchFactory->newLinkBatch();
$lb->setCaller( __METHOD__ );
@ -505,6 +514,10 @@ class BlockListPager extends TablePager {
}
$lb->execute();
// Format comments
// The keys of formattedComments will be the corresponding offset into $result
$this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'ipb_reason' );
}
}

View file

@ -20,6 +20,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Linker\LinkRenderer;
@ -127,6 +128,15 @@ class ContribsPager extends RangeChronologicalPager {
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var CommentFormatter */
private $commentFormatter;
/** @var string[] */
private $formattedComments = [];
/** @var RevisionRecord[] Cached revisions by ID */
private $revisions = [];
/**
* @param IContextSource $context
* @param array $options
@ -138,6 +148,7 @@ class ContribsPager extends RangeChronologicalPager {
* @param RevisionStore|null $revisionStore
* @param NamespaceInfo|null $namespaceInfo
* @param UserIdentity|null $targetUser
* @param CommentFormatter|null $commentFormatter
*/
public function __construct(
IContextSource $context,
@ -149,7 +160,8 @@ class ContribsPager extends RangeChronologicalPager {
ActorMigration $actorMigration = null,
RevisionStore $revisionStore = null,
NamespaceInfo $namespaceInfo = null,
UserIdentity $targetUser = null
UserIdentity $targetUser = null,
CommentFormatter $commentFormatter = null
) {
// Class is used directly in extensions - T266484
$services = MediaWikiServices::getInstance();
@ -225,6 +237,7 @@ class ContribsPager extends RangeChronologicalPager {
$this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );
$this->revisionStore = $revisionStore ?? $services->getRevisionStore();
$this->namespaceInfo = $namespaceInfo ?? $services->getNamespaceInfo();
$this->commentFormatter = $commentFormatter ?? $services->getCommentFormatter();
}
public function getDefaultQuery() {
@ -567,28 +580,41 @@ class ContribsPager extends RangeChronologicalPager {
$this->mResult->seek( 0 );
$parentRevIds = [];
$this->mParentLens = [];
$batch = $this->linkBatchFactory->newLinkBatch();
$revisions = [];
$linkBatch = $this->linkBatchFactory->newLinkBatch();
$isIpRange = $this->isQueryableRange( $this->target );
# Give some pointers to make (last) links
foreach ( $this->mResult as $row ) {
if ( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
$parentRevIds[] = $row->rev_parent_id;
}
if ( isset( $row->rev_id ) ) {
if ( $this->revisionStore->isRevisionRow( $row ) ) {
$this->mParentLens[$row->rev_id] = $row->rev_len;
if ( $isIpRange ) {
// If this is an IP range, batch the IP's talk page
$batch->add( NS_USER_TALK, $row->rev_user_text );
$linkBatch->add( NS_USER_TALK, $row->rev_user_text );
}
$batch->add( $row->page_namespace, $row->page_title );
$linkBatch->add( $row->page_namespace, $row->page_title );
$revisions[$row->rev_id] = $this->revisionStore->newRevisionFromRow( $row );
}
}
# Fetch rev_len for revisions not already scanned above
$this->mParentLens += $this->revisionStore->getRevisionSizes(
array_diff( $parentRevIds, array_keys( $this->mParentLens ) )
);
$batch->execute();
$this->mResult->seek( 0 );
$linkBatch->execute();
$this->formattedComments = $this->commentFormatter->createRevisionBatch()
->authority( $this->getAuthority() )
->revisions( $revisions )
->hideIfDeleted()
->execute();
# For performance, save the revision objects for later.
# The array is indexed by rev_id. doBatchLookups() may be called
# multiple times with different results, so merge the revisions array,
# ignoring any duplicates.
$this->revisions += $revisions;
}
/**
@ -606,24 +632,25 @@ class ContribsPager extends RangeChronologicalPager {
}
/**
* Check whether the revision associated is valid for formatting. If has no
* associated revision ID then null is returned.
*
* This was previously used by formatRow() but now exists only for the
* convenience of extensions.
* If the object looks like a revision row, or corresponds to a previously
* cached revision, return the RevisionRecord. Otherwise, return null.
*
* @since 1.35
* @deprecated since 1.37 use RevisionStore::isRevisionRow()
*
* @param stdClass $row
* @param mixed $row
* @param Title|null $title
* @return RevisionRecord|null
*/
public function tryCreatingRevisionRecord( $row, $title = null ) {
if ( !$this->revisionStore->isRevisionRow( $row ) ) {
if ( $row instanceof stdClass && isset( $row->rev_id )
&& isset( $this->revisions[$row->rev_id] )
) {
return $this->revisions[$row->rev_id];
} elseif ( $this->revisionStore->isRevisionRow( $row ) ) {
return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
} else {
return null;
}
return $this->revisionStore->newRevisionFromRow( $row, 0, $title );
}
/**
@ -656,7 +683,8 @@ class ContribsPager extends RangeChronologicalPager {
// skip formatting so that the row can be formatted by the
// ContributionsLineEnding hook below.
// FIXME: have some better way for extensions to provide formatted rows.
if ( $this->revisionStore->isRevisionRow( $row ) ) {
$revRecord = $this->tryCreatingRevisionRecord( $row, $page );
if ( $revRecord ) {
$revRecord = $this->revisionStore->newRevisionFromRow( $row, 0, $page );
$attribs['data-mw-revid'] = $revRecord->getId();
@ -732,7 +760,12 @@ class ContribsPager extends RangeChronologicalPager {
}
$lang = $this->getLanguage();
$comment = $lang->getDirMark() . Linker::revComment( $revRecord, false, true, false );
$comment = $lang->getDirMark() . (
$this->formattedComments[$row->rev_id] ??
$this->commentFormatter->formatRevision(
$revRecord, $user, false, true, false
) );
$d = ChangesList::revDateLink( $revRecord, $user, $lang, $page );
# When querying for an IP range, we want to always show user and user talk links.

View file

@ -20,6 +20,7 @@
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\Linker\LinkRenderer;
use Wikimedia\Rdbms\ILoadBalancer;
@ -37,6 +38,12 @@ class ProtectedPagesPager extends TablePager {
/** @var UserCache */
private $userCache;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
/** @var string[] */
private $formattedComments = [];
/**
* @param SpecialPage $form
* @param array $conds
@ -53,6 +60,7 @@ class ProtectedPagesPager extends TablePager {
* @param ILoadBalancer $loadBalancer
* @param CommentStore $commentStore
* @param UserCache $userCache
* @param RowCommentFormatter $rowCommentFormatter
*/
public function __construct(
$form,
@ -69,7 +77,8 @@ class ProtectedPagesPager extends TablePager {
LinkBatchFactory $linkBatchFactory,
ILoadBalancer $loadBalancer,
CommentStore $commentStore,
UserCache $userCache
UserCache $userCache,
RowCommentFormatter $rowCommentFormatter
) {
// Set database before parent constructor to avoid setting it there with wfGetDB
$this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
@ -86,6 +95,7 @@ class ProtectedPagesPager extends TablePager {
$this->linkBatchFactory = $linkBatchFactory;
$this->commentStore = $commentStore;
$this->userCache = $userCache;
$this->rowCommentFormatter = $rowCommentFormatter;
}
public function preprocessResults( $result ) {
@ -113,6 +123,9 @@ class ProtectedPagesPager extends TablePager {
}
$lb->execute();
// Format the comments
$this->formattedComments = $this->rowCommentFormatter->formatRows( $result, 'log_comment' );
}
protected function getFieldNames() {
@ -255,8 +268,7 @@ class ProtectedPagesPager extends TablePager {
LogPage::DELETED_COMMENT,
$this->getUser()
) ) {
$value = $this->commentStore->getComment( 'log_comment', $row )->text;
$formatted = Linker::formatComment( $value ?? '' );
$formatted = $this->formattedComments[$this->getResultOffset()];
} else {
$formatted = $this->msg( 'rev-deleted-comment' )->escaped();
}

View file

@ -1,5 +1,8 @@
<?php
use MediaWiki\CommentFormatter\CommentItem;
use MediaWiki\MediaWikiServices;
require_once __DIR__ . '/../includes/Benchmarker.php';
class BenchmarkCommentFormatter extends Benchmarker {
@ -23,11 +26,13 @@ class BenchmarkCommentFormatter extends Benchmarker {
}
$entries = $result['query']['recentchanges'];
$inputs = [];
$comments = [];
foreach ( $entries as $entry ) {
$inputs[] = [
'comment' => $entry['comment'],
'title' => Title::newFromText( $entry['title'] )
];
$comments[] = $entry['comment'];
}
$this->bench( [
'Linker::formatComment' => [
@ -41,6 +46,30 @@ class BenchmarkCommentFormatter extends Benchmarker {
}
},
],
'CommentFormatter::createBatch' => [
'function' => static function () use ( $inputs ) {
Title::clearCaches();
$formatter = MediaWikiServices::getInstance()->getCommentFormatter();
$comments = [];
foreach ( $inputs as $input ) {
$comments[] = ( new CommentItem( $input['comment'] ) )
->selfLinkTarget( $input['title'] );
}
$formatter->createBatch()
->comments( $comments )
->execute();
}
],
'CommentFormatter::formatStrings' => [
'function' => static function () use ( $comments ) {
Title::clearCaches();
$formatter = MediaWikiServices::getInstance()->getCommentFormatter();
$formatter->formatStrings( $comments );
}
],
] );
}
}

View file

@ -228,6 +228,9 @@ $wgAutoloadClasses += [
# tests/phpunit/unit/includes/auth
'MediaWiki\Tests\Unit\Auth\AuthenticationProviderTestTrait' => "$testDir/phpunit/unit/includes/auth/AuthenticationProviderTestTrait.php",
# tests/phpunit/unit/includes/CommentFormatter
'MediaWiki\Tests\Unit\CommentFormatter\CommentFormatterTestUtils' => "$testDir/phpunit/unit/includes/CommentFormatter/CommentFormatterTestUtils.php",
# tests/phpunit/unit/includes/editpage/Constraint and tests/phpunit/integration/includes/editpage/Constraint
'EditConstraintTestTrait' => "$testDir/phpunit/unit/includes/editpage/Constraint/EditConstraintTestTrait.php",

View file

@ -267,8 +267,9 @@ class LinkerTest extends MediaWikiLangTestCase {
/**
* @dataProvider provideCasesForFormatComment
* @covers Linker::formatComment
* @covers Linker::formatAutocomments
* @covers Linker::formatLinksInComment
* @covers \MediaWiki\CommentFormatter\CommentParser
* @covers \MediaWiki\CommentFormatter\CommentFormatter
*/
public function testFormatComment(
$expected, $comment, $title = false, $local = false, $wikiId = null
@ -490,6 +491,8 @@ class LinkerTest extends MediaWikiLangTestCase {
/**
* @covers Linker::formatLinksInComment
* @covers \MediaWiki\CommentFormatter\CommentParser
* @covers \MediaWiki\CommentFormatter\CommentFormatter
* @dataProvider provideCasesForFormatLinksInComment
*/
public function testFormatLinksInComment( $expected, $input, $wiki ) {
@ -666,53 +669,6 @@ class LinkerTest extends MediaWikiLangTestCase {
$this->assertEquals( $expected, $result );
}
/**
* @covers Linker::makeCommentLink
* @dataProvider provideMakeCommentLink
*/
public function testMakeCommentLink( $expected, $linkTarget, $text, $wikiId = null, $options = [] ) {
$conf = new SiteConfiguration();
$conf->settings = [
'wgServer' => [
'enwiki' => '//en.example.org'
],
'wgArticlePath' => [
'enwiki' => '/w/$1',
],
];
$conf->suffixes = [ 'wiki' ];
$this->setMwGlobals( [
'wgScript' => '/wiki/index.php',
'wgArticlePath' => '/wiki/$1',
'wgCapitalLinks' => true,
'wgConf' => $conf,
] );
$this->assertEquals( $expected, Linker::makeCommentLink( $linkTarget, $text, $wikiId, $options ) );
}
public static function provideMakeCommentLink() {
return [
[
'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Test</a>',
new TitleValue( NS_SPECIAL, 'BlankPage' ),
'Test'
],
'External comment link' => [
'<a class="external" rel="nofollow" href="//en.example.org/w/BlankPage">Test</a>',
new TitleValue( NS_MAIN, 'BlankPage' ),
'Test',
'enwiki'
],
'External special page comment link' => [
'<a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage">Test</a>',
new TitleValue( NS_SPECIAL, 'BlankPage' ),
'Test',
'enwiki'
],
];
}
/**
* @covers Linker::commentBlock
* @dataProvider provideCommentBlock

View file

@ -52,7 +52,8 @@ class RollbackActionTest extends MediaWikiIntegrationTestCase {
$mwServices->getContentHandlerFactory(),
$mwServices->getRollbackPageFactory(),
$mwServices->getUserOptionsLookup(),
$mwServices->getWatchlistManager()
$mwServices->getWatchlistManager(),
$mwServices->getCommentFormatter()
);
}

View file

@ -33,6 +33,9 @@ class ContribsPagerTest extends MediaWikiIntegrationTestCase {
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var CommentFormatter */
private $commentFormatter;
protected function setUp(): void {
parent::setUp();
@ -44,6 +47,7 @@ class ContribsPagerTest extends MediaWikiIntegrationTestCase {
$this->loadBalancer = $services->getDBLoadBalancer();
$this->actorMigration = $services->getActorMigration();
$this->namespaceInfo = $services->getNamespaceInfo();
$this->commentFormatter = $services->getCommentFormatter();
$this->pager = $this->getContribsPager( [
'start' => '2017-01-01',
'end' => '2017-02-02',
@ -61,7 +65,8 @@ class ContribsPagerTest extends MediaWikiIntegrationTestCase {
$this->actorMigration,
$this->revisionStore,
$this->namespaceInfo,
$targetUser
$targetUser,
$this->commentFormatter
);
}

View file

@ -7,6 +7,7 @@ use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\MediaWikiServices;
use MediaWiki\SpecialPage\SpecialPageFactory;
use Wikimedia\Rdbms\ILoadBalancer;
@ -41,6 +42,9 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
/** @var BlockActionInfo */
private $blockActionInfo;
/** @var RowCommentFormatter */
private $rowCommentFormatter;
protected function setUp(): void {
parent::setUp();
@ -52,6 +56,7 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
$this->commentStore = $services->getCommentStore();
$this->blockUtils = $services->getBlockUtils();
$this->blockActionInfo = $services->getBlockActionInfo();
$this->rowCommentFormatter = $services->getRowCommentFormatter();
}
private function getBlockListPager() {
@ -64,7 +69,8 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
$this->specialPageFactory,
$this->commentStore,
$this->blockUtils,
$this->blockActionInfo
$this->blockActionInfo,
$this->rowCommentFormatter
);
}
@ -229,6 +235,12 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
* @covers ::preprocessResults
*/
public function testPreprocessResults() {
$this->tablesUsed[] = 'ipblocks';
$this->tablesUsed[] = 'ipblocks_restrictions';
$this->tablesUsed[] = 'comment';
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'user';
// Test the Link Cache.
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$wrappedlinkCache = TestingAccessWrapper::newFromObject( $linkCache );
@ -238,7 +250,8 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
'User:127.0.0.1',
'User_talk:127.0.0.1',
$admin->getUserPage()->getPrefixedDBkey(),
$admin->getTalkPage()->getPrefixedDBkey()
$admin->getTalkPage()->getPrefixedDBkey(),
'Comment_link'
];
foreach ( $links as $link ) {
@ -251,9 +264,11 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
'ipb_by_text' => $admin->getName(),
'ipb_sitewide' => 1,
'ipb_timestamp' => $this->db->timestamp( wfTimestamp( TS_MW ) ),
'ipb_reason_text' => '[[Comment link]]',
'ipb_reason_data' => null,
];
$pager = $this->getBlockListPager();
$pager->preprocessResults( [ $row ] );
$pager->preprocessResults( new FakeResultWrapper( [ $row ] ) );
foreach ( $links as $link ) {
$this->assertSame( 1, $wrappedlinkCache->badLinks->get( $link ), "Bad link [[$link]]" );
@ -265,9 +280,11 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
'ipb_by' => $admin->getId(),
'ipb_by_text' => $admin->getName(),
'ipb_sitewide' => 1,
'ipb_reason_text' => '',
'ipb_reason_data' => null,
];
$pager = $this->getBlockListPager();
$pager->preprocessResults( [ $row ] );
$pager->preprocessResults( new FakeResultWrapper( [ $row ] ) );
$this->assertObjectNotHasAttribute( 'ipb_restrictions', $row );
@ -291,7 +308,11 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
$blockStore->insertBlock( $block );
$result = $this->db->select( 'ipblocks', [ '*' ], [ 'ipb_id' => $block->getId() ] );
$result = $this->db->newSelectQueryBuilder()
->queryInfo( DatabaseBlock::getQueryInfo() )
->where( [ 'ipb_id' => $block->getId() ] )
->caller( __METHOD__ )
->fetchResultSet();
$pager = $this->getBlockListPager();
$pager->preprocessResults( $result );
@ -306,8 +327,5 @@ class BlockListPagerTest extends MediaWikiIntegrationTestCase {
$this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleID() );
$this->assertEquals( $title->getDBkey(), $restriction->getTitle()->getDBkey() );
$this->assertEquals( $title->getNamespace(), $restriction->getTitle()->getNamespace() );
// Delete the block and the restrictions.
$blockStore->deleteBlock( $block );
}
}

View file

@ -0,0 +1,380 @@
<?php
namespace MediaWiki\Tests\Integration\CommentFormatter;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\CommentItem;
use MediaWiki\CommentFormatter\CommentParser;
use MediaWiki\CommentFormatter\CommentParserFactory;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Tests\Unit\CommentFormatter\CommentFormatterTestUtils;
use MediaWiki\User\UserIdentityValue;
use MediaWikiIntegrationTestCase;
use TitleValue;
/**
* Trivial comment formatting with a mocked parser. Can't be a unit test because
* of the wfMessage() calls.
*
* @covers \MediaWiki\CommentFormatter\CommentFormatter
*/
class CommentFormatterTest extends MediaWikiIntegrationTestCase {
private function getParser() {
return new class extends CommentParser {
public function __construct() {
}
public function preprocess(
string $comment, LinkTarget $selfLinkTarget = null, $samePage = false,
$wikiId = null, $enableSectionLinks = true
) {
if ( $comment === '' || $comment === '*' ) {
return $comment; // Hack to make it work more like the real parser
}
return CommentFormatterTestUtils::dumpArray( [
'comment' => $comment,
'selfLinkTarget' => $selfLinkTarget,
'samePage' => $samePage,
'wikiId' => $wikiId,
'enableSectionLinks' => $enableSectionLinks
] );
}
public function preprocessUnsafe(
$comment, LinkTarget $selfLinkTarget = null, $samePage = false, $wikiId = null,
$enableSectionLinks = true
) {
return CommentFormatterTestUtils::dumpArray( [
'comment' => $comment,
'selfLinkTarget' => $selfLinkTarget,
'samePage' => $samePage,
'wikiId' => $wikiId,
'enableSectionLinks' => $enableSectionLinks,
'unsafe' => true
] );
}
public function finalize( $comments ) {
return $comments;
}
};
}
private function getParserFactory() {
$parser = $this->getParser();
return new class( $parser ) extends CommentParserFactory {
private $parser;
public function __construct( $parser ) {
$this->parser = $parser;
}
public function create() {
return $this->parser;
}
};
}
private function newCommentFormatter() {
return new CommentFormatter( $this->getParserFactory() );
}
public function testCreateBatch() {
$formatter = $this->newCommentFormatter();
$result = $formatter->createBatch()
->strings( [ 'key' => 'c' ] )
->useBlock()
->useParentheses()
->samePage()
->execute();
$this->assertSame(
[
'key' =>
// parentheses have to come after something so I guess it
// makes sense that there is a space here
' ' .
'<span class="comment">(comment=c, samePage, !wikiId, enableSectionLinks)</span>'
],
$result
);
}
public function testFormatItems() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatItems( [
'key' => new CommentItem( 'c' )
] );
$this->assertSame(
[ 'key' => 'comment=c, !samePage, !wikiId, enableSectionLinks' ],
$result
);
}
public function testFormat() {
$formatter = $this->newCommentFormatter();
$result = $formatter->format(
'c',
new TitleValue( 0, 'Page' ),
true,
'enwiki'
);
$this->assertSame(
'comment=c, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks',
$result
);
}
public function testFormatBlock() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatBlock(
'c',
new TitleValue( 0, 'Page' ),
true,
'enwiki',
true
);
$this->assertSame(
' <span class="comment">' .
'(comment=c, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks)' .
'</span>',
$result
);
}
public function testFormatLinksUnsafe() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatLinksUnsafe(
'c',
new TitleValue( 0, 'Page' ),
true,
'enwiki'
);
$this->assertSame(
'comment=c, selfLinkTarget=0:Page, samePage, wikiId=enwiki, !enableSectionLinks, unsafe',
$result
);
}
public function testFormatLinks() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatLinks(
'c',
new TitleValue( 0, 'Page' ),
true,
'enwiki'
);
$this->assertSame(
'comment=c, selfLinkTarget=0:Page, samePage, wikiId=enwiki, !enableSectionLinks',
$result
);
}
public function testFormatStrings() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatStrings(
[
'a' => 'A',
'b' => 'B'
],
new TitleValue( 0, 'Page' ),
true,
'enwiki'
);
$this->assertSame(
[
'a' => 'comment=A, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks',
'b' => 'comment=B, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks'
],
$result
);
}
public function testFormatStringsAsBlock() {
$formatter = $this->newCommentFormatter();
$result = $formatter->formatStringsAsBlock(
[
'a' => 'A',
'b' => 'B'
],
new TitleValue( 0, 'Page' ),
true,
'enwiki',
true
);
$this->assertSame(
[
'a' => ' <span class="comment">(' .
'comment=A, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks' .
')</span>',
'b' => ' <span class="comment">(' .
'comment=B, selfLinkTarget=0:Page, samePage, wikiId=enwiki, enableSectionLinks' .
')</span>'
],
$result
);
}
public function provideFormatRevision() {
$normal = ' <span class="comment">(' .
'comment=hello, selfLinkTarget=Page, !samePage, enableSectionLinks' .
')</span>';
$deleted = ' <span class="history-deleted comment"> ' .
'<span class="comment">(edit summary removed)</span></span>';
$deletedAllowed = ' <span class="history-deleted comment"> ' .
'<span class="comment">(' .
'comment=hello, selfLinkTarget=Page, !samePage, enableSectionLinks' .
')</span></span>';
return [
'not deleted' => [
'hello', false, false, false, true,
$normal,
],
'deleted, for public, not allowed' => [
'hello', true, true, false, true,
$deleted
],
'deleted, for public, allowed' => [
'hello', true, true, true, true,
$deleted
],
'deleted, for private, not allowed' => [
'hello', false, true, false, true,
$deleted
],
'deleted, for private, allowed' => [
'hello', false, true, true, true,
$deletedAllowed,
],
'empty' => [
'', false, false, false, true,
''
],
'asterisk' => [
'*', false, false, false, true,
''
]
];
}
/**
* @param string $text
* @param bool $isDeleted
* @param bool $isAllowed
* @return array{RevisionRecord,Authority}
* @throws \MWException
*/
private function makeRevisionAndAuthority( $text, $isDeleted, $isAllowed ) {
$page = new PageIdentityValue( 1, 0, 'Page', false );
$rev = new MutableRevisionRecord( $page );
$comment = new \CommentStoreComment( 1, $text );
$rev->setId( 100 );
$rev->setComment( $comment );
$rev->setVisibility( $isDeleted ? RevisionRecord::DELETED_COMMENT : 0 );
$user = new UserIdentityValue( 1, 'Sysop' );
$rights = $isAllowed ? [ 'deletedhistory' ] : [];
$authority = new SimpleAuthority( $user, $rights );
return [ $rev, $authority ];
}
/** @dataProvider provideFormatRevision */
public function testFormatRevision( $comment, $isPublic, $isDeleted, $isAllowed, $useParentheses,
$expected
) {
list( $rev, $authority ) = $this->makeRevisionAndAuthority(
$comment, $isDeleted, $isAllowed );
$formatter = $this->newCommentFormatter();
$result = $formatter->formatRevision(
$rev,
$authority,
false,
$isPublic
);
$this->assertSame(
$expected,
$result
);
}
/** @dataProvider provideFormatRevision */
public function testFormatRevisions( $comment, $isPublic, $isDeleted, $isAllowed, $useParentheses,
$expected
) {
list( $rev, $authority ) = $this->makeRevisionAndAuthority(
$comment, $isDeleted, $isAllowed );
$formatter = $this->newCommentFormatter();
$result = $formatter->formatRevisions(
[ 'key' => $rev ],
$authority,
false,
$isPublic
);
$this->assertSame(
[ 'key' => $expected ],
$result
);
}
public function testFormatRevisionsById() {
list( $rev, $authority ) = $this->makeRevisionAndAuthority(
'hello', false, false );
$formatter = $this->newCommentFormatter();
$result = $formatter->formatRevisions(
[ 'key' => $rev ],
$authority,
false,
false,
true,
true
);
$this->assertSame(
[ 100 => ' <span class="comment">(' .
'comment=hello, selfLinkTarget=Page, !samePage, enableSectionLinks' .
')</span>'
],
$result
);
}
/** @dataProvider provideFormatRevision */
public function testCreateRevisionBatch( $comment, $isPublic, $isDeleted, $isAllowed, $useParentheses,
$expected
) {
list( $rev, $authority ) = $this->makeRevisionAndAuthority(
$comment, $isDeleted, $isAllowed );
$formatter = $this->newCommentFormatter();
$result = $formatter->createRevisionBatch()
->authority( $authority )
->hideIfDeleted( $isPublic )
->useParentheses()
->revisions( [ 'key' => $rev ] )
->execute();
$this->assertSame(
[ 'key' => $expected ],
$result
);
}
public function testCreateRevisionBatchById() {
list( $rev, $authority ) = $this->makeRevisionAndAuthority(
'hello', false, false );
$formatter = $this->newCommentFormatter();
$result = $formatter->createRevisionBatch()
->authority( $authority )
->useParentheses()
->indexById()
->revisions( [ 'key' => $rev ] )
->execute();
$this->assertSame(
[ 100 => ' <span class="comment">(' .
'comment=hello, selfLinkTarget=Page, !samePage, enableSectionLinks' .
')</span>'
],
$result
);
}
}

View file

@ -0,0 +1,396 @@
<?php
namespace MediaWiki\Tests\Integration\CommentFormatter;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\CommentFormatter\CommentParser;
use SiteConfiguration;
use Title;
/**
* @covers \MediaWiki\CommentFormatter\CommentParser
*/
class CommentParserTest extends \MediaWikiIntegrationTestCase {
private function getParser() {
$services = $this->getServiceContainer();
return new CommentParser(
$services->getLinkRenderer(),
$services->getLinkBatchFactory(),
$services->getLinkCache(),
$services->getRepoGroup(),
$services->getContentLanguage(),
$services->getContentLanguage(),
$services->getTitleParser(),
$services->getNamespaceInfo(),
$services->getHookContainer()
);
}
/**
* Copied from LinkerTest so that LinkerTest can be deleted once deprecation
* and removal of Linker::formatComment() is complete.
*
* @return array[]
*/
public function provideFormatComment() {
$wikiId = 'enwiki'; // $wgConf has a fake entry for this
// phpcs:disable Generic.Files.LineLength
return [
// Linker::formatComment
[
'a&lt;script&gt;b',
'a<script>b',
],
[
'a—b',
'a&mdash;b',
],
[
"&#039;&#039;&#039;not bolded&#039;&#039;&#039;",
"'''not bolded'''",
],
[
"try &lt;script&gt;evil&lt;/scipt&gt; things",
"try <script>evil</scipt> things",
],
// Linker::formatAutocomments
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>',
"/* autocomment */",
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#linkie.3F" title="Special:BlankPage">→‎&#91;[linkie?]]</a></span></span>',
"/* [[linkie?]] */",
],
[
'<span dir="auto"><span class="autocomment">: </span> // Edit via via</span>',
// Regression test for T222857
"/* */ // Edit via via",
],
[
'<span dir="auto"><span class="autocomment">: </span> foobar</span>',
// Regression test for T222857
"/**/ foobar",
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> post</span>',
"/* autocomment */ post",
],
[
'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>',
"pre /* autocomment */",
],
[
'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> post</span>',
"pre /* autocomment */ post",
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a>: </span> multiple? <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment2" title="Special:BlankPage">→autocomment2</a></span></span></span>',
"/* autocomment */ multiple? /* autocomment2 */",
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.2F.2A" title="Special:BlankPage">→autocomment containing /*</a>: </span> T70361</span>',
"/* autocomment containing /* */ T70361"
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.22quotes.22" title="Special:BlankPage">→autocomment containing &quot;quotes&quot;</a></span></span>',
"/* autocomment containing \"quotes\" */"
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.3Cscript.3Etags.3C.2Fscript.3E" title="Special:BlankPage">→autocomment containing &lt;script&gt;tags&lt;/script&gt;</a></span></span>',
"/* autocomment containing <script>tags</script> */"
],
[
'<span dir="auto"><span class="autocomment"><a href="#autocomment">→autocomment</a></span></span>',
"/* autocomment */",
false, true
],
[
'<span dir="auto"><span class="autocomment">autocomment</span></span>',
"/* autocomment */",
null
],
[
'',
"/* */",
false, true
],
[
'',
"/* */",
null
],
[
'<span dir="auto"><span class="autocomment">[[</span></span>',
"/* [[ */",
false, true
],
[
'<span dir="auto"><span class="autocomment">[[</span></span>',
"/* [[ */",
null
],
[
"foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#.23\">→‎&#91;[#_\t_]]</a></span></span>",
"foo /* [[#_\t_]] */",
false, true
],
[
"foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#_.09\">#_\t_</a></span></span>",
"foo /* [[#_\t_]] */",
null
],
[
'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→autocomment</a></span></span>',
"/* autocomment */",
false, false
],
[
'<span dir="auto"><span class="autocomment"><a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage#autocomment">→autocomment</a></span></span>',
"/* autocomment */",
false, false, $wikiId
],
// Linker::formatLinksInComment
[
'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">link</a> def',
"abc [[link]] def",
],
[
'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">text</a> def',
"abc [[link|text]] def",
],
[
'abc <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a> def',
"abc [[Special:BlankPage|]] def",
],
[
'abc <a href="/wiki/index.php?title=%C4%84%C5%9B%C5%BC&amp;action=edit&amp;redlink=1" class="new" title="Ąśż (page does not exist)">ąśż</a> def',
"abc [[%C4%85%C5%9B%C5%BC]] def",
],
[
'abc <a href="/wiki/Special:BlankPage#section" title="Special:BlankPage">#section</a> def',
"abc [[#section]] def",
],
[
'abc <a href="/wiki/index.php?title=/subpage&amp;action=edit&amp;redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> def',
"abc [[/subpage]] def",
],
[
'abc <a href="/wiki/index.php?title=%22evil!%22&amp;action=edit&amp;redlink=1" class="new" title="&quot;evil!&quot; (page does not exist)">&quot;evil!&quot;</a> def',
"abc [[\"evil!\"]] def",
],
[
'abc [[&lt;script&gt;very evil&lt;/script&gt;]] def',
"abc [[<script>very evil</script>]] def",
],
[
'abc [[|]] def',
"abc [[|]] def",
],
[
'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">link</a> def',
"abc [[link]] def",
false, false
],
[
'abc <a class="external" rel="nofollow" href="//en.example.org/w/Link">link</a> def',
"abc [[link]] def",
false, false, $wikiId
],
[
'<a href="/wiki/index.php?title=Special:Upload&amp;wpDestFile=LinkerTest.jpg" class="new" title="LinkerTest.jpg">Media:LinkerTest.jpg</a>',
'[[Media:LinkerTest.jpg]]'
],
[
'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
'[[:Special:BlankPage]]'
],
[
'<a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">linktrail</a>...',
'[[link]]trail...'
]
];
// phpcs:enable
}
/**
* Adapted from LinkerTest
*
* @dataProvider provideFormatComment
*/
public function testFormatComment(
$expected, $comment, $title = false, $local = false, $wikiId = null
) {
$conf = new SiteConfiguration();
$conf->settings = [
'wgServer' => [
'enwiki' => '//en.example.org',
'dewiki' => '//de.example.org',
],
'wgArticlePath' => [
'enwiki' => '/w/$1',
'dewiki' => '/w/$1',
],
];
$conf->suffixes = [ 'wiki' ];
$this->setMwGlobals( [
'wgScript' => '/wiki/index.php',
'wgArticlePath' => '/wiki/$1',
'wgCapitalLinks' => true,
'wgConf' => $conf,
// TODO: update tests when the default changes
'wgFragmentMode' => [ 'legacy' ],
] );
if ( $title === false ) {
// We need a page title that exists
$title = Title::newFromText( 'Special:BlankPage' );
}
$parser = $this->getParser();
$result = $parser->finalize(
$parser->preprocess(
$comment,
$title,
$local,
$wikiId
)
);
$this->assertEquals( $expected, $result );
}
/**
* Adapted from LinkerTest
*/
public static function provideFormatLinksInComment() {
// phpcs:disable Generic.Files.LineLength
return [
[
'foo bar <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
'foo bar [[Special:BlankPage]]',
null,
],
[
'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
'[[ :Special:BlankPage]]',
null,
],
[
'[[Foo<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
'[[Foo[[Special:BlankPage]]',
null,
],
[
'<a class="external" rel="nofollow" href="//en.example.org/w/Foo%27bar">Foo&#039;bar</a>',
"[[Foo'bar]]",
'enwiki',
],
[
'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage">Special:BlankPage</a>',
'foo bar [[Special:BlankPage]]',
'enwiki',
],
[
'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/File:Example">Image:Example</a>',
'foo bar [[Image:Example]]',
'enwiki',
],
];
// phpcs:enable
}
/**
* Adapted from LinkerTest. Note that we test the new HTML escaping variant.
*
* @dataProvider provideFormatLinksInComment
*/
public function testFormatLinksInComment( $expected, $input, $wiki ) {
$conf = new SiteConfiguration();
$conf->settings = [
'wgServer' => [
'enwiki' => '//en.example.org'
],
'wgArticlePath' => [
'enwiki' => '/w/$1',
],
];
$conf->suffixes = [ 'wiki' ];
$this->setMwGlobals( [
'wgScript' => '/wiki/index.php',
'wgArticlePath' => '/wiki/$1',
'wgCapitalLinks' => true,
'wgConf' => $conf,
] );
$parser = $this->getParser();
$title = Title::newFromText( 'Special:BlankPage' );
$result = $parser->finalize(
$parser->preprocess(
$input, $title, false, $wiki, false
)
);
$this->assertEquals( $expected, $result );
}
public function testLinkCacheInteraction() {
$this->tablesUsed[] = 'page';
$services = $this->getServiceContainer();
$present = Title::newFromText( 'Present' );
$absent = Title::newFromText( 'Absent' );
$this->editPage(
$present,
'content'
);
$parser = $this->getParser();
$linkCache = $services->getLinkCache();
$result = $parser->finalize( [
$parser->preprocess( "[[$present]]" ),
$parser->preprocess( "[[$absent]]" )
] );
$expected = [
'<a href="/index.php/Present" title="Present">Present</a>',
// phpcs:ignore Generic.Files.LineLength
'<a href="/index.php?title=Absent&amp;action=edit&amp;redlink=1" class="new" title="Absent (page does not exist)">Absent</a>'
];
$this->assertSame( $expected, $result );
$this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $present ) );
$this->assertTrue( $linkCache->isBadLink( $absent ) );
// Run the comment batch again and confirm that LinkBatch does not need
// to execute a query. This is a CommentParser responsibility since
// LinkBatch does not provide a transparent read-through cache.
// TODO: Generic $this->assertQueryCount() would do the job.
$lbf = $services->getDBLoadBalancerFactory();
$fakeLB = $lbf->newMainLB();
$fakeLB->disable( __METHOD__ );
$linkBatchFactory = new LinkBatchFactory(
$services->getLinkCache(),
$services->getTitleFormatter(),
$services->getContentLanguage(),
$services->getGenderCache(),
$fakeLB
);
$parser = new CommentParser(
$services->getLinkRenderer(),
$linkBatchFactory,
$linkCache,
$services->getRepoGroup(),
$services->getContentLanguage(),
$services->getContentLanguage(),
$services->getTitleParser(),
$services->getNamespaceInfo(),
$services->getHookContainer()
);
$result = $parser->finalize( [
$parser->preprocess( "[[$present]]" ),
$parser->preprocess( "[[$absent]]" )
] );
$this->assertSame( $expected, $result );
}
}

View file

@ -0,0 +1,141 @@
<?php
namespace MediaWiki\Tests\Integration\CommentFormatter;
use MediaWiki\CommentFormatter\CommentParser;
use MediaWiki\CommentFormatter\CommentParserFactory;
use MediaWiki\CommentFormatter\RowCommentFormatter;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Tests\Unit\CommentFormatter\CommentFormatterTestUtils;
/**
* @covers \MediaWiki\CommentFormatter\RowCommentFormatter
* @covers \MediaWiki\CommentFormatter\RowCommentIterator
*/
class RowCommentFormatterTest extends \MediaWikiIntegrationTestCase {
private function getParser() {
return new class extends CommentParser {
public function __construct() {
}
public function preprocess(
string $comment, LinkTarget $selfLinkTarget = null, $samePage = false,
$wikiId = null, $enableSectionLinks = true
) {
if ( $comment === '' || $comment === '*' ) {
return $comment; // Hack to make it work more like the real parser
}
return CommentFormatterTestUtils::dumpArray( [
'comment' => $comment,
'selfLinkTarget' => $selfLinkTarget,
'samePage' => $samePage,
'wikiId' => $wikiId,
'enableSectionLinks' => $enableSectionLinks
] );
}
};
}
private function getParserFactory() {
$parser = $this->getParser();
return new class( $parser ) extends CommentParserFactory {
private $parser;
public function __construct( $parser ) {
$this->parser = $parser;
}
public function create() {
return $this->parser;
}
};
}
private function newCommentFormatter() {
return new RowCommentFormatter(
$this->getParserFactory(),
$this->getServiceContainer()->getCommentStore()
);
}
public function testFormatRows() {
$rows = [
(object)[
'comment_text' => 'hello',
'comment_data' => null,
'namespace' => '0',
'title' => 'Page',
'id' => 1
]
];
$commentFormatter = $this->newCommentFormatter();
$result = $commentFormatter->formatRows(
$rows,
'comment',
'namespace',
'title',
'id'
);
$this->assertSame(
[
1 => 'comment=hello, selfLinkTarget=0:Page, !samePage, !wikiId, enableSectionLinks'
],
$result
);
}
public function testRowsWithFormatItems() {
$rows = [
(object)[
'comment_text' => 'hello',
'comment_data' => null,
'namespace' => '0',
'title' => 'Page',
'id' => 1
]
];
$commentFormatter = $this->newCommentFormatter();
$result = $commentFormatter->formatItems(
$commentFormatter->rows( $rows )
->commentKey( 'comment' )
->namespaceField( 'namespace' )
->titleField( 'title' )
->indexField( 'id' )
);
$this->assertSame(
[
1 => 'comment=hello, selfLinkTarget=0:Page, !samePage, !wikiId, enableSectionLinks'
],
$result
);
}
public function testRowsWithCreateBatch() {
$rows = [
(object)[
'comment_text' => 'hello',
'comment_data' => null,
'namespace' => '0',
'title' => 'Page',
'id' => 1
]
];
$commentFormatter = $this->newCommentFormatter();
$result = $commentFormatter->createBatch()
->comments(
$commentFormatter->rows( $rows )
->commentKey( 'comment' )
->namespaceField( 'namespace' )
->titleField( 'title' )
->indexField( 'id' )
)
->samePage( true )
->execute();
$this->assertSame(
[
1 => 'comment=hello, selfLinkTarget=0:Page, samePage, !wikiId, enableSectionLinks'
],
$result
);
}
}

View file

@ -0,0 +1,321 @@
<?php
namespace MediaWiki\Tests\Unit\CommentFormatter;
use MediaWiki\CommentFormatter\CommentBatch;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\CommentItem;
use MediaWikiUnitTestCase;
use TitleValue;
/**
* Trivial unit test with the universe mocked.
*
* @covers \MediaWiki\CommentFormatter\CommentBatch
* @covers \MediaWiki\CommentFormatter\CommentItem
* @covers \MediaWiki\CommentFormatter\StringCommentIterator
*/
class CommentBatchTest extends MediaWikiUnitTestCase {
private $calls;
private function getFormatter() {
return new class( $this->calls ) extends CommentFormatter {
private $calls;
public function __construct( &$calls ) {
$this->calls =& $calls;
}
public function formatItemsInternal(
$items, $selfLinkTarget = null, $samePage = null, $wikiId = null,
$enableSectionLinks = null, $useBlock = null, $useParentheses = null
) {
$paramDump = CommentFormatterTestUtils::dumpArray( [
'selfLinkTarget' => $selfLinkTarget,
'samePage' => $samePage,
'wikiId' => $wikiId,
'enableSectionLinks' => $enableSectionLinks,
'useBlock' => $useBlock,
'useParentheses' => $useParentheses,
] );
if ( $paramDump !== '' ) {
$lines = [ $paramDump ];
}
/** @var CommentItem $item */
foreach ( $items as $i => $item ) {
$lines[] = "$i. " . CommentFormatterTestUtils::dumpArray( [
'comment' => $item->comment,
'selfLinkTarget' => $item->selfLinkTarget,
'samePage' => $item->samePage,
'wikiId' => $item->wikiId,
] );
}
$this->calls[] = $lines;
return [];
}
};
}
private function newBatch() {
return new CommentBatch( $this->getFormatter() );
}
public function testComments() {
$this->newBatch()
->comments( [ new CommentItem( 'c' ) ] )
->execute();
$this->assertSame(
[
[ '0. comment=c' ],
],
$this->calls
);
}
public function testStrings() {
$this->newBatch()
->strings( [
1 => 'one',
3 => 'three',
5 => 'five',
] )
->execute();
$this->assertSame(
[
[
'1. comment=one',
'3. comment=three',
'5. comment=five'
],
],
$this->calls
);
}
public function provideUseBlock() {
return [
[
null,
[ '0. comment=c' ]
],
[
false,
[ '!useBlock', '0. comment=c' ]
],
[
true,
[ 'useBlock', '0. comment=c' ]
],
];
}
/** @dataProvider provideUseBlock */
public function testUseBlock( $useBlock, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $useBlock !== null ) {
$batch->useBlock( $useBlock );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideUseParentheses() {
return [
[
null,
[ '0. comment=c' ]
],
[
false,
[ '!useParentheses', '0. comment=c' ]
],
[
true,
[ 'useParentheses', '0. comment=c' ]
],
];
}
/** @dataProvider provideUseParentheses */
public function testUseParentheses( $useParentheses, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $useParentheses !== null ) {
$batch->useParentheses( $useParentheses );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideSelfLinkTarget() {
return [
[
null,
[ '0. comment=c' ]
],
[
[ 0, 'Page' ],
[ 'selfLinkTarget=0:Page', '0. comment=c' ]
],
];
}
/** @dataProvider provideSelfLinkTarget */
public function testSelfLinkTarget( $selfLinkTarget, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $selfLinkTarget !== null ) {
$batch->selfLinkTarget( new TitleValue( $selfLinkTarget[0], $selfLinkTarget[1] ) );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideEnableSectionLinks() {
return [
[
null,
[ '0. comment=c' ]
],
[
false,
[ '!enableSectionLinks', '0. comment=c' ]
],
[
true,
[ 'enableSectionLinks', '0. comment=c' ]
],
];
}
/** @dataProvider provideEnableSectionLinks */
public function testEnableSectionLinks( $enableSectionLinks, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $enableSectionLinks !== null ) {
$batch->enableSectionLinks( $enableSectionLinks );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideDisableSectionLinks() {
return [
[
null,
[ '0. comment=c' ]
],
[
true,
[ '!enableSectionLinks', '0. comment=c' ]
],
];
}
/** @dataProvider provideDisableSectionLinks */
public function testDisableSectionLinks( $disableSectionLinks, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $disableSectionLinks !== null ) {
$batch->disableSectionLinks();
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideSamePage() {
return [
[
null,
[ '0. comment=c' ]
],
[
false,
[ '!samePage', '0. comment=c' ]
],
[
true,
[ 'samePage', '0. comment=c' ]
],
];
}
/** @dataProvider provideSamePage */
public function testSamePage( $samePage, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $samePage !== null ) {
$batch->samePage( $samePage );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function provideWikiId() {
return [
[
null,
[ '0. comment=c' ]
],
[
'enwiki',
[ 'wikiId=enwiki', '0. comment=c' ]
],
];
}
/** @dataProvider provideWikiId */
public function testWikiId( $wikiId, $expected ) {
$batch = $this->newBatch()
->strings( [ 'c' ] );
if ( $wikiId !== null ) {
$batch->wikiId( $wikiId );
}
$batch->execute();
$this->assertSame( $this->calls, [ $expected ] );
}
public function testItemSelfLinkTarget() {
$item = ( new CommentItem( 'c' ) )
->selfLinkTarget( new TitleValue( 0, 'Page' ) );
$this->newBatch()
->comments( [ $item ] )
->execute();
$this->assertSame(
[
[ '0. comment=c, selfLinkTarget=0:Page' ],
],
$this->calls
);
}
public function testItemSamePage() {
$item = ( new CommentItem( 'c' ) )
->samePage();
$this->newBatch()
->comments( [ $item ] )
->execute();
$this->assertSame(
[
[ '0. comment=c, samePage' ],
],
$this->calls
);
}
public function testItemWikiId() {
$item = ( new CommentItem( 'c' ) )
->wikiId( 'enwiki' );
$this->newBatch()
->comments( [ $item ] )
->execute();
$this->assertSame(
[
[ '0. comment=c, wikiId=enwiki' ],
],
$this->calls
);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Tests\Unit\CommentFormatter;
class CommentFormatterTestUtils {
public static function dumpArray( $a ) {
$s = '';
foreach ( $a as $k => $v ) {
if ( $v === null ) {
continue;
}
if ( $s !== '' ) {
$s .= ', ';
}
if ( $v === true ) {
$s .= $k;
} elseif ( $v === false ) {
$s .= "!$k";
} else {
$s .= "$k=$v";
}
}
return $s;
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace MediaWiki\Tests\Unit\CommentFormatter;
use ArrayIterator;
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\CommentFormatter\RevisionCommentBatch;
use MediaWiki\Permissions\Authority;
use MediaWiki\Permissions\SimpleAuthority;
use MediaWiki\User\UserIdentityValue;
use MediaWikiUnitTestCase;
/**
* Trivial unit test with the universe mocked.
*
* @covers \MediaWiki\CommentFormatter\RevisionCommentBatch
*/
class RevisionCommentBatchTest extends MediaWikiUnitTestCase {
private function getFormatter( $callback ) {
return new class( $callback ) extends CommentFormatter {
private $callback;
public function __construct( $callback ) {
$this->callback = $callback;
}
public function formatRevisions(
$revisions, Authority $authority, $samePage = false, $isPublic = false,
$useParentheses = true, $indexById = false
) {
( $this->callback )( get_defined_vars() );
}
};
}
private function newBatch( $callback ) {
return new RevisionCommentBatch(
$this->getFormatter( $callback )
);
}
private function getAuthority() {
return new SimpleAuthority(
new UserIdentityValue( 1, 'Sysop' ),
[]
);
}
public function testAuthority() {
$authority = $this->getAuthority();
$batch = $this->newBatch(
function ( $params ) use ( $authority ) {
$this->assertSame( $authority, $params['authority'] );
}
);
$batch->authority( $authority )->execute();
}
public function testNoAuthority() {
$this->expectException( \TypeError::class );
$batch = $this->newBatch(
static function ( $params ) {
}
);
$batch
->revisions( [] )
->execute();
}
public function testRevisions() {
$revisions = new ArrayIterator( [] );
$batch = $this->newBatch(
function ( $params ) use ( $revisions ) {
$this->assertSame( $revisions, $params['revisions'] );
// Check default booleans while we have them
$this->assertFalse( $params['samePage'] );
$this->assertFalse( $params['isPublic'] );
$this->assertFalse( $params['useParentheses'] );
$this->assertFalse( $params['indexById'] );
}
);
$batch
->authority( $this->getAuthority() )
->revisions( $revisions )
->execute();
}
public function testSamePage() {
$batch = $this->newBatch(
function ( $params ) {
$this->assertTrue( $params['samePage'] );
}
);
$batch
->authority( $this->getAuthority() )
->samePage()
->execute();
}
public function testUseParentheses() {
$batch = $this->newBatch(
function ( $params ) {
$this->assertTrue( $params['useParentheses'] );
}
);
$batch
->authority( $this->getAuthority() )
->useParentheses()
->execute();
}
public function hideIfPrivate() {
$batch = $this->newBatch(
function ( $params ) {
$this->assertTrue( $params['isPublic'] );
}
);
$batch
->authority( $this->getAuthority() )
->hideIfDeleted()
->execute();
}
public function indexById() {
$batch = $this->newBatch(
function ( $params ) {
$this->assertTrue( $params['indexById'] );
}
);
$batch
->authority( $this->getAuthority() )
->indexById()
->execute();
}
}