2020-05-22 16:26:05 +00:00
|
|
|
<?php
|
|
|
|
|
2022-03-18 03:28:06 +00:00
|
|
|
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
2020-05-22 16:26:05 +00:00
|
|
|
|
2020-09-16 12:06:14 +00:00
|
|
|
use JsonSerializable;
|
2022-03-18 03:28:06 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\CommentModifier;
|
|
|
|
use MediaWiki\Extension\DiscussionTools\ImmutableRange;
|
2023-12-11 15:38:02 +00:00
|
|
|
use MediaWiki\Parser\Sanitizer;
|
2021-11-18 22:30:26 +00:00
|
|
|
use Wikimedia\Parsoid\DOM\Element;
|
2024-02-16 16:53:51 +00:00
|
|
|
use Wikimedia\Parsoid\Utils\DOMCompat;
|
2022-02-08 22:11:24 +00:00
|
|
|
use Wikimedia\Parsoid\Utils\DOMUtils;
|
2020-07-29 23:57:51 +00:00
|
|
|
|
2020-06-25 12:23:17 +00:00
|
|
|
/**
|
|
|
|
* A thread item, either a heading or a comment
|
|
|
|
*/
|
2022-03-18 03:28:06 +00:00
|
|
|
abstract class ContentThreadItem implements JsonSerializable, ThreadItem {
|
|
|
|
use ThreadItemTrait;
|
|
|
|
|
2023-09-16 00:31:47 +00:00
|
|
|
protected string $type;
|
|
|
|
protected ImmutableRange $range;
|
|
|
|
protected Element $rootNode;
|
|
|
|
protected int $level;
|
|
|
|
protected ?ContentThreadItem $parent = null;
|
|
|
|
/** @var string[] */
|
|
|
|
protected array $warnings = [];
|
|
|
|
|
|
|
|
protected string $name;
|
|
|
|
protected string $id;
|
|
|
|
/** @var ContentThreadItem[] */
|
|
|
|
protected array $replies = [];
|
2023-12-07 22:57:21 +00:00
|
|
|
/** @var string|bool */
|
|
|
|
private $transcludedFrom;
|
2023-09-16 00:31:47 +00:00
|
|
|
|
|
|
|
/** @var ?array[] */
|
|
|
|
protected ?array $authors = null;
|
|
|
|
protected int $commentCount;
|
|
|
|
protected ?ContentCommentItem $oldestReply;
|
|
|
|
protected ?ContentCommentItem $latestReply;
|
2022-07-05 23:21:34 +00:00
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2020-06-25 12:23:17 +00:00
|
|
|
* @param string $type `heading` or `comment`
|
2020-10-01 19:36:11 +00:00
|
|
|
* @param int $level Indentation level
|
2020-06-25 12:23:17 +00:00
|
|
|
* @param ImmutableRange $range Object describing the extent of the comment, including the
|
|
|
|
* signature and timestamp.
|
2023-12-07 22:57:21 +00:00
|
|
|
* @param bool|string $transcludedFrom
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
|
|
|
public function __construct(
|
2023-12-07 22:57:21 +00:00
|
|
|
string $type, int $level, ImmutableRange $range, $transcludedFrom
|
2020-05-22 16:26:05 +00:00
|
|
|
) {
|
|
|
|
$this->type = $type;
|
|
|
|
$this->level = $level;
|
|
|
|
$this->range = $range;
|
2023-12-07 22:57:21 +00:00
|
|
|
$this->transcludedFrom = $transcludedFrom;
|
2020-05-22 16:26:05 +00:00
|
|
|
}
|
|
|
|
|
2021-01-28 17:19:52 +00:00
|
|
|
/**
|
|
|
|
* Get summary metadata for a thread.
|
|
|
|
*/
|
2022-07-05 23:21:34 +00:00
|
|
|
private function calculateThreadSummary(): void {
|
|
|
|
if ( $this->authors !== null ) {
|
|
|
|
return;
|
|
|
|
}
|
2021-01-28 17:19:52 +00:00
|
|
|
$authors = [];
|
|
|
|
$commentCount = 0;
|
2022-07-05 17:36:56 +00:00
|
|
|
$oldestReply = null;
|
|
|
|
$latestReply = null;
|
2022-09-06 13:35:52 +00:00
|
|
|
$threadScan = static function ( ContentThreadItem $comment ) use (
|
2022-07-05 17:36:56 +00:00
|
|
|
&$authors, &$commentCount, &$oldestReply, &$latestReply, &$threadScan
|
2021-01-28 17:19:52 +00:00
|
|
|
) {
|
2022-09-06 13:35:52 +00:00
|
|
|
if ( $comment instanceof ContentCommentItem ) {
|
2021-01-28 17:19:52 +00:00
|
|
|
$author = $comment->getAuthor();
|
2022-09-06 13:16:10 +00:00
|
|
|
if ( !isset( $authors[ $author] ) ) {
|
|
|
|
$authors[ $author ] = [
|
|
|
|
'username' => $author,
|
|
|
|
'displayNames' => [],
|
|
|
|
];
|
2021-01-28 17:19:52 +00:00
|
|
|
}
|
2022-09-06 13:16:10 +00:00
|
|
|
$displayName = $comment->getDisplayName();
|
|
|
|
if ( $displayName && !in_array( $displayName, $authors[ $author ][ 'displayNames' ], true ) ) {
|
|
|
|
$authors[ $author ][ 'displayNames' ][] = $displayName;
|
|
|
|
}
|
|
|
|
|
2022-07-05 17:36:56 +00:00
|
|
|
if (
|
|
|
|
!$oldestReply ||
|
|
|
|
( $comment->getTimestamp() < $oldestReply->getTimestamp() )
|
|
|
|
) {
|
|
|
|
$oldestReply = $comment;
|
|
|
|
}
|
2021-01-28 17:19:52 +00:00
|
|
|
if (
|
|
|
|
!$latestReply ||
|
|
|
|
( $latestReply->getTimestamp() < $comment->getTimestamp() )
|
|
|
|
) {
|
|
|
|
$latestReply = $comment;
|
|
|
|
}
|
|
|
|
$commentCount++;
|
|
|
|
}
|
|
|
|
// Get the set of authors in the same format from each reply
|
|
|
|
$replies = $comment->getReplies();
|
|
|
|
array_walk( $replies, $threadScan );
|
|
|
|
};
|
|
|
|
$replies = $this->getReplies();
|
|
|
|
array_walk( $replies, $threadScan );
|
|
|
|
|
|
|
|
ksort( $authors );
|
2022-07-05 23:21:34 +00:00
|
|
|
|
2022-09-06 13:16:10 +00:00
|
|
|
$this->authors = array_values( $authors );
|
2022-07-05 23:21:34 +00:00
|
|
|
$this->commentCount = $commentCount;
|
|
|
|
$this->oldestReply = $oldestReply;
|
|
|
|
$this->latestReply = $latestReply;
|
2021-01-28 17:19:52 +00:00
|
|
|
}
|
|
|
|
|
2020-07-20 14:13:59 +00:00
|
|
|
/**
|
2022-07-05 23:21:34 +00:00
|
|
|
* Get the list of authors in the tree below this thread item.
|
2020-07-20 14:13:59 +00:00
|
|
|
*
|
|
|
|
* Usually called on a HeadingItem to find all authors in a thread.
|
|
|
|
*
|
2022-09-06 13:16:10 +00:00
|
|
|
* @return array[] Authors, with `username` and `displayNames` (list of display names) properties.
|
2020-07-20 14:13:59 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getAuthorsBelow(): array {
|
2022-07-05 23:21:34 +00:00
|
|
|
$this->calculateThreadSummary();
|
|
|
|
return $this->authors;
|
|
|
|
}
|
2020-07-20 14:13:59 +00:00
|
|
|
|
2022-07-05 23:21:34 +00:00
|
|
|
/**
|
|
|
|
* Get the number of comment items in the tree below this thread item.
|
|
|
|
*/
|
|
|
|
public function getCommentCount(): int {
|
|
|
|
$this->calculateThreadSummary();
|
|
|
|
return $this->commentCount;
|
|
|
|
}
|
2020-07-20 14:13:59 +00:00
|
|
|
|
2022-07-05 23:21:34 +00:00
|
|
|
/**
|
|
|
|
* Get the latest reply in the tree below this thread item, null if there are no replies
|
|
|
|
*/
|
|
|
|
public function getLatestReply(): ?ContentCommentItem {
|
|
|
|
$this->calculateThreadSummary();
|
|
|
|
return $this->latestReply;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the oldest reply in the tree below this thread item, null if there are no replies
|
|
|
|
*/
|
|
|
|
public function getOldestReply(): ?ContentCommentItem {
|
|
|
|
$this->calculateThreadSummary();
|
|
|
|
return $this->oldestReply;
|
2020-07-20 14:13:59 +00:00
|
|
|
}
|
|
|
|
|
2022-02-21 00:22:39 +00:00
|
|
|
/**
|
2022-07-05 23:21:34 +00:00
|
|
|
* Get a flat list of thread items in the comment tree below this thread item.
|
2022-02-21 00:22:39 +00:00
|
|
|
*
|
2022-03-18 03:28:06 +00:00
|
|
|
* @return ContentThreadItem[] Thread items
|
2022-02-21 00:22:39 +00:00
|
|
|
*/
|
|
|
|
public function getThreadItemsBelow(): array {
|
|
|
|
$threadItems = [];
|
2022-09-06 13:35:52 +00:00
|
|
|
$getReplies = static function ( ContentThreadItem $threadItem ) use ( &$threadItems, &$getReplies ) {
|
2022-02-21 00:22:39 +00:00
|
|
|
$threadItems[] = $threadItem;
|
|
|
|
foreach ( $threadItem->getReplies() as $reply ) {
|
|
|
|
$getReplies( $reply );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
foreach ( $this->getReplies() as $reply ) {
|
|
|
|
$getReplies( $reply );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $threadItems;
|
|
|
|
}
|
|
|
|
|
2020-07-20 14:48:41 +00:00
|
|
|
/**
|
2023-12-07 22:57:21 +00:00
|
|
|
* @inheritDoc
|
2020-07-20 14:48:41 +00:00
|
|
|
*/
|
|
|
|
public function getTranscludedFrom() {
|
2023-12-07 22:57:21 +00:00
|
|
|
return $this->transcludedFrom;
|
2020-07-20 14:48:41 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 18:25:34 +00:00
|
|
|
/**
|
|
|
|
* Get the HTML of this thread item
|
|
|
|
*
|
|
|
|
* @return string HTML
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getHTML(): string {
|
2020-07-22 18:25:34 +00:00
|
|
|
$fragment = $this->getRange()->cloneContents();
|
2020-11-20 00:21:30 +00:00
|
|
|
CommentModifier::unwrapFragment( $fragment );
|
2024-02-16 16:53:51 +00:00
|
|
|
// Does not work: T357812
|
|
|
|
// $editsection = DOMCompat::querySelector( $fragment, 'mw\\:editsection' );
|
|
|
|
for ( $n = $fragment->firstChild; $n; $n = $n->nextSibling ) {
|
|
|
|
if ( $n instanceof Element ) {
|
|
|
|
if ( strtolower( $n->tagName ) === 'mw:editsection' ) {
|
|
|
|
$n->parentNode->removeChild( $n );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$editsection = DOMCompat::querySelector( $n, 'mw\\:editsection' );
|
|
|
|
if ( $editsection ) {
|
|
|
|
$editsection->parentNode->removeChild( $editsection );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-08 22:11:24 +00:00
|
|
|
return DOMUtils::getFragmentInnerHTML( $fragment );
|
2020-07-22 18:25:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the text of this thread item
|
|
|
|
*
|
|
|
|
* @return string Text
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getText(): string {
|
2022-02-10 20:34:05 +00:00
|
|
|
$html = $this->getHTML();
|
|
|
|
return Sanitizer::stripAllTags( $html );
|
2020-07-22 18:25:34 +00:00
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
|
|
|
* @return string Thread item type
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getType(): string {
|
2020-05-22 16:26:05 +00:00
|
|
|
return $this->type;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-10-01 19:36:11 +00:00
|
|
|
* @return int Indentation level
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getLevel(): int {
|
2020-05-22 16:26:05 +00:00
|
|
|
return $this->level;
|
|
|
|
}
|
|
|
|
|
2020-10-01 19:36:11 +00:00
|
|
|
/**
|
2022-03-18 03:28:06 +00:00
|
|
|
* @return ContentThreadItem|null Parent thread item
|
2020-10-01 19:36:11 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getParent(): ?ThreadItem {
|
2020-10-01 19:36:11 +00:00
|
|
|
return $this->parent;
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2020-07-22 18:25:34 +00:00
|
|
|
* @return ImmutableRange Range of the entire thread item
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getRange(): ImmutableRange {
|
2020-05-22 16:26:05 +00:00
|
|
|
return $this->range;
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:57:51 +00:00
|
|
|
/**
|
2022-02-21 18:56:15 +00:00
|
|
|
* @return Element Root node (level is relative to this node)
|
2020-07-29 23:57:51 +00:00
|
|
|
*/
|
2022-02-21 18:56:15 +00:00
|
|
|
public function getRootNode(): Element {
|
2020-07-29 23:57:51 +00:00
|
|
|
return $this->rootNode;
|
|
|
|
}
|
|
|
|
|
2021-02-12 19:16:13 +00:00
|
|
|
/**
|
|
|
|
* @return string Thread item name
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getName(): string {
|
2021-02-12 19:16:13 +00:00
|
|
|
return $this->name;
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2020-09-22 23:05:25 +00:00
|
|
|
* @return string Thread ID
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getId(): string {
|
2020-05-22 16:26:05 +00:00
|
|
|
return $this->id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-03-18 03:28:06 +00:00
|
|
|
* @return ContentThreadItem[] Replies to this thread item
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getReplies(): array {
|
2020-05-22 16:26:05 +00:00
|
|
|
return $this->replies;
|
|
|
|
}
|
|
|
|
|
2020-11-02 18:35:38 +00:00
|
|
|
/**
|
|
|
|
* @return string[] Warnings
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function getWarnings(): array {
|
2020-11-02 18:35:38 +00:00
|
|
|
return $this->warnings;
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2020-10-01 19:36:11 +00:00
|
|
|
* @param int $level Indentation level
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function setLevel( int $level ): void {
|
2020-05-22 16:26:05 +00:00
|
|
|
$this->level = $level;
|
|
|
|
}
|
|
|
|
|
2022-03-18 03:28:06 +00:00
|
|
|
public function setParent( ContentThreadItem $parent ): void {
|
2020-10-01 19:36:11 +00:00
|
|
|
$this->parent = $parent;
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
|
|
|
* @param ImmutableRange $range Thread item range
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function setRange( ImmutableRange $range ): void {
|
2020-05-22 16:26:05 +00:00
|
|
|
$this->range = $range;
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:57:51 +00:00
|
|
|
/**
|
2022-02-21 18:56:15 +00:00
|
|
|
* @param Element $rootNode Root node (level is relative to this node)
|
2020-07-29 23:57:51 +00:00
|
|
|
*/
|
2022-02-21 18:56:15 +00:00
|
|
|
public function setRootNode( Element $rootNode ): void {
|
2020-07-29 23:57:51 +00:00
|
|
|
$this->rootNode = $rootNode;
|
|
|
|
}
|
|
|
|
|
2021-02-12 19:16:13 +00:00
|
|
|
/**
|
2023-09-16 00:31:47 +00:00
|
|
|
* @param string $name Thread item name
|
2021-02-12 19:16:13 +00:00
|
|
|
*/
|
2023-09-16 00:31:47 +00:00
|
|
|
public function setName( string $name ): void {
|
2021-02-12 19:16:13 +00:00
|
|
|
$this->name = $name;
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2023-09-16 00:31:47 +00:00
|
|
|
* @param string $id Thread ID
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2023-09-16 00:31:47 +00:00
|
|
|
public function setId( string $id ): void {
|
2020-05-22 16:26:05 +00:00
|
|
|
$this->id = $id;
|
|
|
|
}
|
|
|
|
|
2021-07-22 07:25:13 +00:00
|
|
|
public function addWarning( string $warning ): void {
|
2020-11-02 18:35:38 +00:00
|
|
|
$this->warnings[] = $warning;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-11-11 08:31:59 +00:00
|
|
|
* @param string[] $warnings
|
2020-11-02 18:35:38 +00:00
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
public function addWarnings( array $warnings ): void {
|
2020-11-02 18:35:38 +00:00
|
|
|
$this->warnings = array_merge( $this->warnings, $warnings );
|
|
|
|
}
|
|
|
|
|
2020-05-22 16:26:05 +00:00
|
|
|
/**
|
2022-03-18 03:28:06 +00:00
|
|
|
* @param ContentThreadItem $reply Reply comment
|
2020-05-22 16:26:05 +00:00
|
|
|
*/
|
2022-03-18 03:28:06 +00:00
|
|
|
public function addReply( ContentThreadItem $reply ): void {
|
2020-05-22 16:26:05 +00:00
|
|
|
$this->replies[] = $reply;
|
|
|
|
}
|
|
|
|
}
|