mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-23 16:06:53 +00:00
Merge "Store permalink data, implement Special:FindComment/GoToComment"
This commit is contained in:
commit
95fb33fb57
|
@ -11,6 +11,8 @@ $specialPageAliases = [];
|
|||
/** English (English) */
|
||||
$specialPageAliases['en'] = [
|
||||
'TopicSubscriptions' => [ 'TopicSubscriptions' ],
|
||||
'FindComment' => [ 'FindComment' ],
|
||||
'GoToComment' => [ 'GoToComment' ],
|
||||
];
|
||||
|
||||
/** čeština (Czech) */
|
||||
|
|
|
@ -18,8 +18,11 @@
|
|||
],
|
||||
"dbschema": [
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_subscription.json --sql sql/mysql/discussiontools_subscription.sql --type mysql",
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_persistent.json --sql sql/mysql/discussiontools_persistent.sql --type mysql",
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_subscription.json --sql sql/postgres/discussiontools_subscription.sql --type postgres",
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_subscription.json --sql sql/sqlite/discussiontools_subscription.sql --type sqlite"
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_persistent.json --sql sql/postgres/discussiontools_persistent.sql --type postgres",
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_subscription.json --sql sql/sqlite/discussiontools_subscription.sql --type sqlite",
|
||||
"php ../../maintenance/generateSchemaSql.php --json sql/discussiontools_persistent.json --sql sql/sqlite/discussiontools_persistent.sql --type sqlite"
|
||||
],
|
||||
"phan": "phan -d . --long-progress-bar",
|
||||
"phpcs": "phpcs -sp --cache"
|
||||
|
|
|
@ -470,6 +470,19 @@
|
|||
"LinkRenderer",
|
||||
"LinkBatchFactory"
|
||||
]
|
||||
},
|
||||
"FindComment": {
|
||||
"class": "\\MediaWiki\\Extension\\DiscussionTools\\SpecialFindComment",
|
||||
"services": [
|
||||
"DiscussionTools.ThreadItemStore",
|
||||
"DiscussionTools.ThreadItemFormatter"
|
||||
]
|
||||
},
|
||||
"GoToComment": {
|
||||
"class": "\\MediaWiki\\Extension\\DiscussionTools\\SpecialGoToComment",
|
||||
"services": [
|
||||
"DiscussionTools.ThreadItemStore"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Hooks": {
|
||||
|
@ -477,6 +490,7 @@
|
|||
"EchoGetBundleRules": "\\MediaWiki\\Extension\\DiscussionTools\\Hooks\\EchoHooks::onEchoGetBundleRules",
|
||||
"EchoGetEventsForRevision": "\\MediaWiki\\Extension\\DiscussionTools\\Hooks\\EchoHooks::onEchoGetEventsForRevision",
|
||||
"MinervaNeueTalkPageOverlay": "\\MediaWiki\\Extension\\DiscussionTools\\Hooks\\MobileHooks::onMinervaNeueTalkPageOverlay",
|
||||
"RevisionDataUpdates": "dataupdates",
|
||||
"LoadExtensionSchemaUpdates": "installer",
|
||||
"ParserAfterTidy": "parser",
|
||||
"ArticleViewHeader": "page",
|
||||
|
@ -494,6 +508,12 @@
|
|||
"RecentChange_save": "tags"
|
||||
},
|
||||
"HookHandlers": {
|
||||
"dataupdates": {
|
||||
"class": "MediaWiki\\Extension\\DiscussionTools\\Hooks\\DataUpdatesHooks",
|
||||
"services": [
|
||||
"DiscussionTools.ThreadItemStore"
|
||||
]
|
||||
},
|
||||
"installer": {
|
||||
"class": "MediaWiki\\Extension\\DiscussionTools\\Hooks\\InstallerHooks"
|
||||
},
|
||||
|
|
|
@ -35,6 +35,15 @@
|
|||
"discussiontools-error-noswitchtove-table": "table syntax",
|
||||
"discussiontools-error-noswitchtove-template": "template syntax",
|
||||
"discussiontools-error-noswitchtove-title": "Visual mode disabled",
|
||||
"discussiontools-findcomment-gotocomment": "If it only appears in the current revision of one page, you can [[Special:GoToComment/$1|go directly to the comment using this link]]. Otherwise it will redirect to these search results.",
|
||||
"discussiontools-findcomment-label-idorname": "Comment ID or name",
|
||||
"discussiontools-findcomment-label-search": "Search",
|
||||
"discussiontools-findcomment-noresults": "No results.",
|
||||
"discussiontools-findcomment-results-id": "Comment with the given ID has appeared on the following {{PLURAL:$1|page|pages}}:",
|
||||
"discussiontools-findcomment-results-name": "Comment with the given name has appeared on the following {{PLURAL:$1|page|pages}}:",
|
||||
"discussiontools-findcomment-results-notcurrent": "(not in current revision)",
|
||||
"discussiontools-findcomment-results-transcluded": "(transcluded from another page)",
|
||||
"discussiontools-findcomment-title": "Find comment",
|
||||
"discussiontools-limitreport-errorreqid": "DiscussionTools error request ID",
|
||||
"discussiontools-limitreport-timeusage": "DiscussionTools time usage",
|
||||
"discussiontools-limitreport-timeusage-value": "$1 {{PLURAL:$1|second|seconds}}",
|
||||
|
|
|
@ -47,6 +47,15 @@
|
|||
"discussiontools-error-noswitchtove-table": "Type of syntax detected, used as a parameter in {{msg-mw|discussiontools-error-noswitchtove}}.",
|
||||
"discussiontools-error-noswitchtove-template": "Type of syntax detected, used as a parameter in {{msg-mw|discussiontools-error-noswitchtove}}.",
|
||||
"discussiontools-error-noswitchtove-title": "Type of syntax detected, used as a parameter in {{msg-mw|discussiontools-error-noswitchtove}}.",
|
||||
"discussiontools-findcomment-gotocomment": "Message shown after submitting the form on [[Special:FindComment]]. Follows {{msg-mw|discussiontools-findcomment-results-id}} or {{msg-mw|discussiontools-findcomment-results-name}} and a list of results.\n\nParameters:\n* $1 – comment ID, can only be used in link target",
|
||||
"discussiontools-findcomment-label-idorname": "Text field label on [[Special:FindComment]]",
|
||||
"discussiontools-findcomment-label-search": "Button label on [[Special:FindComment]]\n\n'ID' and 'name' are technical terms, see https://www.mediawiki.org/wiki/Extension:DiscussionTools/How_it_works#Assigning_ID_and_name",
|
||||
"discussiontools-findcomment-noresults": "Message shown after submitting the form on [[Special:FindComment]].",
|
||||
"discussiontools-findcomment-results-id": "Message shown after submitting the form on [[Special:FindComment]]. Followed by a bulleted list.\n\nParameters:\n* $1 – number of results\n\n'ID' and 'name' are technical terms, see https://www.mediawiki.org/wiki/Extension:DiscussionTools/How_it_works#Assigning_ID_and_name",
|
||||
"discussiontools-findcomment-results-name": "Message shown after submitting the form on [[Special:FindComment]]. Followed by a bulleted list.\n\nParameters:\n* $1 – number of results\n\n'ID' and 'name' are technical terms, see https://www.mediawiki.org/wiki/Extension:DiscussionTools/How_it_works#Assigning_ID_and_name",
|
||||
"discussiontools-findcomment-results-notcurrent": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.",
|
||||
"discussiontools-findcomment-results-transcluded": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.",
|
||||
"discussiontools-findcomment-title": "Page title for [[Special:FindComment]], also shown on the list on [[Special:SpecialPages]]",
|
||||
"discussiontools-limitreport-errorreqid": "Label for the ID of the web request in which a DiscussionTools error has occurred.",
|
||||
"discussiontools-limitreport-timeusage": "Label for the time usage (duration) of DiscussionTools in the parser limit report. Followed by {{msg-mw|discussiontools-limitreport-timeusage-value}}.\n\nSimilar to:\n* {{msg-mw|limitreport-cputime}}\n* {{msg-mw|limitreport-walltime}}\n* {{msg-mw|scribunto-limitreport-timeusage}}",
|
||||
"discussiontools-limitreport-timeusage-value": "Follows {{msg-mw|discussiontools-limitreport-timeusage}}.\n\nParameters:\n* $1 - the usage in seconds\n{{Identical|Second}}",
|
||||
|
|
52
includes/Hooks/DataUpdatesHooks.php
Normal file
52
includes/Hooks/DataUpdatesHooks.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
/**
|
||||
* DiscussionTools data updates hooks
|
||||
*
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\Hooks;
|
||||
|
||||
use DeferrableUpdate;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItemStore;
|
||||
use MediaWiki\Revision\RenderedRevision;
|
||||
use MediaWiki\Storage\Hook\RevisionDataUpdatesHook;
|
||||
use MWCallableUpdate;
|
||||
use Title;
|
||||
|
||||
class DataUpdatesHooks implements RevisionDataUpdatesHook {
|
||||
|
||||
/** @var ThreadItemStore */
|
||||
private $threadItemStore;
|
||||
|
||||
/**
|
||||
* @param ThreadItemStore $threadItemStore
|
||||
*/
|
||||
public function __construct(
|
||||
ThreadItemStore $threadItemStore
|
||||
) {
|
||||
$this->threadItemStore = $threadItemStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Title $title
|
||||
* @param RenderedRevision $renderedRevision
|
||||
* @param DeferrableUpdate[] &$updates
|
||||
* @return bool|void
|
||||
*/
|
||||
public function onRevisionDataUpdates( $title, $renderedRevision, &$updates ) {
|
||||
// This doesn't trigger on action=purge, only on automatic purge after editing a template or
|
||||
// transcluded page, and API action=purge&forcelinkupdate=1.
|
||||
|
||||
// TODO Deduplicate work between this and the Echo hook (make it use Parsoid too)
|
||||
$rev = $renderedRevision->getRevision();
|
||||
if ( HookUtils::isAvailableForTitle( $title ) ) {
|
||||
$updates[] = new MWCallableUpdate( function () use ( $rev ) {
|
||||
$threadItemSet = HookUtils::parseRevisionParsoidHtml( $rev );
|
||||
$this->threadItemStore->insertThreadItems( $rev, $threadItemSet );
|
||||
}, __METHOD__ );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,13 +11,22 @@ namespace MediaWiki\Extension\DiscussionTools\Hooks;
|
|||
|
||||
use ExtensionRegistry;
|
||||
use IContextSource;
|
||||
use MediaWiki\Extension\DiscussionTools\CommentUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet;
|
||||
use MediaWiki\Extension\VisualEditor\ParsoidHelper;
|
||||
use MediaWiki\Linker\LinkTarget;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
use MWException;
|
||||
use OutputPage;
|
||||
use Psr\Log\NullLogger;
|
||||
use RequestContext;
|
||||
use Title;
|
||||
use TitleValue;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\Parsoid\Utils\DOMCompat;
|
||||
use Wikimedia\Parsoid\Utils\DOMUtils;
|
||||
|
||||
class HookUtils {
|
||||
public const REPLYTOOL = 'replytool';
|
||||
|
@ -67,6 +76,43 @@ class HookUtils {
|
|||
return static::$propCache[ $id ][ $prop ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a revision by using the discussion parser on the HTML provided by Parsoid.
|
||||
*
|
||||
* @param RevisionRecord $revRecord
|
||||
* @return ContentThreadItemSet
|
||||
*/
|
||||
public static function parseRevisionParsoidHtml( RevisionRecord $revRecord ): ContentThreadItemSet {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
$parsoidHelper = new ParsoidHelper(
|
||||
$services->getMainConfig(),
|
||||
new NullLogger(),
|
||||
false
|
||||
);
|
||||
|
||||
// Get HTML for the revision
|
||||
$status = $parsoidHelper->requestRestbasePageHtml( $revRecord );
|
||||
|
||||
if ( !$status->isOK() ) {
|
||||
[ 'message' => $msg, 'params' => $params ] = $status->getErrors()[0];
|
||||
throw new MWException( wfMessage( $msg, ...$params )->text() );
|
||||
}
|
||||
|
||||
$title = TitleValue::newFromPage( $revRecord->getPage() );
|
||||
|
||||
$response = $status->getValue();
|
||||
$html = $response['body'];
|
||||
|
||||
// Run the discussion parser on it
|
||||
$doc = DOMUtils::parseHTML( $html );
|
||||
$container = DOMCompat::getBody( $doc );
|
||||
|
||||
CommentUtils::unwrapParsoidSections( $container );
|
||||
|
||||
$parser = $services->getService( 'DiscussionTools.CommentParser' );
|
||||
return $parser->parse( $container, $title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a DiscussionTools feature is available to this user
|
||||
*
|
||||
|
|
|
@ -30,5 +30,9 @@ class InstallerHooks implements
|
|||
'discussiontools_subscription',
|
||||
"$base/../sql/$type/discussiontools_subscription.sql"
|
||||
);
|
||||
$updater->addExtensionTable(
|
||||
'discussiontools_items',
|
||||
"$base/../sql/$type/discussiontools_persistent.sql"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,5 +29,22 @@ return [
|
|||
$services->getReadOnlyMode(),
|
||||
$services->getUserFactory()
|
||||
);
|
||||
}
|
||||
},
|
||||
'DiscussionTools.ThreadItemStore' => static function ( MediaWikiServices $services ): ThreadItemStore {
|
||||
return new ThreadItemStore(
|
||||
$services->getConfigFactory(),
|
||||
$services->getDBLoadBalancerFactory(),
|
||||
$services->getReadOnlyMode(),
|
||||
$services->getPageStore(),
|
||||
$services->getRevisionStore(),
|
||||
$services->getTitleFormatter(),
|
||||
$services->getActorStore()
|
||||
);
|
||||
},
|
||||
'DiscussionTools.ThreadItemFormatter' => static function ( MediaWikiServices $services ): ThreadItemFormatter {
|
||||
return new ThreadItemFormatter(
|
||||
$services->getTitleFormatter(),
|
||||
$services->getLinkRenderer()
|
||||
);
|
||||
},
|
||||
];
|
||||
|
|
128
includes/SpecialFindComment.php
Normal file
128
includes/SpecialFindComment.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use FormSpecialPage;
|
||||
use Html;
|
||||
use HTMLForm;
|
||||
|
||||
class SpecialFindComment extends FormSpecialPage {
|
||||
|
||||
/** @var ThreadItemStore */
|
||||
private $threadItemStore;
|
||||
|
||||
/** @var ThreadItemFormatter */
|
||||
private $threadItemFormatter;
|
||||
|
||||
/**
|
||||
* @param ThreadItemStore $threadItemStore
|
||||
* @param ThreadItemFormatter $threadItemFormatter
|
||||
*/
|
||||
public function __construct(
|
||||
ThreadItemStore $threadItemStore,
|
||||
ThreadItemFormatter $threadItemFormatter
|
||||
) {
|
||||
parent::__construct( 'FindComment' );
|
||||
$this->threadItemStore = $threadItemStore;
|
||||
$this->threadItemFormatter = $threadItemFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getFormFields() {
|
||||
return [
|
||||
'idorname' => [
|
||||
'label-message' => 'discussiontools-findcomment-label-idorname',
|
||||
'name' => 'idorname',
|
||||
'type' => 'text',
|
||||
'default' => $this->par,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getDisplayFormat() {
|
||||
return 'ooui';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function alterForm( HTMLForm $form ) {
|
||||
$form->setMethod( 'GET' );
|
||||
$form->setWrapperLegend( true );
|
||||
$form->setSubmitTextMsg( 'discussiontools-findcomment-label-search' );
|
||||
// Remove subpage when submitting
|
||||
$form->setTitle( $this->getPageTitle() );
|
||||
}
|
||||
|
||||
private $idOrName;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function onSubmit( array $data ) {
|
||||
$this->idOrName = $data['idorname'];
|
||||
// Always display the form again
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute( $par ) {
|
||||
parent::execute( $par );
|
||||
|
||||
$out = $this->getOutput();
|
||||
$results = false;
|
||||
|
||||
if ( $this->idOrName ) {
|
||||
$byId = $this->threadItemStore->findNewestRevisionsById( $this->idOrName );
|
||||
if ( $byId ) {
|
||||
$this->displayItems( $byId, 'discussiontools-findcomment-results-id' );
|
||||
$results = true;
|
||||
}
|
||||
|
||||
$byName = $this->threadItemStore->findNewestRevisionsByName( $this->idOrName );
|
||||
if ( $byName ) {
|
||||
$this->displayItems( $byName, 'discussiontools-findcomment-results-name' );
|
||||
$results = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $results ) {
|
||||
$out->addHTML( Html::rawElement( 'p', [],
|
||||
$this->msg( 'discussiontools-findcomment-gotocomment', $this->idOrName )->parse() ) );
|
||||
} elseif ( $this->idOrName ) {
|
||||
$out->addHTML( Html::rawElement( 'p', [],
|
||||
$this->msg( 'discussiontools-findcomment-noresults' )->parse() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $threadItems
|
||||
* @param string $msgKey
|
||||
*/
|
||||
private function displayItems( array $threadItems, string $msgKey ) {
|
||||
$out = $this->getOutput();
|
||||
|
||||
$list = [];
|
||||
foreach ( $threadItems as $item ) {
|
||||
$line = $this->threadItemFormatter->formatLine( $item, $this );
|
||||
$list[] = Html::rawElement( 'li', [], $line );
|
||||
}
|
||||
|
||||
$out->addHTML( Html::rawElement( 'p', [], $this->msg( $msgKey, count( $list ) )->parse() ) );
|
||||
$out->addHTML( Html::rawElement( 'ul', [], implode( '', $list ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getDescription() {
|
||||
return $this->msg( 'discussiontools-findcomment-title' )->text();
|
||||
}
|
||||
}
|
59
includes/SpecialGoToComment.php
Normal file
59
includes/SpecialGoToComment.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use RedirectSpecialPage;
|
||||
use SpecialPage;
|
||||
use Title;
|
||||
|
||||
class SpecialGoToComment extends RedirectSpecialPage {
|
||||
|
||||
/** @var ThreadItemStore */
|
||||
private $threadItemStore;
|
||||
|
||||
/**
|
||||
* @param ThreadItemStore $threadItemStore
|
||||
*/
|
||||
public function __construct(
|
||||
ThreadItemStore $threadItemStore
|
||||
) {
|
||||
parent::__construct( 'GoToComment' );
|
||||
$this->threadItemStore = $threadItemStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getRedirect( $subpage ) {
|
||||
$results = [];
|
||||
|
||||
// Search for all thread items with the given ID or name, returning results from the latest
|
||||
// revision of each page they appeared on.
|
||||
//
|
||||
// If there is exactly one result which is not transcluded from another page and in the current
|
||||
// revision of its page, redirect to it.
|
||||
//
|
||||
// Otherwise, redirect to full search results on Special:FindComment.
|
||||
|
||||
if ( $subpage ) {
|
||||
$threadItems = $this->threadItemStore->findNewestRevisionsById( $subpage );
|
||||
foreach ( $threadItems as $item ) {
|
||||
if ( $item->getRevision()->isCurrent() && !is_string( $item->getTranscludedFrom() ) ) {
|
||||
$results[] = $item;
|
||||
}
|
||||
}
|
||||
$threadItems = $this->threadItemStore->findNewestRevisionsByName( $subpage );
|
||||
foreach ( $threadItems as $item ) {
|
||||
if ( $item->getRevision()->isCurrent() && !is_string( $item->getTranscludedFrom() ) ) {
|
||||
$results[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $results ) === 1 ) {
|
||||
return Title::castFromPageIdentity( $results[0]->getPage() )->createFragmentTarget( $results[0]->getId() );
|
||||
} else {
|
||||
return SpecialPage::getTitleFor( 'FindComment', $subpage ?: false );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use MediaWiki\Page\ProperPageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
||||
class DatabaseCommentItem extends DatabaseThreadItem implements CommentItem {
|
||||
use CommentItemTrait {
|
||||
|
@ -16,6 +18,8 @@ class DatabaseCommentItem extends DatabaseThreadItem implements CommentItem {
|
|||
private $author;
|
||||
|
||||
/**
|
||||
* @param ProperPageIdentity $page
|
||||
* @param RevisionRecord $rev
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
* @param DatabaseThreadItem|null $parent
|
||||
|
@ -25,10 +29,11 @@ class DatabaseCommentItem extends DatabaseThreadItem implements CommentItem {
|
|||
* @param string $author
|
||||
*/
|
||||
public function __construct(
|
||||
ProperPageIdentity $page, RevisionRecord $rev,
|
||||
string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level,
|
||||
string $timestamp, string $author
|
||||
) {
|
||||
parent::__construct( 'comment', $name, $id, $parent, $transcludedFrom, $level );
|
||||
parent::__construct( $page, $rev, 'comment', $name, $id, $parent, $transcludedFrom, $level );
|
||||
$this->timestamp = $timestamp;
|
||||
$this->author = $author;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use MediaWiki\Page\ProperPageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
||||
class DatabaseHeadingItem extends DatabaseThreadItem implements HeadingItem {
|
||||
use HeadingItemTrait;
|
||||
|
||||
|
@ -14,6 +17,8 @@ class DatabaseHeadingItem extends DatabaseThreadItem implements HeadingItem {
|
|||
private const PLACEHOLDER_HEADING_LEVEL = 99;
|
||||
|
||||
/**
|
||||
* @param ProperPageIdentity $page
|
||||
* @param RevisionRecord $rev
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
* @param DatabaseThreadItem|null $parent
|
||||
|
@ -22,10 +27,11 @@ class DatabaseHeadingItem extends DatabaseThreadItem implements HeadingItem {
|
|||
* @param ?int $headingLevel Heading level (1-6). Use null for a placeholder heading.
|
||||
*/
|
||||
public function __construct(
|
||||
ProperPageIdentity $page, RevisionRecord $rev,
|
||||
string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level,
|
||||
?int $headingLevel
|
||||
) {
|
||||
parent::__construct( 'heading', $name, $id, $parent, $transcludedFrom, $level );
|
||||
parent::__construct( $page, $rev, 'heading', $name, $id, $parent, $transcludedFrom, $level );
|
||||
$this->placeholderHeading = $headingLevel === null;
|
||||
$this->headingLevel = $this->placeholderHeading ? static::PLACEHOLDER_HEADING_LEVEL : $headingLevel;
|
||||
}
|
||||
|
|
|
@ -3,10 +3,16 @@
|
|||
namespace MediaWiki\Extension\DiscussionTools\ThreadItem;
|
||||
|
||||
use JsonSerializable;
|
||||
use MediaWiki\Page\ProperPageIdentity;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
||||
class DatabaseThreadItem implements JsonSerializable, ThreadItem {
|
||||
use ThreadItemTrait;
|
||||
|
||||
/** @var ProperPageIdentity */
|
||||
private $page;
|
||||
/** @var RevisionRecord */
|
||||
private $rev;
|
||||
/** @var string */
|
||||
private $type;
|
||||
/** @var string */
|
||||
|
@ -23,6 +29,8 @@ class DatabaseThreadItem implements JsonSerializable, ThreadItem {
|
|||
private $level;
|
||||
|
||||
/**
|
||||
* @param ProperPageIdentity $page
|
||||
* @param RevisionRecord $rev
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param string $id
|
||||
|
@ -31,8 +39,11 @@ class DatabaseThreadItem implements JsonSerializable, ThreadItem {
|
|||
* @param int $level
|
||||
*/
|
||||
public function __construct(
|
||||
ProperPageIdentity $page, RevisionRecord $rev,
|
||||
string $type, string $name, string $id, ?DatabaseThreadItem $parent, $transcludedFrom, int $level
|
||||
) {
|
||||
$this->page = $page;
|
||||
$this->rev = $rev;
|
||||
$this->name = $name;
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
|
@ -41,6 +52,20 @@ class DatabaseThreadItem implements JsonSerializable, ThreadItem {
|
|||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ProperPageIdentity
|
||||
*/
|
||||
public function getPage(): ProperPageIdentity {
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RevisionRecord
|
||||
*/
|
||||
public function getRevision(): RevisionRecord {
|
||||
return $this->rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
|
76
includes/ThreadItemFormatter.php
Normal file
76
includes/ThreadItemFormatter.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseThreadItem;
|
||||
use MediaWiki\Linker\LinkRenderer;
|
||||
use MessageLocalizer;
|
||||
use TitleFormatter;
|
||||
use TitleValue;
|
||||
|
||||
/**
|
||||
* Displays links to comments and headings represented as ThreadItems.
|
||||
*/
|
||||
class ThreadItemFormatter {
|
||||
/** @var TitleFormatter */
|
||||
private $titleFormatter;
|
||||
|
||||
/** @var LinkRenderer */
|
||||
private $linkRenderer;
|
||||
|
||||
/**
|
||||
* @param TitleFormatter $titleFormatter
|
||||
* @param LinkRenderer $linkRenderer
|
||||
*/
|
||||
public function __construct(
|
||||
TitleFormatter $titleFormatter,
|
||||
LinkRenderer $linkRenderer
|
||||
) {
|
||||
$this->titleFormatter = $titleFormatter;
|
||||
$this->linkRenderer = $linkRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a link to a thread item on the page.
|
||||
*
|
||||
* @param DatabaseThreadItem $item
|
||||
* @return string
|
||||
*/
|
||||
public function makeLink( DatabaseThreadItem $item ): string {
|
||||
$title = TitleValue::newFromPage( $item->getPage() )->createFragmentTarget( $item->getId() );
|
||||
|
||||
$query = [];
|
||||
if ( !$item->getRevision()->isCurrent() ) {
|
||||
$query['oldid'] = $item->getRevision()->getId();
|
||||
}
|
||||
|
||||
$text = $this->titleFormatter->getPrefixedText( $title );
|
||||
$link = $this->linkRenderer->makeLink( $title, $text, [], $query );
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a link to a thread item on the page, with additional information (used on special pages).
|
||||
*
|
||||
* @param DatabaseThreadItem $item
|
||||
* @param MessageLocalizer $context
|
||||
* @return string
|
||||
*/
|
||||
public function formatLine( DatabaseThreadItem $item, MessageLocalizer $context ): string {
|
||||
$contents = [];
|
||||
|
||||
$contents[] = $this->makeLink( $item );
|
||||
|
||||
if ( !$item->getRevision()->isCurrent() ) {
|
||||
$contents[] = $context->msg( 'discussiontools-findcomment-results-notcurrent' )->escaped();
|
||||
}
|
||||
|
||||
if ( is_string( $item->getTranscludedFrom() ) ) {
|
||||
$contents[] = $context->msg( 'discussiontools-findcomment-results-transcluded' )->escaped();
|
||||
}
|
||||
|
||||
return implode( $context->msg( 'word-separator' )->escaped(), $contents );
|
||||
}
|
||||
|
||||
}
|
569
includes/ThreadItemStore.php
Normal file
569
includes/ThreadItemStore.php
Normal file
|
@ -0,0 +1,569 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use ConfigFactory;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseCommentItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseHeadingItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\DatabaseThreadItem;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem;
|
||||
use MediaWiki\Page\PageStore;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MediaWiki\User\ActorStore;
|
||||
use MWTimestamp;
|
||||
use ReadOnlyMode;
|
||||
use stdClass;
|
||||
use TitleFormatter;
|
||||
use Wikimedia\Rdbms\ILBFactory;
|
||||
use Wikimedia\Rdbms\ILoadBalancer;
|
||||
use Wikimedia\Rdbms\IMaintainableDatabase;
|
||||
use Wikimedia\Rdbms\IResultWrapper;
|
||||
use Wikimedia\Rdbms\SelectQueryBuilder;
|
||||
|
||||
/**
|
||||
* Stores and fetches ThreadItemSets from the database.
|
||||
*/
|
||||
class ThreadItemStore {
|
||||
/** @var ConfigFactory */
|
||||
private $configFactory;
|
||||
|
||||
/** @var ILoadBalancer */
|
||||
private $loadBalancer;
|
||||
|
||||
/** @var ReadOnlyMode */
|
||||
private $readOnlyMode;
|
||||
|
||||
/** @var PageStore */
|
||||
private $pageStore;
|
||||
|
||||
/** @var RevisionStore */
|
||||
private $revStore;
|
||||
|
||||
/** @var TitleFormatter */
|
||||
private $titleFormatter;
|
||||
|
||||
/** @var ActorStore */
|
||||
private $actorStore;
|
||||
|
||||
/**
|
||||
* @param ConfigFactory $configFactory
|
||||
* @param ILBFactory $lbFactory
|
||||
* @param ReadOnlyMode $readOnlyMode
|
||||
* @param PageStore $pageStore
|
||||
* @param RevisionStore $revStore
|
||||
* @param TitleFormatter $titleFormatter
|
||||
* @param ActorStore $actorStore
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactory $configFactory,
|
||||
ILBFactory $lbFactory,
|
||||
ReadOnlyMode $readOnlyMode,
|
||||
PageStore $pageStore,
|
||||
RevisionStore $revStore,
|
||||
TitleFormatter $titleFormatter,
|
||||
ActorStore $actorStore
|
||||
) {
|
||||
$this->configFactory = $configFactory;
|
||||
$this->loadBalancer = $lbFactory->getMainLB();
|
||||
$this->readOnlyMode = $readOnlyMode;
|
||||
$this->pageStore = $pageStore;
|
||||
$this->revStore = $revStore;
|
||||
$this->titleFormatter = $titleFormatter;
|
||||
$this->actorStore = $actorStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the tables necessary for this feature haven't been created yet,
|
||||
* to allow failing softly in that case.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isDisabled(): bool {
|
||||
static $tablesCreated = null;
|
||||
if ( $tablesCreated === null ) {
|
||||
$tablesCreated = $this->getConnectionRef( DB_REPLICA )->tableExists( 'discussiontools_items', __METHOD__ );
|
||||
}
|
||||
return !$tablesCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $dbIndex DB_PRIMARY or DB_REPLICA
|
||||
*
|
||||
* @return IMaintainableDatabase
|
||||
*/
|
||||
private function getConnectionRef( int $dbIndex ): IMaintainableDatabase {
|
||||
return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the thread items with the given name in the newest revision of every page in which they
|
||||
* have appeared.
|
||||
*
|
||||
* @param string|string[] $itemName
|
||||
* @return DatabaseThreadItem[]
|
||||
*/
|
||||
public function findNewestRevisionsByName( $itemName ): array {
|
||||
if ( $this->isDisabled() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryBuilder = $this->getIdsNamesBuilder()
|
||||
->where( [
|
||||
'it_itemname' => $itemName,
|
||||
// Disallow querying for headings of sections that contain no comments.
|
||||
// They all share the same name, so this would return a huge useless list on most wikis.
|
||||
// (But we still store them, as we might need this data elsewhere.)
|
||||
"it_itemname != 'h-'",
|
||||
] );
|
||||
|
||||
$result = $this->fetchItemsResultSet( $queryBuilder );
|
||||
$revs = $this->fetchRevisionAndPageForItems( $result );
|
||||
|
||||
$threadItems = [];
|
||||
foreach ( $result as $row ) {
|
||||
$threadItem = $this->getThreadItemFromRow( $row, null, $revs );
|
||||
if ( $threadItem ) {
|
||||
$threadItems[] = $threadItem;
|
||||
}
|
||||
}
|
||||
return $threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the thread items with the given ID in the newest revision of every page in which they have
|
||||
* appeared.
|
||||
*
|
||||
* @param string|string[] $itemId
|
||||
* @return DatabaseThreadItem[]
|
||||
*/
|
||||
public function findNewestRevisionsById( $itemId ): array {
|
||||
if ( $this->isDisabled() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryBuilder = $this->getIdsNamesBuilder();
|
||||
|
||||
// First find the name associated with the ID; then find by name. Otherwise we wouldn't find the
|
||||
// latest revision in case comment ID changed, e.g. the comment was moved elsewhere on the page.
|
||||
$itemNameQueryBuilder = $this->getIdsNamesBuilder()
|
||||
->where( [ 'itid_itemid' => $itemId ] )
|
||||
->field( 'it_itemname' );
|
||||
// I think there may be more than 1 only in case of headings?
|
||||
// For comments, any ID corresponds to just 1 name.
|
||||
// Not sure how bad it is to not have limit( 1 ) here?
|
||||
// It might scan a bunch of rows...
|
||||
// ->limit( 1 );
|
||||
|
||||
$queryBuilder
|
||||
->where( [
|
||||
'it_itemname IN (' . $itemNameQueryBuilder->getSQL() . ')',
|
||||
"it_itemname != 'h-'",
|
||||
] );
|
||||
|
||||
$result = $this->fetchItemsResultSet( $queryBuilder );
|
||||
$revs = $this->fetchRevisionAndPageForItems( $result );
|
||||
|
||||
$threadItems = [];
|
||||
foreach ( $result as $row ) {
|
||||
$threadItem = $this->getThreadItemFromRow( $row, null, $revs );
|
||||
if ( $threadItem ) {
|
||||
$threadItems[] = $threadItem;
|
||||
}
|
||||
}
|
||||
return $threadItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SelectQueryBuilder $queryBuilder
|
||||
* @return IResultWrapper
|
||||
*/
|
||||
private function fetchItemsResultSet( SelectQueryBuilder $queryBuilder ): IResultWrapper {
|
||||
$queryBuilder
|
||||
->fields( [
|
||||
'itr_id',
|
||||
'it_itemname',
|
||||
'it_timestamp',
|
||||
'it_actor',
|
||||
'itid_itemid',
|
||||
'itr_parent_id',
|
||||
'itr_transcludedfrom',
|
||||
'itr_level',
|
||||
'itr_headinglevel',
|
||||
'itr_revision_id',
|
||||
] )
|
||||
// PageStore fields for the transcluded-from page
|
||||
->leftJoin( 'page', null, [ 'page_id = itr_transcludedfrom' ] )
|
||||
->fields( $this->pageStore->getSelectFields() )
|
||||
// ActorStore fields for the author
|
||||
->leftJoin( 'actor', null, [ 'actor_id = it_actor' ] )
|
||||
->fields( [ 'actor_id', 'actor_name', 'actor_user' ] )
|
||||
// Parent item ID (the string, not just the primary key)
|
||||
->leftJoin(
|
||||
$this->getIdsNamesBuilder()
|
||||
->fields( [
|
||||
'itr_parent__itr_id' => 'itr_id',
|
||||
'itr_parent__itid_itemid' => 'itid_itemid',
|
||||
] ),
|
||||
null,
|
||||
[ 'itr_parent_id = itr_parent__itr_id' ]
|
||||
)
|
||||
->field( 'itr_parent__itid_itemid' );
|
||||
|
||||
return $queryBuilder->fetchResultSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IResultWrapper $result
|
||||
* @return stdClass[]
|
||||
*/
|
||||
private function fetchRevisionAndPageForItems( IResultWrapper $result ): array {
|
||||
// This could theoretically be done in the same query as fetchItemsResultSet(),
|
||||
// but the resulting query would be two screens long
|
||||
// and we'd have to alias a lot of fields to avoid conflicts.
|
||||
$revs = [];
|
||||
foreach ( $result as $row ) {
|
||||
$revs[ $row->itr_revision_id ] = null;
|
||||
}
|
||||
$revQueryBuilder = $this->getConnectionRef( DB_REPLICA )->newSelectQueryBuilder()
|
||||
->queryInfo( $this->revStore->getQueryInfo( [ 'page' ] ) )
|
||||
->fields( $this->pageStore->getSelectFields() )
|
||||
->where( $revs ? [ 'rev_id' => array_keys( $revs ) ] : '0=1' );
|
||||
$revResult = $revQueryBuilder->fetchResultSet();
|
||||
foreach ( $revResult as $row ) {
|
||||
$revs[ $row->rev_id ] = $row;
|
||||
}
|
||||
return $revs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass $row
|
||||
* @param DatabaseThreadItemSet|null $set
|
||||
* @param array $revs
|
||||
* @return DatabaseThreadItem|null
|
||||
*/
|
||||
private function getThreadItemFromRow(
|
||||
stdClass $row, ?DatabaseThreadItemSet $set, array $revs
|
||||
): ?DatabaseThreadItem {
|
||||
if ( $revs[ $row->itr_revision_id ] === null ) {
|
||||
// We didn't find the 'revision' table row at all, this revision is deleted.
|
||||
// (The page may or may not have other non-deleted revisions.)
|
||||
// Pretend the thread item doesn't exist to avoid leaking data to users who shouldn't see it.
|
||||
// TODO Allow privileged users to see it (we'd need to query from 'archive')
|
||||
return null;
|
||||
}
|
||||
|
||||
$revRow = $revs[$row->itr_revision_id];
|
||||
$page = $this->pageStore->newPageRecordFromRow( $revRow );
|
||||
$rev = $this->revStore->newRevisionFromRow( $revRow );
|
||||
if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
|
||||
// This revision is revision-deleted.
|
||||
// TODO Allow privileged users to see it
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( $set && $row->itr_parent__itid_itemid ) {
|
||||
$parent = $set->findCommentById( $row->itr_parent__itid_itemid );
|
||||
} else {
|
||||
$parent = null;
|
||||
}
|
||||
|
||||
$transcludedFrom = $row->itr_transcludedfrom === null ? false : (
|
||||
$row->itr_transcludedfrom === '0' ? true :
|
||||
$this->titleFormatter->getPrefixedText(
|
||||
$this->pageStore->newPageRecordFromRow( $row )
|
||||
)
|
||||
);
|
||||
|
||||
if ( $row->it_timestamp !== null && $row->it_actor !== null ) {
|
||||
$author = $this->actorStore->newActorFromRow( $row )->getName();
|
||||
|
||||
$item = new DatabaseCommentItem(
|
||||
$page,
|
||||
$rev,
|
||||
$row->it_itemname,
|
||||
$row->itid_itemid,
|
||||
$parent,
|
||||
$transcludedFrom,
|
||||
(int)$row->itr_level,
|
||||
$row->it_timestamp,
|
||||
$author
|
||||
);
|
||||
} else {
|
||||
$item = new DatabaseHeadingItem(
|
||||
$page,
|
||||
$rev,
|
||||
$row->it_itemname,
|
||||
$row->itid_itemid,
|
||||
$parent,
|
||||
$transcludedFrom,
|
||||
(int)$row->itr_level,
|
||||
$row->itr_headinglevel === null ? null : (int)$row->itr_headinglevel
|
||||
);
|
||||
}
|
||||
|
||||
if ( $parent ) {
|
||||
$parent->addReply( $item );
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the thread item set for the given revision, assuming that it is the current revision of
|
||||
* its page.
|
||||
*
|
||||
* @param int $revId
|
||||
* @return DatabaseThreadItemSet
|
||||
*/
|
||||
public function findThreadItemsInCurrentRevision( int $revId ): DatabaseThreadItemSet {
|
||||
if ( $this->isDisabled() ) {
|
||||
return new DatabaseThreadItemSet();
|
||||
}
|
||||
|
||||
$queryBuilder = $this->getIdsNamesBuilder();
|
||||
$queryBuilder
|
||||
->where( [ 'itr_revision_id' => $revId ] )
|
||||
// We must process parents before their children in the loop later
|
||||
->orderBy( 'itr_id', SelectQueryBuilder::SORT_ASC );
|
||||
|
||||
$result = $this->fetchItemsResultSet( $queryBuilder );
|
||||
$revs = $this->fetchRevisionAndPageForItems( $result );
|
||||
|
||||
$set = new DatabaseThreadItemSet();
|
||||
foreach ( $result as $row ) {
|
||||
$threadItem = $this->getThreadItemFromRow( $row, $set, $revs );
|
||||
if ( $threadItem ) {
|
||||
$set->addThreadItem( $threadItem );
|
||||
$set->updateIdAndNameMaps( $threadItem );
|
||||
}
|
||||
}
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SelectQueryBuilder
|
||||
*/
|
||||
private function getIdsNamesBuilder(): SelectQueryBuilder {
|
||||
$dbr = $this->getConnectionRef( DB_REPLICA );
|
||||
|
||||
$queryBuilder = $dbr->newSelectQueryBuilder()
|
||||
->from( 'discussiontools_items' )
|
||||
->join( 'discussiontools_item_pages', null, [ 'itp_items_id = it_id' ] )
|
||||
->join( 'discussiontools_item_revisions', null, [
|
||||
'itr_items_id = it_id',
|
||||
// Only the latest revision of the items with each name
|
||||
'itr_revision_id = itp_newest_revision_id',
|
||||
] )
|
||||
->join( 'discussiontools_item_ids', null, [ 'itid_id = itr_itemid_id' ] );
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the thread item set.
|
||||
*
|
||||
* @param RevisionRecord $rev
|
||||
* @param ThreadItemSet $threadItemSet
|
||||
* @return bool
|
||||
*/
|
||||
public function insertThreadItems( RevisionRecord $rev, ThreadItemSet $threadItemSet ): bool {
|
||||
if ( $this->isDisabled() || $this->readOnlyMode->isReadOnly() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dbw = $this->getConnectionRef( DB_PRIMARY );
|
||||
$didInsert = false;
|
||||
$method = __METHOD__;
|
||||
|
||||
$dbw->doAtomicSection( $method, function ( $dbw ) use ( $method, $rev, $threadItemSet, &$didInsert ) {
|
||||
$itemRevisionsIds = [];
|
||||
foreach ( $threadItemSet->getThreadItems() as $item ) {
|
||||
$itemIdsId = $dbw->selectField(
|
||||
'discussiontools_item_ids',
|
||||
'itid_id',
|
||||
[ 'itid_itemid' => $item->getId() ],
|
||||
$method
|
||||
);
|
||||
if ( $itemIdsId === false ) {
|
||||
$dbw->insert(
|
||||
'discussiontools_item_ids',
|
||||
[
|
||||
'itid_itemid' => $item->getId(),
|
||||
],
|
||||
$method
|
||||
);
|
||||
$itemIdsId = $dbw->insertId();
|
||||
$didInsert = true;
|
||||
}
|
||||
|
||||
$itemsId = $dbw->selectField(
|
||||
'discussiontools_items',
|
||||
'it_id',
|
||||
[ 'it_itemname' => $item->getName() ],
|
||||
$method
|
||||
);
|
||||
if ( $itemsId === false ) {
|
||||
$dbw->insert(
|
||||
'discussiontools_items',
|
||||
[
|
||||
'it_itemname' => $item->getName(),
|
||||
] +
|
||||
( $item instanceof CommentItem ? [
|
||||
'it_timestamp' =>
|
||||
$dbw->timestamp( $item->getTimestampString() ),
|
||||
'it_actor' =>
|
||||
$this->actorStore->findActorIdByName( $item->getAuthor(), $dbw ),
|
||||
] : [] ),
|
||||
$method
|
||||
);
|
||||
$itemsId = $dbw->insertId();
|
||||
$didInsert = true;
|
||||
}
|
||||
|
||||
$itemRevisionsId = $dbw->selectField(
|
||||
'discussiontools_item_revisions',
|
||||
'itr_id',
|
||||
[
|
||||
'itr_itemid_id' => $itemIdsId,
|
||||
'itr_revision_id' => $rev->getId(),
|
||||
],
|
||||
$method
|
||||
);
|
||||
if ( $itemRevisionsId === false ) {
|
||||
$transcl = $item->getTranscludedFrom();
|
||||
$dbw->insert(
|
||||
'discussiontools_item_revisions',
|
||||
[
|
||||
'itr_itemid_id' => $itemIdsId,
|
||||
'itr_revision_id' => $rev->getId(),
|
||||
'itr_items_id' => $itemsId,
|
||||
'itr_parent_id' =>
|
||||
// This assumes that parent items were processed first
|
||||
$item->getParent() ? $itemRevisionsIds[ $item->getParent()->getId() ] : null,
|
||||
'itr_transcludedfrom' =>
|
||||
$transcl === false ? null : (
|
||||
$transcl === true ? 0 :
|
||||
$this->pageStore->getPageByText( $transcl )->getId()
|
||||
),
|
||||
'itr_level' => $item->getLevel(),
|
||||
] +
|
||||
( $item instanceof HeadingItem ? [
|
||||
'itr_headinglevel' => $item->isPlaceholderHeading() ? null : $item->getHeadingLevel(),
|
||||
] : [] ),
|
||||
$method
|
||||
);
|
||||
$itemRevisionsId = $dbw->insertId();
|
||||
$didInsert = true;
|
||||
}
|
||||
$itemRevisionsIds[ $item->getId() ] = $itemRevisionsId;
|
||||
|
||||
// Update (or insert) the references to oldest/newest item revision.
|
||||
// The page revision we're processing is usually the newest one, but it doesn't have to be
|
||||
// (in case of backfilling using the maintenance script, or in case of revisions being
|
||||
// imported), so we need all these funky queries to see if we need to update oldest/newest.
|
||||
|
||||
// This should be a single upsert query (INSERT ... ON DUPLICATE KEY UPDATE), however it
|
||||
// doesn't work in practice:
|
||||
//
|
||||
// - Attempt 1:
|
||||
// https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/771974/14/includes/ThreadItemStore.php#451
|
||||
//
|
||||
// This is the same logic as below in SQL: only doing a single comparison of the timestamp
|
||||
// of the current revision to the existing data in discussiontools_item_pages.
|
||||
// It worked great on my machine, `mysql --version`:
|
||||
// mysql Ver 8.0.29-0ubuntu0.20.04.3 for Linux on x86_64 ((Ubuntu))
|
||||
// but it failed in Wikimedia CI, `mysql --version`:
|
||||
// mysql Ver 15.1 Distrib 10.3.34-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
|
||||
// …with the error:
|
||||
// "Error 1054: Unknown column 'itp_oldest_revision_id' in 'where clause'".
|
||||
// Apparently it doesn't like dependent subqueries in the UPDATE part of an upsert.
|
||||
// I'm not sure if it's a bug in MariaDB or if you're not supposed to do that.
|
||||
//
|
||||
// - Attempt 2:
|
||||
// https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DiscussionTools/+/771974/15/includes/ThreadItemStore.php#451
|
||||
//
|
||||
// This avoids the dependent subquery: instead of comparing to the existing data in
|
||||
// discussiontools_item_pages, it just takes the IDs with min/max timestamp from
|
||||
// discussiontools_item_revisions/revision. This should be a simple lookup from an index,
|
||||
// but apparently it doesn't work that way and is significantly slower (the maintenance
|
||||
// script went from 13 minutes to 18 minutes when processing a few thousands of revisions
|
||||
// on my local testing wiki).
|
||||
//
|
||||
// In the end, the solution below using multiple queries is just as fast as the original,
|
||||
// and only a little more verbose.
|
||||
|
||||
$itemPagesRow = $dbw->newSelectQueryBuilder()
|
||||
->from( 'discussiontools_item_pages' )
|
||||
->join( 'revision', 'revision_oldest', [ 'itp_oldest_revision_id = revision_oldest.rev_id' ] )
|
||||
->join( 'revision', 'revision_newest', [ 'itp_newest_revision_id = revision_newest.rev_id' ] )
|
||||
->field( 'itp_id' )
|
||||
->field( 'revision_oldest.rev_timestamp', 'oldest_rev_timestamp' )
|
||||
->field( 'revision_newest.rev_timestamp', 'newest_rev_timestamp' )
|
||||
->where( [
|
||||
'itp_items_id' => $itemsId,
|
||||
'itp_page_id' => $rev->getPageId(),
|
||||
] )
|
||||
->fetchRow();
|
||||
if ( $itemPagesRow === false ) {
|
||||
$dbw->insert(
|
||||
'discussiontools_item_pages',
|
||||
[
|
||||
'itp_items_id' => $itemsId,
|
||||
'itp_page_id' => $rev->getPageId(),
|
||||
'itp_oldest_revision_id' => $rev->getId(),
|
||||
'itp_newest_revision_id' => $rev->getId(),
|
||||
],
|
||||
$method
|
||||
);
|
||||
} else {
|
||||
$existingTime = ( new MWTimestamp( $itemPagesRow->oldest_rev_timestamp ) )->getTimestamp( TS_MW );
|
||||
if ( $existingTime >= $rev->getTimestamp() ) {
|
||||
$dbw->update(
|
||||
'discussiontools_item_pages',
|
||||
[ 'itp_oldest_revision_id' => $rev->getId() ],
|
||||
[ 'itp_id' => $itemPagesRow->itp_id ],
|
||||
$method
|
||||
);
|
||||
}
|
||||
$existingTime = ( new MWTimestamp( $itemPagesRow->newest_rev_timestamp ) )->getTimestamp( TS_MW );
|
||||
if ( $existingTime <= $rev->getTimestamp() ) {
|
||||
$dbw->update(
|
||||
'discussiontools_item_pages',
|
||||
[ 'itp_newest_revision_id' => $rev->getId() ],
|
||||
[ 'itp_id' => $itemPagesRow->itp_id ],
|
||||
$method
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete rows that we don't care about, to save space (item revisions with the same ID and
|
||||
// name as the one we just inserted, which are not the oldest or newest revision).
|
||||
$oldestRevisionSql = $dbw->selectSQLText(
|
||||
'discussiontools_item_pages',
|
||||
'itp_oldest_revision_id',
|
||||
[ 'itp_items_id' => $itemsId ],
|
||||
$method
|
||||
);
|
||||
$newestRevisionSql = $dbw->selectSQLText(
|
||||
'discussiontools_item_pages',
|
||||
'itp_newest_revision_id',
|
||||
[ 'itp_items_id' => $itemsId ],
|
||||
$method
|
||||
);
|
||||
$dbw->delete(
|
||||
'discussiontools_item_revisions',
|
||||
[
|
||||
'itr_itemid_id' => $itemIdsId,
|
||||
'itr_items_id' => $itemsId,
|
||||
"itr_revision_id NOT IN ($oldestRevisionSql)",
|
||||
"itr_revision_id NOT IN ($newestRevisionSql)",
|
||||
],
|
||||
$method
|
||||
);
|
||||
}
|
||||
}, $dbw::ATOMIC_CANCELABLE );
|
||||
|
||||
return $didInsert;
|
||||
}
|
||||
}
|
123
maintenance/persistRevisionThreadItems.php
Normal file
123
maintenance/persistRevisionThreadItems.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\Maintenance;
|
||||
|
||||
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
||||
use MediaWiki\Extension\DiscussionTools\ThreadItemStore;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\RevisionStore;
|
||||
use MWExceptionRenderer;
|
||||
use stdClass;
|
||||
use TableCleanup;
|
||||
use Throwable;
|
||||
use Title;
|
||||
|
||||
// Security: Disable all stream wrappers and reenable individually as needed
|
||||
foreach ( stream_get_wrappers() as $wrapper ) {
|
||||
stream_wrapper_unregister( $wrapper );
|
||||
}
|
||||
|
||||
stream_wrapper_restore( 'file' );
|
||||
$basePath = getenv( 'MW_INSTALL_PATH' );
|
||||
if ( $basePath ) {
|
||||
if ( !is_dir( $basePath )
|
||||
|| strpos( $basePath, '.' ) !== false
|
||||
|| strpos( $basePath, '~' ) !== false
|
||||
) {
|
||||
die( "Bad MediaWiki install path: $basePath\n" );
|
||||
}
|
||||
} else {
|
||||
$basePath = __DIR__ . '/../../..';
|
||||
}
|
||||
require_once "$basePath/maintenance/Maintenance.php";
|
||||
// Autoloader isn't set up yet until we do `require_once RUN_MAINTENANCE_IF_MAIN`…
|
||||
// but our class needs to exist at that point D:
|
||||
require_once "$basePath/maintenance/TableCleanup.php";
|
||||
|
||||
class PersistRevisionThreadItems extends TableCleanup {
|
||||
|
||||
/** @var ThreadItemStore */
|
||||
private $itemStore;
|
||||
|
||||
/** @var RevisionStore */
|
||||
private $revStore;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$this->requireExtension( 'DiscussionTools' );
|
||||
$this->addDescription( 'Persist thread item information for the given pages/revisions' );
|
||||
$this->addOption( 'rev', 'Revision ID to process', false, true, false, true );
|
||||
$this->addOption( 'page', 'Page title to process', false, true, false, true );
|
||||
$this->addOption( 'all', 'Process the whole wiki', false, false, false, false );
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
|
||||
$this->itemStore = $services->getService( 'DiscussionTools.ThreadItemStore' );
|
||||
$this->revStore = $services->getRevisionStore();
|
||||
|
||||
if ( $this->getOption( 'all' ) ) {
|
||||
$conds = [];
|
||||
|
||||
} elseif ( $this->getOption( 'page' ) ) {
|
||||
$linkBatch = $services->getLinkBatchFactory()->newLinkBatch();
|
||||
foreach ( $this->getOption( 'page' ) as $page ) {
|
||||
$linkBatch->addObj( Title::newFromText( $page ) );
|
||||
}
|
||||
$pageIds = array_map( static function ( $page ) {
|
||||
return $page->getId();
|
||||
}, $linkBatch->getPageIdentities() );
|
||||
|
||||
$conds = [ 'rev_page' => $pageIds ];
|
||||
|
||||
} elseif ( $this->getOption( 'rev' ) ) {
|
||||
$conds = [ 'rev_id' => $this->getOption( 'rev' ) ];
|
||||
} else {
|
||||
$this->error( "One of 'all', 'page', or 'rev' required" );
|
||||
$this->maybeHelp( true );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->runTable( [
|
||||
'table' => 'revision',
|
||||
'conds' => $conds,
|
||||
'index' => [ 'rev_page', 'rev_timestamp', 'rev_id' ],
|
||||
'callback' => 'processRow',
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass $row Database table row
|
||||
*/
|
||||
protected function processRow( stdClass $row ) {
|
||||
$changed = false;
|
||||
try {
|
||||
// HACK (because we don't query the table this data ordinarily comes from,
|
||||
// and we don't care about edit summaries here)
|
||||
$row->rev_comment_text = '';
|
||||
$row->rev_comment_data = null;
|
||||
$row->rev_comment_cid = null;
|
||||
|
||||
$rev = $this->revStore->newRevisionFromRow( $row );
|
||||
$title = Title::newFromLinkTarget(
|
||||
$rev->getPageAsLinkTarget()
|
||||
);
|
||||
if ( HookUtils::isAvailableForTitle( $title ) ) {
|
||||
$threadItemSet = HookUtils::parseRevisionParsoidHtml( $rev );
|
||||
|
||||
if ( !$this->dryrun ) {
|
||||
// Store permalink data
|
||||
$changed = $this->itemStore->insertThreadItems( $rev, $threadItemSet );
|
||||
}
|
||||
}
|
||||
} catch ( Throwable $e ) {
|
||||
$this->output( "Error while processing revid=$row->rev_id, pageid=$row->rev_page\n" );
|
||||
MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW );
|
||||
}
|
||||
$this->progress( (int)$changed );
|
||||
}
|
||||
}
|
||||
|
||||
$maintClass = PersistRevisionThreadItems::class;
|
||||
require_once RUN_MAINTENANCE_IF_MAIN;
|
260
sql/discussiontools_persistent.json
Normal file
260
sql/discussiontools_persistent.json
Normal file
|
@ -0,0 +1,260 @@
|
|||
[
|
||||
{
|
||||
"name": "discussiontools_items",
|
||||
"columns": [
|
||||
{
|
||||
"name": "it_id",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"autoincrement": true,
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "it_itemname",
|
||||
"comment": "Internal name used to identify this item across all pages and revisions where it might appear, see CommentParser::computeName()",
|
||||
"type": "binary",
|
||||
"options": {
|
||||
"notnull": true,
|
||||
"length": 255
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "it_timestamp",
|
||||
"comment": "Date and time from signature, or null for headings",
|
||||
"type": "mwtimestamp",
|
||||
"options": {
|
||||
"notnull": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "it_actor",
|
||||
"comment": "Author from signature, or null for headings. (key to actor.actor_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "it_itemname",
|
||||
"comment": "",
|
||||
"columns": [
|
||||
"it_itemname"
|
||||
],
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"pk": [
|
||||
"it_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "discussiontools_item_ids",
|
||||
"columns": [
|
||||
{
|
||||
"name": "itid_id",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"autoincrement": true,
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itid_itemid",
|
||||
"comment": "Internal ID used to identify this item in this revision, see CommentParser::computeId()",
|
||||
"type": "binary",
|
||||
"options": {
|
||||
"notnull": true,
|
||||
"length": 255
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "itid_itemid",
|
||||
"comment": "",
|
||||
"columns": [
|
||||
"itid_itemid"
|
||||
],
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"pk": [
|
||||
"itid_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "discussiontools_item_pages",
|
||||
"comment": "Data in this table is redundant to discussiontools_item_revisions, but if we want to find all pages where a given comment has appeared, querying it from that table would require some awful joins and be expensive",
|
||||
"columns": [
|
||||
{
|
||||
"name": "itp_id",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"autoincrement": true,
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itp_items_id",
|
||||
"comment": "(key to discussiontools_items.it_id)",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itp_page_id",
|
||||
"comment": "(key to page.page_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itp_oldest_revision_id",
|
||||
"comment": "(key to revision.rev_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itp_newest_revision_id",
|
||||
"comment": "(key to revision.rev_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "itp_items_id_page_id",
|
||||
"comment": "insertThreadItems()",
|
||||
"columns": [
|
||||
"itp_items_id",
|
||||
"itp_page_id"
|
||||
],
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"name": "itp_items_id_newest_revision_id",
|
||||
"comment": "findNewestRevisionsByName(), findNewestRevisionsById()",
|
||||
"columns": [
|
||||
"itp_items_id",
|
||||
"itp_newest_revision_id"
|
||||
],
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"pk": [
|
||||
"itp_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "discussiontools_item_revisions",
|
||||
"comment": "",
|
||||
"columns": [
|
||||
{
|
||||
"name": "itr_id",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"autoincrement": true,
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_itemid_id",
|
||||
"comment": "(key to discussiontools_item_ids.itid_id)",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_revision_id",
|
||||
"comment": "(key to revision.rev_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_items_id",
|
||||
"comment": "(key to discussiontools_items.it_id)",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_parent_id",
|
||||
"comment": "(key to discussiontools_item_revisions.itr_id)",
|
||||
"type": "integer",
|
||||
"options": {
|
||||
"notnull": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_transcludedfrom",
|
||||
"comment": "Page where this item is transcluded from (null: not transcluded, 0: transcluded from unknown page) (key to page.page_id)",
|
||||
"type": "bigint",
|
||||
"options": {
|
||||
"unsigned": true,
|
||||
"notnull": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_level",
|
||||
"comment": "Indentation level (0 for headings, 1+ for comments)",
|
||||
"type": "mwtinyint",
|
||||
"options": {
|
||||
"notnull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itr_headinglevel",
|
||||
"comment": "Heading level (1-6), or null for placeholder headings and comments",
|
||||
"type": "mwtinyint",
|
||||
"options": {
|
||||
"notnull": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "itr_revision_id",
|
||||
"comment": "findThreadItemsInCurrentRevision()",
|
||||
"columns": [
|
||||
"itr_revision_id"
|
||||
],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "itr_itemid_id_revision_id",
|
||||
"comment": "findNewestRevisionsByName(), findNewestRevisionsById(), insertThreadItems()",
|
||||
"columns": [
|
||||
"itr_itemid_id",
|
||||
"itr_revision_id"
|
||||
],
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"pk": [
|
||||
"itr_id"
|
||||
]
|
||||
}
|
||||
]
|
49
sql/mysql/discussiontools_persistent.sql
Normal file
49
sql/mysql/discussiontools_persistent.sql
Normal file
|
@ -0,0 +1,49 @@
|
|||
-- This file is automatically generated using maintenance/generateSchemaSql.php.
|
||||
-- Source: sql/discussiontools_persistent.json
|
||||
-- Do not modify this file directly.
|
||||
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
|
||||
CREATE TABLE /*_*/discussiontools_items (
|
||||
it_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
it_itemname VARBINARY(255) NOT NULL,
|
||||
it_timestamp BINARY(14) DEFAULT NULL,
|
||||
it_actor BIGINT UNSIGNED DEFAULT NULL,
|
||||
UNIQUE INDEX it_itemname (it_itemname),
|
||||
PRIMARY KEY(it_id)
|
||||
) /*$wgDBTableOptions*/;
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_ids (
|
||||
itid_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
itid_itemid VARBINARY(255) NOT NULL,
|
||||
UNIQUE INDEX itid_itemid (itid_itemid),
|
||||
PRIMARY KEY(itid_id)
|
||||
) /*$wgDBTableOptions*/;
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_pages (
|
||||
itp_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
itp_items_id INT UNSIGNED NOT NULL,
|
||||
itp_page_id BIGINT UNSIGNED NOT NULL,
|
||||
itp_oldest_revision_id BIGINT UNSIGNED NOT NULL,
|
||||
itp_newest_revision_id BIGINT UNSIGNED NOT NULL,
|
||||
UNIQUE INDEX itp_items_id_page_id (itp_items_id, itp_page_id),
|
||||
UNIQUE INDEX itp_items_id_newest_revision_id (
|
||||
itp_items_id, itp_newest_revision_id
|
||||
),
|
||||
PRIMARY KEY(itp_id)
|
||||
) /*$wgDBTableOptions*/;
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_revisions (
|
||||
itr_id INT UNSIGNED AUTO_INCREMENT NOT NULL,
|
||||
itr_itemid_id INT NOT NULL,
|
||||
itr_revision_id BIGINT UNSIGNED NOT NULL,
|
||||
itr_items_id INT NOT NULL,
|
||||
itr_parent_id INT DEFAULT NULL,
|
||||
itr_transcludedfrom BIGINT UNSIGNED DEFAULT NULL,
|
||||
itr_level TINYINT NOT NULL,
|
||||
itr_headinglevel TINYINT DEFAULT NULL,
|
||||
INDEX itr_revision_id (itr_revision_id),
|
||||
UNIQUE INDEX itr_itemid_id_revision_id (itr_itemid_id, itr_revision_id),
|
||||
PRIMARY KEY(itr_id)
|
||||
) /*$wgDBTableOptions*/;
|
55
sql/postgres/discussiontools_persistent.sql
Normal file
55
sql/postgres/discussiontools_persistent.sql
Normal file
|
@ -0,0 +1,55 @@
|
|||
-- This file is automatically generated using maintenance/generateSchemaSql.php.
|
||||
-- Source: sql/discussiontools_persistent.json
|
||||
-- Do not modify this file directly.
|
||||
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
|
||||
CREATE TABLE discussiontools_items (
|
||||
it_id SERIAL NOT NULL,
|
||||
it_itemname TEXT NOT NULL,
|
||||
it_timestamp TIMESTAMPTZ DEFAULT NULL,
|
||||
it_actor BIGINT DEFAULT NULL,
|
||||
PRIMARY KEY(it_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX it_itemname ON discussiontools_items (it_itemname);
|
||||
|
||||
|
||||
CREATE TABLE discussiontools_item_ids (
|
||||
itid_id SERIAL NOT NULL,
|
||||
itid_itemid TEXT NOT NULL,
|
||||
PRIMARY KEY(itid_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX itid_itemid ON discussiontools_item_ids (itid_itemid);
|
||||
|
||||
|
||||
CREATE TABLE discussiontools_item_pages (
|
||||
itp_id SERIAL NOT NULL,
|
||||
itp_items_id INT NOT NULL,
|
||||
itp_page_id BIGINT NOT NULL,
|
||||
itp_oldest_revision_id BIGINT NOT NULL,
|
||||
itp_newest_revision_id BIGINT NOT NULL,
|
||||
PRIMARY KEY(itp_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX itp_items_id_page_id ON discussiontools_item_pages (itp_items_id, itp_page_id);
|
||||
|
||||
CREATE UNIQUE INDEX itp_items_id_newest_revision_id ON discussiontools_item_pages (
|
||||
itp_items_id, itp_newest_revision_id
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE discussiontools_item_revisions (
|
||||
itr_id SERIAL NOT NULL,
|
||||
itr_itemid_id INT NOT NULL,
|
||||
itr_revision_id BIGINT NOT NULL,
|
||||
itr_items_id INT NOT NULL,
|
||||
itr_parent_id INT DEFAULT NULL,
|
||||
itr_transcludedfrom BIGINT DEFAULT NULL,
|
||||
itr_level SMALLINT NOT NULL,
|
||||
itr_headinglevel SMALLINT DEFAULT NULL,
|
||||
PRIMARY KEY(itr_id)
|
||||
);
|
||||
|
||||
CREATE INDEX itr_revision_id ON discussiontools_item_revisions (itr_revision_id);
|
||||
|
||||
CREATE UNIQUE INDEX itr_itemid_id_revision_id ON discussiontools_item_revisions (itr_itemid_id, itr_revision_id);
|
47
sql/sqlite/discussiontools_persistent.sql
Normal file
47
sql/sqlite/discussiontools_persistent.sql
Normal file
|
@ -0,0 +1,47 @@
|
|||
-- This file is automatically generated using maintenance/generateSchemaSql.php.
|
||||
-- Source: sql/discussiontools_persistent.json
|
||||
-- Do not modify this file directly.
|
||||
-- See https://www.mediawiki.org/wiki/Manual:Schema_changes
|
||||
CREATE TABLE /*_*/discussiontools_items (
|
||||
it_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
it_itemname BLOB NOT NULL, it_timestamp BLOB DEFAULT NULL,
|
||||
it_actor BIGINT UNSIGNED DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX it_itemname ON /*_*/discussiontools_items (it_itemname);
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_ids (
|
||||
itid_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
itid_itemid BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX itid_itemid ON /*_*/discussiontools_item_ids (itid_itemid);
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_pages (
|
||||
itp_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
itp_items_id INTEGER UNSIGNED NOT NULL,
|
||||
itp_page_id BIGINT UNSIGNED NOT NULL,
|
||||
itp_oldest_revision_id BIGINT UNSIGNED NOT NULL,
|
||||
itp_newest_revision_id BIGINT UNSIGNED NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX itp_items_id_page_id ON /*_*/discussiontools_item_pages (itp_items_id, itp_page_id);
|
||||
|
||||
CREATE UNIQUE INDEX itp_items_id_newest_revision_id ON /*_*/discussiontools_item_pages (
|
||||
itp_items_id, itp_newest_revision_id
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE /*_*/discussiontools_item_revisions (
|
||||
itr_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
itr_itemid_id INTEGER NOT NULL, itr_revision_id BIGINT UNSIGNED NOT NULL,
|
||||
itr_items_id INTEGER NOT NULL, itr_parent_id INTEGER DEFAULT NULL,
|
||||
itr_transcludedfrom BIGINT UNSIGNED DEFAULT NULL,
|
||||
itr_level SMALLINT NOT NULL, itr_headinglevel SMALLINT DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX itr_revision_id ON /*_*/discussiontools_item_revisions (itr_revision_id);
|
||||
|
||||
CREATE UNIQUE INDEX itr_itemid_id_revision_id ON /*_*/discussiontools_item_revisions (itr_itemid_id, itr_revision_id);
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720010100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720010100-A"
|
||||
},
|
||||
{
|
||||
"itid_id": "3",
|
||||
"itid_itemid": "c-Y-20220720010200-X-20220720010100"
|
||||
},
|
||||
{
|
||||
"itid_id": "4",
|
||||
"itid_itemid": "c-Z-20220720010300-Y-20220720010200"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "3",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "4",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,72 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "5",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "4",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "6",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "7",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "6",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "8",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "7",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "9",
|
||||
"itr_itemid_id": "4",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "4",
|
||||
"itr_parent_id": "8",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "3",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720010100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720010100",
|
||||
"it_timestamp": "20220720010100",
|
||||
"it_actor": "2"
|
||||
},
|
||||
{
|
||||
"it_id": "3",
|
||||
"it_itemname": "c-Y-20220720010200",
|
||||
"it_timestamp": "20220720010200",
|
||||
"it_actor": "3"
|
||||
},
|
||||
{
|
||||
"it_id": "4",
|
||||
"it_itemname": "c-Z-20220720010300",
|
||||
"it_timestamp": "20220720010300",
|
||||
"it_actor": "4"
|
||||
}
|
||||
]
|
33
tests/cases/ThreadItemStore/1simple-example/dump.xml
Normal file
33
tests/cases/ThreadItemStore/1simple-example/dump.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore1</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 01:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T01:01:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Y</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 01:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 01:02, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T01:02:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 01:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 01:02, 20 July 2022 (UTC)
|
||||
::D. --[[User:Z]] 01:03, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T01:03:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720020100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720020100-A"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "2"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "2"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "3",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "4",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "3",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720020100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720020100",
|
||||
"it_timestamp": "20220720020100",
|
||||
"it_actor": "2"
|
||||
}
|
||||
]
|
33
tests/cases/ThreadItemStore/2archived-section/dump.xml
Normal file
33
tests/cases/ThreadItemStore/2archived-section/dump.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore2</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 02:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T02:01:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve" />
|
||||
<timestamp>2022-07-20T02:02:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore2/Archive</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 02:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T02:03:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720030100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720030100-A"
|
||||
},
|
||||
{
|
||||
"itid_id": "3",
|
||||
"itid_itemid": "h-C-20220720030100"
|
||||
},
|
||||
{
|
||||
"itid_id": "4",
|
||||
"itid_itemid": "c-X-20220720030100-C"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "2"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "2"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "3"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "3"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "3",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "4",
|
||||
"itr_itemid_id": "4",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "3",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720030100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720030100",
|
||||
"it_timestamp": "20220720030100",
|
||||
"it_actor": "2"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore3</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 03:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T03:01:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore3b</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== C ==
|
||||
D. --[[User:X]] 03:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T03:01:01Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720040100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720040100-A"
|
||||
},
|
||||
{
|
||||
"itid_id": "3",
|
||||
"itid_itemid": "c-Y-20220720040200-X-20220720040100"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,44 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "3"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "3"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "3",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "3"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "5",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "6",
|
||||
"itp_items_id": "3",
|
||||
"itp_page_id": "3",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "4"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,82 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "3",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "4",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "3",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "5",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "4",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "6",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": "2",
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "7",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "6",
|
||||
"itr_transcludedfrom": "2",
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "8",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "7",
|
||||
"itr_transcludedfrom": "2",
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720040100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720040100",
|
||||
"it_timestamp": "20220720040100",
|
||||
"it_actor": "2"
|
||||
},
|
||||
{
|
||||
"it_id": "3",
|
||||
"it_itemname": "c-Y-20220720040200",
|
||||
"it_timestamp": "20220720040200",
|
||||
"it_actor": "3"
|
||||
}
|
||||
]
|
34
tests/cases/ThreadItemStore/4transcluded-section/dump.xml
Normal file
34
tests/cases/ThreadItemStore/4transcluded-section/dump.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore4/b</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 04:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T04:01:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Y</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 04:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 04:02, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T04:02:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore4</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">{{Talk:ThreadItemStore4/b}}</text>
|
||||
<timestamp>2022-07-20T04:03:00Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720050100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720050100-A"
|
||||
},
|
||||
{
|
||||
"itid_id": "3",
|
||||
"itid_itemid": "c-Y-20220720050200-X-20220720050100"
|
||||
},
|
||||
{
|
||||
"itid_id": "4",
|
||||
"itid_itemid": "c-Z-20220720050300-Y-20220720050200"
|
||||
},
|
||||
{
|
||||
"itid_id": "5",
|
||||
"itid_itemid": "c-Z-20220720050300-X-20220720050100"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "5"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "5"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "3",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "5"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "4",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "4",
|
||||
"itp_newest_revision_id": "5"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,82 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "5",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "4",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "9",
|
||||
"itr_itemid_id": "4",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "4",
|
||||
"itr_parent_id": "8",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "3",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "10",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "5",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "11",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "5",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "10",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "12",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "5",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "11",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "13",
|
||||
"itr_itemid_id": "5",
|
||||
"itr_revision_id": "5",
|
||||
"itr_items_id": "4",
|
||||
"itr_parent_id": "11",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "2",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720050100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720050100",
|
||||
"it_timestamp": "20220720050100",
|
||||
"it_actor": "2"
|
||||
},
|
||||
{
|
||||
"it_id": "3",
|
||||
"it_itemname": "c-Y-20220720050200",
|
||||
"it_timestamp": "20220720050200",
|
||||
"it_actor": "3"
|
||||
},
|
||||
{
|
||||
"it_id": "4",
|
||||
"it_itemname": "c-Z-20220720050300",
|
||||
"it_timestamp": "20220720050300",
|
||||
"it_actor": "4"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore5</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 05:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T05:01:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Y</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 05:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 05:02, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T05:02:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 05:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 05:02, 20 July 2022 (UTC)
|
||||
::D. --[[User:Z]] 05:03, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T05:03:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 05:01, 20 July 2022 (UTC)
|
||||
:C. --[[User:Y]] 05:02, 20 July 2022 (UTC)
|
||||
:D. --[[User:Z]] 05:03, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T05:03:01Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"itid_id": "1",
|
||||
"itid_itemid": "h-A-20220720060100"
|
||||
},
|
||||
{
|
||||
"itid_id": "2",
|
||||
"itid_itemid": "c-X-20220720060100-A"
|
||||
},
|
||||
{
|
||||
"itid_id": "3",
|
||||
"itid_itemid": "h-C-20220720060200"
|
||||
},
|
||||
{
|
||||
"itid_id": "4",
|
||||
"itid_itemid": "c-Y-20220720060200-C"
|
||||
},
|
||||
{
|
||||
"itid_id": "5",
|
||||
"itid_itemid": "h-C-A-20220720060200"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
[
|
||||
{
|
||||
"itp_id": "1",
|
||||
"itp_items_id": "1",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "2",
|
||||
"itp_items_id": "2",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "2",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "3",
|
||||
"itp_items_id": "3",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "4"
|
||||
},
|
||||
{
|
||||
"itp_id": "4",
|
||||
"itp_items_id": "4",
|
||||
"itp_page_id": "2",
|
||||
"itp_oldest_revision_id": "3",
|
||||
"itp_newest_revision_id": "4"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,82 @@
|
|||
[
|
||||
{
|
||||
"itr_id": "1",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "2",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "2",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "1",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "5",
|
||||
"itr_itemid_id": "3",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "6",
|
||||
"itr_itemid_id": "4",
|
||||
"itr_revision_id": "3",
|
||||
"itr_items_id": "4",
|
||||
"itr_parent_id": "5",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "7",
|
||||
"itr_itemid_id": "1",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "1",
|
||||
"itr_parent_id": null,
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "2"
|
||||
},
|
||||
{
|
||||
"itr_id": "8",
|
||||
"itr_itemid_id": "2",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "2",
|
||||
"itr_parent_id": "7",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
},
|
||||
{
|
||||
"itr_id": "9",
|
||||
"itr_itemid_id": "5",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "3",
|
||||
"itr_parent_id": "7",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "0",
|
||||
"itr_headinglevel": "3"
|
||||
},
|
||||
{
|
||||
"itr_id": "10",
|
||||
"itr_itemid_id": "4",
|
||||
"itr_revision_id": "4",
|
||||
"itr_items_id": "4",
|
||||
"itr_parent_id": "9",
|
||||
"itr_transcludedfrom": null,
|
||||
"itr_level": "1",
|
||||
"itr_headinglevel": null
|
||||
}
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"it_id": "1",
|
||||
"it_itemname": "h-X-20220720060100",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "2",
|
||||
"it_itemname": "c-X-20220720060100",
|
||||
"it_timestamp": "20220720060100",
|
||||
"it_actor": "2"
|
||||
},
|
||||
{
|
||||
"it_id": "3",
|
||||
"it_itemname": "h-Y-20220720060200",
|
||||
"it_timestamp": null,
|
||||
"it_actor": null
|
||||
},
|
||||
{
|
||||
"it_id": "4",
|
||||
"it_itemname": "c-Y-20220720060200",
|
||||
"it_timestamp": "20220720060200",
|
||||
"it_actor": "3"
|
||||
}
|
||||
]
|
34
tests/cases/ThreadItemStore/6changed-heading-level/dump.xml
Normal file
34
tests/cases/ThreadItemStore/6changed-heading-level/dump.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
|
||||
<page>
|
||||
<title>Talk:ThreadItemStore6</title>
|
||||
<ns>1</ns>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>X</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 06:01, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T06:01:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Y</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 06:01, 20 July 2022 (UTC)
|
||||
== C ==
|
||||
D. --[[User:Y]] 06:02, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T06:02:00Z</timestamp>
|
||||
</revision>
|
||||
<revision>
|
||||
<contributor>
|
||||
<username>Z</username>
|
||||
</contributor>
|
||||
<text xml:space="preserve">== A ==
|
||||
B. --[[User:X]] 06:01, 20 July 2022 (UTC)
|
||||
=== C ===
|
||||
D. --[[User:Y]] 06:02, 20 July 2022 (UTC)</text>
|
||||
<timestamp>2022-07-20T06:02:01Z</timestamp>
|
||||
</revision>
|
||||
</page>
|
||||
</mediawiki>
|
104
tests/phpunit/ThreadItemStoreTest.php
Normal file
104
tests/phpunit/ThreadItemStoreTest.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools\Tests;
|
||||
|
||||
use ImportStringSource;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use TestUser;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \MediaWiki\Extension\DiscussionTools\ThreadItemStore
|
||||
*
|
||||
* @group DiscussionTools
|
||||
* @group Database
|
||||
*/
|
||||
class ThreadItemStoreTest extends IntegrationTestCase {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCliArg( $offset ) {
|
||||
// Work around MySQL bug (T256006)
|
||||
if ( $offset === 'use-normal-tables' ) {
|
||||
return true;
|
||||
}
|
||||
return parent::getCliArg( $offset );
|
||||
}
|
||||
|
||||
/** @var @inheritDoc */
|
||||
protected $tablesUsed = [
|
||||
'user',
|
||||
'page',
|
||||
'revision',
|
||||
'discussiontools_items',
|
||||
'discussiontools_item_pages',
|
||||
'discussiontools_item_revisions',
|
||||
'discussiontools_item_ids',
|
||||
];
|
||||
|
||||
/**
|
||||
* @dataProvider provideInsertCases
|
||||
* @covers ::insertThreadItems
|
||||
*/
|
||||
public function testInsertThreadItems( string $dir ): void {
|
||||
// Create users for the imported revisions
|
||||
new TestUser( 'X' );
|
||||
new TestUser( 'Y' );
|
||||
new TestUser( 'Z' );
|
||||
|
||||
// Import revisions
|
||||
$source = new ImportStringSource( static::getText( "$dir/dump.xml" ) );
|
||||
$importer = MediaWikiServices::getInstance()
|
||||
->getWikiImporterFactory()
|
||||
->getWikiImporter( $source );
|
||||
// `true` means to assign edits to the users we created above
|
||||
$importer->setUsernamePrefix( 'import', true );
|
||||
$importer->doImport();
|
||||
|
||||
// Check that expected data has been stored in the database
|
||||
$expected = [];
|
||||
$actual = [];
|
||||
$tables = [
|
||||
'discussiontools_items',
|
||||
'discussiontools_item_pages',
|
||||
'discussiontools_item_revisions',
|
||||
'discussiontools_item_ids',
|
||||
];
|
||||
foreach ( $tables as $table ) {
|
||||
$expected[$table] = static::getJson( "../$dir/$table.json", true );
|
||||
|
||||
$res = wfGetDb( DB_REPLICA )->select(
|
||||
$table,
|
||||
'*',
|
||||
[],
|
||||
__METHOD__,
|
||||
[ 'ORDER BY' => 1 ]
|
||||
);
|
||||
foreach ( $res as $i => $row ) {
|
||||
foreach ( $row as $key => $val ) {
|
||||
$actual[$table][$i][$key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally write updated content to the JSON files
|
||||
if ( getenv( 'DISCUSSIONTOOLS_OVERWRITE_TESTS' ) ) {
|
||||
foreach ( $tables as $table ) {
|
||||
static::overwriteJsonFile( "../$dir/$table.json", $actual[$table] );
|
||||
}
|
||||
}
|
||||
|
||||
static::assertEquals( $expected, $actual );
|
||||
}
|
||||
|
||||
public function provideInsertCases(): array {
|
||||
return [
|
||||
[ 'cases/ThreadItemStore/1simple-example' ],
|
||||
[ 'cases/ThreadItemStore/2archived-section' ],
|
||||
[ 'cases/ThreadItemStore/3indistinguishable-comments' ],
|
||||
[ 'cases/ThreadItemStore/4transcluded-section' ],
|
||||
[ 'cases/ThreadItemStore/5changed-comment-indentation' ],
|
||||
[ 'cases/ThreadItemStore/6changed-heading-level' ],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue