2021-02-09 21:51:09 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* DiscussionTools event dispatcher
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace MediaWiki\Extension\DiscussionTools\Notifications;
|
|
|
|
|
|
|
|
use EchoEvent;
|
|
|
|
use Error;
|
2021-06-02 19:12:16 +00:00
|
|
|
use IDBAccessObject;
|
2021-02-09 21:51:09 +00:00
|
|
|
use Iterator;
|
|
|
|
use MediaWiki\Extension\DiscussionTools\CommentParser;
|
2021-06-25 13:24:46 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\HeadingItem;
|
2021-04-08 12:12:07 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\Extension\DiscussionTools\SubscriptionItem;
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2021-07-15 13:56:13 +00:00
|
|
|
use MediaWiki\Page\PageIdentity;
|
2021-02-09 21:51:09 +00:00
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
|
|
use MediaWiki\User\UserIdentity;
|
2021-06-30 20:24:29 +00:00
|
|
|
use ParserOptions;
|
2021-04-08 12:12:07 +00:00
|
|
|
use Title;
|
2021-06-30 20:24:29 +00:00
|
|
|
use Wikimedia\Assert\Assert;
|
2021-07-29 02:16:15 +00:00
|
|
|
use Wikimedia\Parsoid\DOM\Element;
|
2021-02-09 21:51:09 +00:00
|
|
|
use Wikimedia\Parsoid\Utils\DOMUtils;
|
|
|
|
|
|
|
|
class EventDispatcher {
|
|
|
|
/**
|
2021-06-30 20:24:29 +00:00
|
|
|
* @param RevisionRecord $revRecord
|
2021-02-09 21:51:09 +00:00
|
|
|
* @return CommentParser
|
|
|
|
*/
|
2021-07-22 07:25:13 +00:00
|
|
|
private static function getParsedRevision( RevisionRecord $revRecord ): CommentParser {
|
2021-06-30 20:24:29 +00:00
|
|
|
$services = MediaWikiServices::getInstance();
|
|
|
|
|
|
|
|
$pageRecord = $services->getPageStore()->getPageByReference( $revRecord->getPage() );
|
|
|
|
Assert::postcondition( $pageRecord !== null, 'Revision had no page' );
|
|
|
|
|
|
|
|
// If the $revRecord was fetched from the primary database, this will also fetch the content
|
|
|
|
// from the primary database (using the same query flags)
|
|
|
|
$status = $services->getParserOutputAccess()->getParserOutput(
|
|
|
|
$pageRecord,
|
|
|
|
ParserOptions::newCanonical( 'canonical' ),
|
|
|
|
$revRecord
|
2021-02-09 21:51:09 +00:00
|
|
|
);
|
2021-06-30 20:24:29 +00:00
|
|
|
if ( !$status->isOK() ) {
|
|
|
|
throw new Error( 'Could not load revision for notifications' );
|
|
|
|
}
|
2021-02-09 21:51:09 +00:00
|
|
|
|
2021-06-30 20:24:29 +00:00
|
|
|
$parserOutput = $status->getValue();
|
|
|
|
$html = $parserOutput->getText();
|
2021-02-09 21:51:09 +00:00
|
|
|
|
2021-06-30 20:24:29 +00:00
|
|
|
$doc = DOMUtils::parseHTML( $html );
|
2021-02-09 21:51:09 +00:00
|
|
|
$container = $doc->getElementsByTagName( 'body' )->item( 0 );
|
2021-07-29 02:16:15 +00:00
|
|
|
if ( !( $container instanceof Element ) ) {
|
2021-02-09 21:51:09 +00:00
|
|
|
throw new Error( 'Could not load revision for notifications' );
|
|
|
|
}
|
|
|
|
return CommentParser::newFromGlobalState( $container );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array &$events
|
|
|
|
* @param RevisionRecord $newRevRecord
|
|
|
|
*/
|
|
|
|
public static function generateEventsForRevision( array &$events, RevisionRecord $newRevRecord ) {
|
|
|
|
$services = MediaWikiServices::getInstance();
|
2021-04-08 12:16:54 +00:00
|
|
|
$dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
|
|
|
|
|
2021-06-07 20:45:54 +00:00
|
|
|
if ( !$dtConfig->get( 'DiscussionToolsEnableTopicSubscriptionBackend' ) ) {
|
2021-04-08 12:16:54 +00:00
|
|
|
// Feature disabled for all users
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-02-09 21:51:09 +00:00
|
|
|
$revisionStore = $services->getRevisionStore();
|
|
|
|
$userFactory = $services->getUserFactory();
|
2021-06-02 19:12:16 +00:00
|
|
|
$oldRevRecord = $revisionStore->getPreviousRevision( $newRevRecord, IDBAccessObject::READ_LATEST );
|
2021-02-09 21:51:09 +00:00
|
|
|
|
|
|
|
if ( $oldRevRecord === null ) {
|
|
|
|
// TODO: Handle page creation (oldRevRecord = null?)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-08 12:12:07 +00:00
|
|
|
$title = Title::newFromLinkTarget(
|
|
|
|
$newRevRecord->getPageAsLinkTarget()
|
|
|
|
);
|
|
|
|
if ( !HookUtils::isAvailableForTitle( $title ) ) {
|
|
|
|
// Not a talk page
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-21 12:30:12 +00:00
|
|
|
$user = $newRevRecord->getUser();
|
|
|
|
if ( !$user ) {
|
|
|
|
// User can be null if the user is deleted, but this is unlikely
|
|
|
|
// to be the case if the user just made an edit
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-02-09 21:51:09 +00:00
|
|
|
$oldParser = self::getParsedRevision( $oldRevRecord );
|
|
|
|
$newParser = self::getParsedRevision( $newRevRecord );
|
|
|
|
|
2021-07-15 13:56:13 +00:00
|
|
|
self::generateEventsFromParsers( $events, $oldParser, $newParser, $newRevRecord, $title, $user );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper for generateEventsForRevision(), separated out for easier testing.
|
|
|
|
*
|
|
|
|
* @param array &$events
|
|
|
|
* @param CommentParser $oldParser
|
|
|
|
* @param CommentParser $newParser
|
|
|
|
* @param RevisionRecord $newRevRecord
|
|
|
|
* @param PageIdentity $title
|
|
|
|
* @param UserIdentity $user
|
|
|
|
*/
|
|
|
|
protected static function generateEventsFromParsers(
|
|
|
|
array &$events,
|
|
|
|
CommentParser $oldParser,
|
|
|
|
CommentParser $newParser,
|
|
|
|
RevisionRecord $newRevRecord,
|
|
|
|
PageIdentity $title,
|
|
|
|
UserIdentity $user
|
|
|
|
) {
|
2021-02-09 21:51:09 +00:00
|
|
|
$newComments = [];
|
|
|
|
foreach ( $newParser->getCommentItems() as $newComment ) {
|
2021-04-21 12:30:12 +00:00
|
|
|
if (
|
|
|
|
$newComment->getAuthor() === $user->getName() &&
|
2021-04-21 12:34:07 +00:00
|
|
|
// Compare comments by name, as ID could be changed by a parent comment
|
|
|
|
// being moved/deleted. The downside is that multiple replies within the
|
|
|
|
// same minute will only fire one notification.
|
|
|
|
count( $oldParser->findCommentsByName( $newComment->getName() ) ) === 0
|
2021-04-21 12:30:12 +00:00
|
|
|
) {
|
2021-02-09 21:51:09 +00:00
|
|
|
$newComments[] = $newComment;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$mentionedUsers = [];
|
|
|
|
foreach ( $events as $event ) {
|
|
|
|
if ( $event['type'] === 'mention' || $event['type'] === 'mention-summary' ) {
|
|
|
|
// Array is keyed by user id so we can do a simple array merge
|
|
|
|
$mentionedUsers += $event['extra']['mentioned-users'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ( $newComments as $newComment ) {
|
|
|
|
$heading = $newComment->getHeading();
|
2021-06-25 13:24:46 +00:00
|
|
|
// Find a level 2 heading, because the interface doesn't allow subscribing to other headings.
|
|
|
|
// (T286736)
|
|
|
|
while ( $heading instanceof HeadingItem && $heading->getHeadingLevel() !== 2 ) {
|
|
|
|
$heading = $heading->getParent();
|
|
|
|
}
|
|
|
|
if ( !( $heading instanceof HeadingItem ) ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Check if the name corresponds to a section that contain no comments (only sub-sections).
|
|
|
|
// The interface doesn't allow subscribing to them either, because they can't be distinguished
|
|
|
|
// from each other. (T285796)
|
|
|
|
if ( $heading->getName() === 'h-' ) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-02-09 21:51:09 +00:00
|
|
|
$events[] = [
|
|
|
|
'type' => 'dt-subscribed-new-comment',
|
2021-04-08 12:12:07 +00:00
|
|
|
'title' => $title,
|
2021-02-09 21:51:09 +00:00
|
|
|
'extra' => [
|
|
|
|
'subscribed-comment-name' => $heading->getName(),
|
|
|
|
'comment-id' => $newComment->getId(),
|
2021-04-21 12:36:29 +00:00
|
|
|
'comment-name' => $newComment->getName(),
|
2021-02-09 21:51:09 +00:00
|
|
|
'content' => $newComment->getBodyText( true ),
|
|
|
|
'section-title' => $heading->getText(),
|
|
|
|
'revid' => $newRevRecord->getId(),
|
|
|
|
'mentioned-users' => $mentionedUsers,
|
|
|
|
],
|
2021-04-21 12:30:12 +00:00
|
|
|
'agent' => $user,
|
2021-02-09 21:51:09 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all users subscribed to a comment
|
|
|
|
*
|
|
|
|
* @param EchoEvent $event
|
|
|
|
* @param int $batchSize
|
|
|
|
* @return UserIdentity[]|Iterator<UserIdentity>
|
|
|
|
*/
|
|
|
|
public static function locateSubscribedUsers( EchoEvent $event, $batchSize = 500 ) {
|
|
|
|
$commentName = $event->getExtraParam( 'subscribed-comment-name' );
|
|
|
|
|
|
|
|
$subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' );
|
|
|
|
$subscriptionItems = $subscriptionStore->getSubscriptionItemsForTopic(
|
|
|
|
$commentName,
|
|
|
|
1
|
|
|
|
);
|
|
|
|
|
|
|
|
// Update notified timestamps
|
|
|
|
$subscriptionStore->updateSubscriptionNotifiedTimestamp(
|
|
|
|
null,
|
|
|
|
$commentName
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODD: Have this return an Iterator instead?
|
2021-05-05 06:59:38 +00:00
|
|
|
$users = array_map( static function ( SubscriptionItem $item ) {
|
2021-02-09 21:51:09 +00:00
|
|
|
return $item->getUserIdentity();
|
|
|
|
}, $subscriptionItems );
|
|
|
|
|
|
|
|
return $users;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|