*
* @file
* @author Kai Nissen
- * @since 1.26
+ * @author Adam Shorland
+ * @since 1.27
*/
+use Wikimedia\Assert\Assert;
+
class CategoryMembershipChange {
const CATEGORY_ADDITION = 1;
const CATEGORY_REMOVAL = -1;
- /** @var string Current timestamp, set during CategoryMembershipChange::__construct() */
+ /**
+ * @var string Current timestamp, set during CategoryMembershipChange::__construct()
+ */
private $timestamp;
- /** @var Title Title instance of the categorized page */
+ /**
+ * @var Title Title instance of the categorized page
+ */
private $pageTitle;
- /** @var WikiPage WikiPage instance of the categorized page */
- private $page;
-
- /** @var Revision Latest Revision instance of the categorized page */
+ /**
+ * @var Revision|null Latest Revision instance of the categorized page
+ */
private $revision;
/**
* @var int
- * Number of pages this WikiPage is embedded by; set by CategoryMembershipChange::setRecursive()
+ * Number of pages this WikiPage is embedded by
+ * Set by CategoryMembershipChange::checkTemplateLinks()
*/
private $numTemplateLinks = 0;
/**
- * @var User
- * instance of the user that created CategoryMembershipChange::$revision
- */
- private $user;
-
- /**
- * @var null|RecentChange
- * RecentChange that is referred to in CategoryMembershipChange::$revision
+ * @var callable|null
*/
- private $correspondingRC;
+ private $newForCategorizationCallback = null;
/**
* @param Title $pageTitle Title instance of the categorized page
* @param Revision $revision Latest Revision instance of the categorized page
+ *
* @throws MWException
*/
public function __construct( Title $pageTitle, Revision $revision = null ) {
$this->pageTitle = $pageTitle;
- $this->page = WikiPage::factory( $pageTitle );
$this->timestamp = wfTimestampNow();
+ $this->revision = $revision;
+ $this->newForCategorizationCallback = array( 'RecentChange', 'newForCategorization' );
+ }
- # if no revision is given, the change was probably triggered by parser functions
- if ( $revision ) {
- $this->revision = $revision;
- $this->correspondingRC = $this->revision->getRecentChange();
- $this->user = $this->getRevisionUser();
- } else {
- $this->user = User::newFromId( 0 );
+ /**
+ * Overrides the default new for categorization callback
+ * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
+ *
+ * @param callable $callback
+ * @see RecentChange::newForCategorization for callback signiture
+ *
+ * @throws MWException
+ */
+ public function overrideNewForCategorizationCallback( $callback ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException( 'Cannot override newForCategorization callback in operation.' );
}
+ Assert::parameterType( 'callable', $callback, '$callback' );
+ $this->newForCategorizationCallback = $callback;
}
/**
* Determines the number of template links for recursive link updates
*/
- public function setRecursive() {
+ public function checkTemplateLinks() {
$this->numTemplateLinks = $this->pageTitle->getBacklinkCache()->getNumLinks( 'templatelinks' );
}
/**
* Create a recentchanges entry for category additions
- * @param string $categoryName
+ *
+ * @param Title $categoryTitle
*/
- public function pageAddedToCategory( $categoryName ) {
- $this->createRecentChangesEntry( $categoryName, self::CATEGORY_ADDITION );
+ public function triggerCategoryAddedNotification( Title $categoryTitle ) {
+ $this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_ADDITION );
}
/**
* Create a recentchanges entry for category removals
- * @param string $categoryName
+ *
+ * @param Title $categoryTitle
*/
- public function pageRemovedFromCategory( $categoryName ) {
- $this->createRecentChangesEntry( $categoryName, self::CATEGORY_REMOVAL );
+ public function triggerCategoryRemovedNotification( Title $categoryTitle ) {
+ $this->createRecentChangesEntry( $categoryTitle, self::CATEGORY_REMOVAL );
}
/**
* Create a recentchanges entry using RecentChange::notifyCategorization()
- * @param string $categoryName
+ *
+ * @param Title $categoryTitle
* @param int $type
*/
- private function createRecentChangesEntry( $categoryName, $type ) {
- $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY );
- if ( !$categoryTitle ) {
- return;
- }
+ private function createRecentChangesEntry( Title $categoryTitle, $type ) {
+ $this->notifyCategorization(
+ $this->timestamp,
+ $categoryTitle,
+ $this->getUser(),
+ $this->getChangeMessageText( $type, array(
+ 'prefixedText' => $this->pageTitle->getPrefixedText(),
+ 'numTemplateLinks' => $this->numTemplateLinks
+ ) ),
+ $this->pageTitle,
+ $this->getPreviousRevisionTimestamp(),
+ $this->revision
+ );
+ }
- $previousRevTimestamp = $this->getPreviousRevisionTimestamp();
- $unpatrolled = $this->revision ? $this->revision->isUnpatrolled() : 0;
+ /**
+ * @param string $timestamp Timestamp of the recent change to occur in TS_MW format
+ * @param Title $categoryTitle Title of the category a page is being added to or removed from
+ * @param User $user User object of the user that made the change
+ * @param string $comment Change summary
+ * @param Title $pageTitle Title of the page that is being added or removed
+ * @param string $lastTimestamp Parent revision timestamp of this change in TS_MW format
+ * @param Revision|null $revision
+ *
+ * @throws MWException
+ */
+ private function notifyCategorization(
+ $timestamp,
+ Title $categoryTitle,
+ User $user = null,
+ $comment,
+ Title $pageTitle,
+ $lastTimestamp,
+ $revision
+ ) {
+ $deleted = $revision ? $revision->getVisibility() & Revision::SUPPRESSED_USER : 0;
+ $newRevId = $revision ? $revision->getId() : 0;
+ /**
+ * T109700 - Default bot flag to true when there is no corresponding RC entry
+ * This means all changes caused by parser functions & Lua on reparse are marked as bot
+ * Also in the case no RC entry could be found due to slave lag
+ */
+ $bot = 1;
$lastRevId = 0;
- $bot = 0;
$ip = '';
- if ( $this->correspondingRC ) {
- $lastRevId = $this->correspondingRC->getAttribute( 'rc_last_oldid' ) ?: 0;
- $bot = $this->correspondingRC->getAttribute( 'rc_bot' ) ?: 0;
- $ip = $this->correspondingRC->getAttribute( 'rc_ip' ) ?: '';
+
+ # If no revision is given, the change was probably triggered by parser functions
+ if ( $revision !== null ) {
+ $correspondingRc = $this->revision->getRecentChange();
+ if ( $correspondingRc === null ) {
+ $correspondingRc = $this->revision->getRecentChange( Revision::READ_LATEST );
+ }
+ if ( $correspondingRc !== null ) {
+ $bot = $correspondingRc->getAttribute( 'rc_bot' ) ?: 0;
+ $ip = $correspondingRc->getAttribute( 'rc_ip' ) ?: '';
+ $lastRevId = $correspondingRc->getAttribute( 'rc_last_oldid' ) ?: 0;
+ }
}
- RecentChange::notifyCategorization(
- $this->timestamp,
- $categoryTitle,
- $this->user,
- $this->getChangeMessage( $type, array(
- 'prefixedUrl' => $this->page->getTitle()->getPrefixedURL(),
- 'numTemplateLinks' => $this->numTemplateLinks
- ) ),
- $this->pageTitle,
- $lastRevId,
- $this->revision ? $this->revision->getId() : 0,
- $previousRevTimestamp,
- $bot,
- $ip,
- $unpatrolled ? 0 : 1,
- $this->revision ? $this->revision->getVisibility() & Revision::SUPPRESSED_USER : 0
+ $rc = call_user_func_array(
+ $this->newForCategorizationCallback,
+ array(
+ $timestamp,
+ $categoryTitle,
+ $user,
+ $comment,
+ $pageTitle,
+ $lastRevId,
+ $newRevId,
+ $lastTimestamp,
+ $bot,
+ $ip,
+ $deleted
+ )
);
+ $rc->save();
}
/**
- * Get the user who created the revision. may be an anonymous IP
- * @return User
+ * Get the user associated with this change.
+ *
+ * If there is no revision associated with the change and thus no editing user
+ * fallback to a default.
+ *
+ * False will be returned if the user name specified in the
+ * 'autochange-username' message is invalid.
+ *
+ * @return User|bool
*/
- private function getRevisionUser() {
- $userId = $this->revision->getUser( Revision::RAW );
- if ( $userId === 0 ) {
- return User::newFromName( $this->revision->getUserText( Revision::RAW ), false );
- } else {
- return User::newFromId( $userId );
+ private function getUser() {
+ if ( $this->revision ) {
+ $userId = $this->revision->getUser( Revision::RAW );
+ if ( $userId === 0 ) {
+ return User::newFromName( $this->revision->getUserText( Revision::RAW ), false );
+ } else {
+ return User::newFromId( $userId );
+ }
+ }
+
+ $username = wfMessage( 'autochange-username' )->inContentLanguage()->text();
+ $user = User::newFromName( $username );
+ # User::newFromName() can return false on a badly configured wiki.
+ if ( $user && !$user->isLoggedIn() ) {
+ $user->addToDatabase();
}
+
+ return $user;
}
/**
* @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION
* or CategoryMembershipChange::CATEGORY_REMOVAL
* @param array $params
- * - prefixedUrl: result of Title::->getPrefixedURL()
- * - numTemplateLinks
+ * - prefixedText: result of Title::->getPrefixedText()
+ *
* @return string
*/
- private function getChangeMessage( $type, array $params ) {
- $msgKey = 'recentchanges-';
-
- switch ( $type ) {
- case self::CATEGORY_ADDITION:
- $msgKey .= 'page-added-to-category';
- break;
- case self::CATEGORY_REMOVAL:
- $msgKey .= 'page-removed-from-category';
- break;
- }
+ private function getChangeMessageText( $type, array $params ) {
+ $array = array(
+ self::CATEGORY_ADDITION => 'recentchanges-page-added-to-category',
+ self::CATEGORY_REMOVAL => 'recentchanges-page-removed-from-category',
+ );
+
+ $msgKey = $array[$type];
if ( intval( $params['numTemplateLinks'] ) > 0 ) {
$msgKey .= '-bundled';
/**
* Returns the timestamp of the page's previous revision or null if the latest revision
* does not refer to a parent revision
+ *
* @return null|string
*/
private function getPreviousRevisionTimestamp() {
- $latestRev = Revision::newFromId( $this->pageTitle->getLatestRevID() );
- $previousRev = Revision::newFromId( $latestRev->getParentId() );
+ $previousRev = Revision::newFromId(
+ $this->pageTitle->getPreviousRevisionID( $this->pageTitle->getLatestRevID() )
+ );
return $previousRev ? $previousRev->getTimestamp() : null;
}