mediawiki-extensions-Discus.../includes/ApiDiscussionToolsPageInfo.php
Bartosz Dziewoński cfa45a5f4c Remove all stuff about legacy IDs
We can no longer change IDs so easily, because they're stored in the
permalink database, so remove this mechanism to make sure it's not
accidentally used in the future.

Change-Id: I392ee1f49c48fc2f23d05e9a37c643438b4f2b9a
2022-08-24 01:01:09 +02:00

227 lines
7.3 KiB
PHP

<?php
namespace MediaWiki\Extension\DiscussionTools;
use ApiBase;
use ApiMain;
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem;
use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem;
use MediaWiki\Extension\VisualEditor\ApiParsoidTrait;
use Title;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Text;
use Wikimedia\Parsoid\Utils\DOMUtils;
class ApiDiscussionToolsPageInfo extends ApiBase {
use ApiDiscussionToolsTrait;
use ApiParsoidTrait;
/**
* @inheritDoc
*/
public function __construct( ApiMain $main, string $name ) {
parent::__construct( $main, $name );
}
/**
* @inheritDoc
*/
public function execute() {
$params = $this->extractRequestParams();
$title = Title::newFromText( $params['page'] );
$prop = array_fill_keys( $params['prop'], true );
if ( !$title ) {
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
}
$revision = $this->getValidRevision( $title, $params['oldid'] ?? null );
$threadItemSet = $this->parseRevision( $revision );
$result = [];
if ( isset( $prop['transcludedfrom'] ) ) {
$result['transcludedfrom'] = static::getTranscludedFrom( $threadItemSet );
}
if ( isset( $prop['threaditemshtml'] ) ) {
$result['threaditemshtml'] = static::getThreadItemsHtml( $threadItemSet );
}
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
/**
* Get transcluded=from data for a ContentThreadItemSet
*
* @param ContentThreadItemSet $threadItemSet
* @return array
*/
private static function getTranscludedFrom( ContentThreadItemSet $threadItemSet ): array {
$threadItems = $threadItemSet->getThreadItems();
$transcludedFrom = [];
foreach ( $threadItems as $threadItem ) {
$from = $threadItem->getTranscludedFrom();
// Key by IDs and names. This assumes that they can never conflict.
$transcludedFrom[ $threadItem->getId() ] = $from;
$name = $threadItem->getName();
if ( isset( $transcludedFrom[ $name ] ) && $transcludedFrom[ $name ] !== $from ) {
// Two or more items with the same name, transcluded from different pages.
// Consider them both to be transcluded from unknown source.
$transcludedFrom[ $name ] = true;
} else {
$transcludedFrom[ $name ] = $from;
}
}
return $transcludedFrom;
}
/**
* Get thread items HTML for a ContentThreadItemSet
*
* @param ContentThreadItemSet $threadItemSet
* @return array
*/
private static function getThreadItemsHtml( ContentThreadItemSet $threadItemSet ): array {
// This function assumes that the start of the ranges associated with
// HeadingItems are going to be at the start of their associated
// heading node (`<h2>^heading</h2>`), i.e. in the position generated
// by getHeadlineNodeAndOffset.
$threads = $threadItemSet->getThreads();
if ( count( $threads ) > 0 && !$threads[0]->isPlaceholderHeading() ) {
$firstHeading = $threads[0];
$firstRange = $firstHeading->getRange();
$rootNode = $firstHeading->getRootNode();
// We need a placeholder if there's content between the beginning
// of rootnode and the start of firstHeading. An ancestor of the
// first heading with a previousSibling is evidence that there's
// probably content. If this is giving false positives we could
// perhaps use linearWalkBackwards and DomUtils::isContentNode.
$closest = CommentUtils::closestElementWithSibling( $firstRange->startContainer, 'previous' );
if ( $closest && !$rootNode->isSameNode( $closest ) ) {
$range = new ImmutableRange( $rootNode, 0, $rootNode, 0 );
$fakeHeading = new ContentHeadingItem( $range, null );
$fakeHeading->setRootNode( $rootNode );
$fakeHeading->setName( 'h-' );
$fakeHeading->setId( 'h-' );
array_unshift( $threads, $fakeHeading );
}
}
$output = array_map( static function ( ContentThreadItem $item ) {
return $item->jsonSerialize( true, static function ( array &$array, ContentThreadItem $item ) {
$array['html'] = $item->getHtml();
} );
}, $threads );
foreach ( $threads as $index => $item ) {
// need to loop over this to fix up empty sections, because we
// need context that's not available inside the array map
if ( $item instanceof ContentHeadingItem && count( $item->getReplies() ) === 0 ) {
// If there are no replies we want to include whatever's
// inside this section as "othercontent". We create a range
// that's between the end of this section's heading and the
// start of next section's heading. The main difficulty here
// is avoiding catching any of the heading's tags within the
// range.
$nextItem = $threads[ $index + 1 ] ?? false;
$startRange = $item->getRange();
if ( $item->isPlaceholderHeading() ) {
// Placeholders don't have any heading to avoid
$startNode = $startRange->startContainer;
$startOffset = $startRange->startOffset;
} else {
$startNode = CommentUtils::closestElementWithSibling( $startRange->endContainer, 'next' );
$startNode = $startNode->nextSibling;
$startOffset = 0;
}
if ( !$startNode ) {
$startNode = $startRange->endContainer;
$startOffset = $startRange->endOffset;
}
if ( $nextItem ) {
$nextStart = $nextItem->getRange()->startContainer;
$endContainer = CommentUtils::closestElementWithSibling( $nextStart, 'previous' );
$endContainer = $endContainer && $endContainer->previousSibling ?
$endContainer->previousSibling : $nextStart;
$endOffset = CommentUtils::childIndexOf( $endContainer );
if ( $endContainer instanceof Text ) {
// This probably means that there's a wrapping node
// e.g. <div>foo\n==heading==\nbar</div>
$endOffset += $endContainer->length;
} elseif ( $endContainer instanceof Element && $endContainer->tagName === 'section' ) {
// if we're in sections, make sure we're selecting the
// end of the previous section
$endOffset = $endContainer->childNodes->length;
} elseif ( $endContainer->parentNode ) {
$endContainer = $endContainer->parentNode;
}
$betweenRange = new ImmutableRange(
$startNode, $startOffset,
$endContainer ?: $nextStart, $endOffset
);
} else {
// This is the last section, so we want to go to the end of the rootnode
$betweenRange = new ImmutableRange(
$startNode, $startOffset,
$item->getRootNode(), $item->getRootNode()->childNodes->length
);
}
$fragment = $betweenRange->cloneContents();
CommentModifier::unwrapFragment( $fragment );
$output[$index]['othercontent'] = trim( DOMUtils::getFragmentInnerHTML( $fragment ) );
}
}
return $output;
}
/**
* @inheritDoc
*/
public function getAllowedParams() {
return [
'page' => [
ParamValidator::PARAM_REQUIRED => true,
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-page',
],
'oldid' => null,
'prop' => [
ParamValidator::PARAM_DEFAULT => 'transcludedfrom',
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_TYPE => [
'transcludedfrom',
'threaditemshtml'
],
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
],
];
}
/**
* @inheritDoc
*/
public function needsToken() {
return false;
}
/**
* @inheritDoc
*/
public function isInternal() {
return true;
}
/**
* @inheritDoc
*/
public function isWriteMode() {
return false;
}
}