mediawiki-extensions-Discus.../includes/CommentItem.php

253 lines
7.4 KiB
PHP
Raw Normal View History

<?php
namespace MediaWiki\Extension\DiscussionTools;
use DateTimeImmutable;
use MediaWiki\MediaWikiServices;
use MWException;
use Sanitizer;
use Title;
Don't refer directly to PHP `dom` extension classes; avoid nonstandard behavior These changes ensure that DiscussionTools is independent of DOM library choice, and will not break if/when Parsoid switches to an alternate (more standards-compliant) DOM library. We run `phan` against the Dodo standards-compliant DOM library, so this ends up flagging uses of non-standard PHP extensions to the DOM. These will be suppressed for now with a "Nonstandard DOM" comment that can be grepped for, since they will eventually will need to be rewritten or worked around. Most frequent issues: * Node::nodeValue and Node::textContent and Element::getAttribute() can return null in a spec-compliant implementation. Add `?? ''` to make spec-compliant results consistent w/ what PHP returns. * DOMXPath doesn't accept anything except DOMDocument. These uses should be replaced with DOMCompat::querySelectorAll() or similar (which end up using DOMXPath under the covers for DOMDocument any way, but are implemented more efficiently in a spec-compliant implementation). * A couple of times we have code like: `while ($node->firstChild!==null) { $node = $node->firstChild; }` and phan's analysis isn't strong enough to determine that $node is still non-null after the while. This same issue should appear with DOMDocument but phan doesn't complain for some reason. One apparently legit issue: * Node::insertBefore() is once called in a funny way which leans on the fact that the second option is optional in PHP. This seems to be a workaround for an ancient PHP bug, and can probably be safely removed. Bug: T287611 Bug: T217867 Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-29 02:16:15 +00:00
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMUtils;
class CommentItem extends ThreadItem {
private $signatureRanges;
private $timestamp;
private $author;
/**
* @param int $level
* @param ImmutableRange $range
* @param ImmutableRange[] $signatureRanges Objects describing the extent of signatures (plus
* timestamps) for this comment. There is always at least one signature, but there may be
* multiple. The author and timestamp of the comment is determined from the first signature.
* The last node in every signature range is a node containing the timestamp.
* @param DateTimeImmutable $timestamp
* @param string $author Comment author's username
*/
public function __construct(
int $level, ImmutableRange $range,
array $signatureRanges, DateTimeImmutable $timestamp, string $author
) {
parent::__construct( 'comment', $level, $range );
$this->signatureRanges = $signatureRanges;
$this->timestamp = $timestamp;
$this->author = $author;
}
/**
* @inheritDoc
*/
public function jsonSerialize( bool $deep = false, ?callable $callback = null ): array {
return array_merge( parent::jsonSerialize( $deep, $callback ), [
'timestamp' => $this->getTimestampString(),
'author' => $this->author,
] );
}
/**
* @return array JSON-serializable array
*/
public function jsonSerializeForDiff(): array {
$data = $this->jsonSerialize();
$heading = $this->getHeading();
$data['headingId'] = $heading->getId();
$subscribableHeading = $this->getSubscribableHeading();
$data['subscribableHeadingId'] = $subscribableHeading ? $subscribableHeading->getId() : null;
return $data;
}
/**
* Get the HTML of this comment's body
*
* @param bool $stripTrailingSeparator Strip a trailing separator between the body and
* the signature which consists of whitespace and hyphens e.g. ' --'
Don't refer directly to PHP `dom` extension classes; avoid nonstandard behavior These changes ensure that DiscussionTools is independent of DOM library choice, and will not break if/when Parsoid switches to an alternate (more standards-compliant) DOM library. We run `phan` against the Dodo standards-compliant DOM library, so this ends up flagging uses of non-standard PHP extensions to the DOM. These will be suppressed for now with a "Nonstandard DOM" comment that can be grepped for, since they will eventually will need to be rewritten or worked around. Most frequent issues: * Node::nodeValue and Node::textContent and Element::getAttribute() can return null in a spec-compliant implementation. Add `?? ''` to make spec-compliant results consistent w/ what PHP returns. * DOMXPath doesn't accept anything except DOMDocument. These uses should be replaced with DOMCompat::querySelectorAll() or similar (which end up using DOMXPath under the covers for DOMDocument any way, but are implemented more efficiently in a spec-compliant implementation). * A couple of times we have code like: `while ($node->firstChild!==null) { $node = $node->firstChild; }` and phan's analysis isn't strong enough to determine that $node is still non-null after the while. This same issue should appear with DOMDocument but phan doesn't complain for some reason. One apparently legit issue: * Node::insertBefore() is once called in a funny way which leans on the fact that the second option is optional in PHP. This seems to be a workaround for an ancient PHP bug, and can probably be safely removed. Bug: T287611 Bug: T217867 Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-29 02:16:15 +00:00
* @return DocumentFragment Cloned fragment of the body content
*/
Don't refer directly to PHP `dom` extension classes; avoid nonstandard behavior These changes ensure that DiscussionTools is independent of DOM library choice, and will not break if/when Parsoid switches to an alternate (more standards-compliant) DOM library. We run `phan` against the Dodo standards-compliant DOM library, so this ends up flagging uses of non-standard PHP extensions to the DOM. These will be suppressed for now with a "Nonstandard DOM" comment that can be grepped for, since they will eventually will need to be rewritten or worked around. Most frequent issues: * Node::nodeValue and Node::textContent and Element::getAttribute() can return null in a spec-compliant implementation. Add `?? ''` to make spec-compliant results consistent w/ what PHP returns. * DOMXPath doesn't accept anything except DOMDocument. These uses should be replaced with DOMCompat::querySelectorAll() or similar (which end up using DOMXPath under the covers for DOMDocument any way, but are implemented more efficiently in a spec-compliant implementation). * A couple of times we have code like: `while ($node->firstChild!==null) { $node = $node->firstChild; }` and phan's analysis isn't strong enough to determine that $node is still non-null after the while. This same issue should appear with DOMDocument but phan doesn't complain for some reason. One apparently legit issue: * Node::insertBefore() is once called in a funny way which leans on the fact that the second option is optional in PHP. This seems to be a workaround for an ancient PHP bug, and can probably be safely removed. Bug: T287611 Bug: T217867 Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-29 02:16:15 +00:00
private function getBodyFragment( bool $stripTrailingSeparator = false ): DocumentFragment {
$fragment = $this->getBodyRange()->cloneContents();
CommentModifier::unwrapFragment( $fragment );
if ( $stripTrailingSeparator ) {
// Find a trailing text node
$lastChild = $fragment->lastChild;
while (
$lastChild &&
!( $lastChild instanceof Text )
) {
$lastChild = $lastChild->lastChild;
}
if (
Don't refer directly to PHP `dom` extension classes; avoid nonstandard behavior These changes ensure that DiscussionTools is independent of DOM library choice, and will not break if/when Parsoid switches to an alternate (more standards-compliant) DOM library. We run `phan` against the Dodo standards-compliant DOM library, so this ends up flagging uses of non-standard PHP extensions to the DOM. These will be suppressed for now with a "Nonstandard DOM" comment that can be grepped for, since they will eventually will need to be rewritten or worked around. Most frequent issues: * Node::nodeValue and Node::textContent and Element::getAttribute() can return null in a spec-compliant implementation. Add `?? ''` to make spec-compliant results consistent w/ what PHP returns. * DOMXPath doesn't accept anything except DOMDocument. These uses should be replaced with DOMCompat::querySelectorAll() or similar (which end up using DOMXPath under the covers for DOMDocument any way, but are implemented more efficiently in a spec-compliant implementation). * A couple of times we have code like: `while ($node->firstChild!==null) { $node = $node->firstChild; }` and phan's analysis isn't strong enough to determine that $node is still non-null after the while. This same issue should appear with DOMDocument but phan doesn't complain for some reason. One apparently legit issue: * Node::insertBefore() is once called in a funny way which leans on the fact that the second option is optional in PHP. This seems to be a workaround for an ancient PHP bug, and can probably be safely removed. Bug: T287611 Bug: T217867 Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-29 02:16:15 +00:00
$lastChild instanceof Text &&
preg_match( '/[\s\-~\x{2010}-\x{2015}\x{2043}\x{2060}]+$/u', $lastChild->nodeValue ?? '', $matches )
) {
$lastChild->nodeValue =
Don't refer directly to PHP `dom` extension classes; avoid nonstandard behavior These changes ensure that DiscussionTools is independent of DOM library choice, and will not break if/when Parsoid switches to an alternate (more standards-compliant) DOM library. We run `phan` against the Dodo standards-compliant DOM library, so this ends up flagging uses of non-standard PHP extensions to the DOM. These will be suppressed for now with a "Nonstandard DOM" comment that can be grepped for, since they will eventually will need to be rewritten or worked around. Most frequent issues: * Node::nodeValue and Node::textContent and Element::getAttribute() can return null in a spec-compliant implementation. Add `?? ''` to make spec-compliant results consistent w/ what PHP returns. * DOMXPath doesn't accept anything except DOMDocument. These uses should be replaced with DOMCompat::querySelectorAll() or similar (which end up using DOMXPath under the covers for DOMDocument any way, but are implemented more efficiently in a spec-compliant implementation). * A couple of times we have code like: `while ($node->firstChild!==null) { $node = $node->firstChild; }` and phan's analysis isn't strong enough to determine that $node is still non-null after the while. This same issue should appear with DOMDocument but phan doesn't complain for some reason. One apparently legit issue: * Node::insertBefore() is once called in a funny way which leans on the fact that the second option is optional in PHP. This seems to be a workaround for an ancient PHP bug, and can probably be safely removed. Bug: T287611 Bug: T217867 Change-Id: I3c4f41c3819770f85d68157c9f690d650b7266a3
2021-07-29 02:16:15 +00:00
substr( $lastChild->nodeValue ?? '', 0, -strlen( $matches[0] ) );
}
}
return $fragment;
}
/**
* Get the HTML of this comment's body
*
*
* @param bool $stripTrailingSeparator See getBodyFragment
* @return string HTML
*/
public function getBodyHTML( bool $stripTrailingSeparator = false ): string {
$fragment = $this->getBodyFragment( $stripTrailingSeparator );
return DOMUtils::getFragmentInnerHTML( $fragment );
}
/**
* Get the text of this comment's body
*
* @param bool $stripTrailingSeparator See getBodyFragment
* @return string Text
*/
public function getBodyText( bool $stripTrailingSeparator = false ): string {
$html = $this->getBodyHTML( $stripTrailingSeparator );
return Sanitizer::stripAllTags( $html );
}
/**
* Get a list of all users mentioned
*
* @return Title[] Title objects for mentioned user pages
*/
public function getMentions(): array {
$fragment = $this->getBodyRange()->cloneContents();
// Note: DOMCompat::getElementsByTagName() doesn't take a DocumentFragment argument
$links = DOMCompat::querySelectorAll( $fragment, 'a' );
$users = [];
foreach ( $links as $link ) {
$href = $link->getAttribute( 'href' );
if ( $href ) {
$siteConfig = MediaWikiServices::getInstance()->getMainConfig();
$title = Title::newFromText( CommentUtils::getTitleFromUrl( $href, $siteConfig ) );
if ( $title && $title->getNamespace() === NS_USER ) {
// TODO: Consider returning User objects
$users[] = $title;
}
}
}
return array_unique( $users );
}
/**
* @return ImmutableRange[] Comment signature ranges
*/
public function getSignatureRanges(): array {
return $this->signatureRanges;
}
/**
* @return ImmutableRange Range of the thread item's "body"
*/
public function getBodyRange(): ImmutableRange {
// Exclude last signature from body
$signatureRanges = $this->getSignatureRanges();
$lastSignature = end( $signatureRanges );
return $this->getRange()->setEnd( $lastSignature->startContainer, $lastSignature->startOffset );
}
/**
* @return DateTimeImmutable Comment timestamp
*/
public function getTimestamp(): DateTimeImmutable {
return $this->timestamp;
}
/**
* Get the comment timestamp in the format used in IDs and names.
*
* Depending on the date of the comment, this may use one of two formats:
*
* - For dates prior to 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
* Uses ISO 8601 date. Almost DateTimeInterface::RFC3339_EXTENDED, but ending with 'Z' instead
* of '+00:00', like Date#toISOString in JavaScript.
*
* - For dates on or after 'DiscussionToolsTimestampFormatSwitchTime' (by default 2022-07-12):
* Uses MediaWiki timestamp (TS_MW in MediaWiki PHP code).
*
* @return string Comment timestamp in standard format
*/
public function getTimestampString(): string {
$dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' );
$switchTime = new DateTimeImmutable(
$dtConfig->get( 'DiscussionToolsTimestampFormatSwitchTime' )
);
$timestamp = $this->getTimestamp();
if ( $timestamp < $switchTime ) {
return $timestamp->format( 'Y-m-d\TH:i:s.v\Z' );
} else {
return $timestamp->format( 'YmdHis' );
}
}
/**
* @return string Comment author
*/
public function getAuthor(): string {
return $this->author;
}
/**
* @return HeadingItem Closest ancestor which is a HeadingItem
*/
public function getHeading(): HeadingItem {
$parent = $this;
while ( $parent instanceof CommentItem ) {
$parent = $parent->getParent();
}
if ( !( $parent instanceof HeadingItem ) ) {
throw new MWException( 'heading parent not found' );
}
return $parent;
}
/**
* @return HeadingItem|null Closest heading that can be used for topic subscriptions
*/
public function getSubscribableHeading(): ?HeadingItem {
$heading = $this->getHeading();
while ( $heading instanceof HeadingItem && !$heading->isSubscribable() ) {
$heading = $heading->getParent();
}
return $heading instanceof HeadingItem ? $heading : null;
}
/**
* @param ImmutableRange $signatureRange Comment signature range to add
*/
public function addSignatureRange( ImmutableRange $signatureRange ): void {
$this->signatureRanges[] = $signatureRange;
}
/**
* @param ImmutableRange[] $signatureRanges Comment signature ranges
*/
public function setSignatureRanges( array $signatureRanges ): void {
$this->signatureRanges = $signatureRanges;
}
/**
* @param DateTimeImmutable $timestamp Comment timestamp
*/
public function setTimestamp( DateTimeImmutable $timestamp ): void {
$this->timestamp = $timestamp;
}
/**
* @param string $author Comment author
*/
public function setAuthor( string $author ): void {
$this->author = $author;
}
}