Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.33% |
56 / 60 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
| CategoryMembershipChange | |
94.92% |
56 / 59 |
|
62.50% |
5 / 8 |
14.03 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| checkTemplateLinks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| triggerCategoryAddedNotification | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| triggerCategoryRemovedNotification | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| createRecentChangesEntry | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| notifyCategorization | |
96.30% |
26 / 27 |
|
0.00% |
0 / 1 |
5 | |||
| getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getChangeMessageText | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\RecentChanges; |
| 8 | |
| 9 | use MediaWiki\Cache\BacklinkCache; |
| 10 | use MediaWiki\MediaWikiServices; |
| 11 | use MediaWiki\Page\PageIdentity; |
| 12 | use MediaWiki\Revision\RevisionRecord; |
| 13 | use MediaWiki\Title\Title; |
| 14 | use MediaWiki\User\UserIdentity; |
| 15 | use Wikimedia\Rdbms\IDBAccessObject; |
| 16 | |
| 17 | /** |
| 18 | * Helper class for category membership changes |
| 19 | * |
| 20 | * @since 1.27 |
| 21 | * @ingroup RecentChanges |
| 22 | * @author Kai Nissen |
| 23 | * @author Addshore |
| 24 | */ |
| 25 | class CategoryMembershipChange { |
| 26 | |
| 27 | private const CATEGORY_ADDITION = 1; |
| 28 | private const CATEGORY_REMOVAL = -1; |
| 29 | |
| 30 | /** |
| 31 | * @var string Timestamp of the revision associated with this category membership change |
| 32 | */ |
| 33 | private $timestamp; |
| 34 | |
| 35 | /** |
| 36 | * @var Title Title instance of the categorized page |
| 37 | */ |
| 38 | private $pageTitle; |
| 39 | |
| 40 | /** |
| 41 | * @var RevisionRecord Latest revision of the categorized page |
| 42 | */ |
| 43 | private RevisionRecord $revision; |
| 44 | |
| 45 | /** @var bool Whether this was caused by an import */ |
| 46 | private $forImport; |
| 47 | |
| 48 | /** |
| 49 | * @var int |
| 50 | * Number of pages this WikiPage is embedded by |
| 51 | * Set by CategoryMembershipChange::checkTemplateLinks() |
| 52 | */ |
| 53 | private $numTemplateLinks = 0; |
| 54 | |
| 55 | private BacklinkCache $backlinkCache; |
| 56 | private RecentChangeFactory $recentChangeFactory; |
| 57 | |
| 58 | /** |
| 59 | * @param Title $pageTitle Title instance of the categorized page |
| 60 | * @param BacklinkCache $backlinkCache |
| 61 | * @param RevisionRecord $revision Latest revision of the categorized page. |
| 62 | * @param RecentChangeFactory $recentChangeFactory |
| 63 | * @param bool $forImport Whether this was caused by an import |
| 64 | */ |
| 65 | public function __construct( |
| 66 | Title $pageTitle, |
| 67 | BacklinkCache $backlinkCache, |
| 68 | RevisionRecord $revision, |
| 69 | RecentChangeFactory $recentChangeFactory, |
| 70 | bool $forImport |
| 71 | ) { |
| 72 | $this->pageTitle = $pageTitle; |
| 73 | $this->revision = $revision; |
| 74 | $this->recentChangeFactory = $recentChangeFactory; |
| 75 | |
| 76 | // Use the current timestamp for creating the RC entry when dealing with imported revisions, |
| 77 | // since their timestamp may be significantly older than the current time. |
| 78 | // This ensures the resulting RC entry won't be immediately reaped by probabilistic RC purging if |
| 79 | // the imported revision is older than $wgRCMaxAge (T377392). |
| 80 | $this->timestamp = $forImport ? wfTimestampNow() : $revision->getTimestamp(); |
| 81 | |
| 82 | $this->backlinkCache = $backlinkCache; |
| 83 | $this->forImport = $forImport; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Determines the number of template links for recursive link updates |
| 88 | */ |
| 89 | public function checkTemplateLinks() { |
| 90 | $this->numTemplateLinks = $this->backlinkCache->getNumLinks( 'templatelinks' ); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Create a recentchanges entry for category additions |
| 95 | */ |
| 96 | public function triggerCategoryAddedNotification( PageIdentity $categoryPage ) { |
| 97 | $this->createRecentChangesEntry( $categoryPage, self::CATEGORY_ADDITION ); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Create a recentchanges entry for category removals |
| 102 | */ |
| 103 | public function triggerCategoryRemovedNotification( PageIdentity $categoryPage ) { |
| 104 | $this->createRecentChangesEntry( $categoryPage, self::CATEGORY_REMOVAL ); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Create a recentchanges entry using RecentChange::notifyCategorization() |
| 109 | * |
| 110 | * @param PageIdentity $categoryPage |
| 111 | * @param int $type |
| 112 | */ |
| 113 | private function createRecentChangesEntry( PageIdentity $categoryPage, $type ) { |
| 114 | $this->notifyCategorization( |
| 115 | $this->timestamp, |
| 116 | $categoryPage, |
| 117 | $this->getUser(), |
| 118 | $this->getChangeMessageText( |
| 119 | $type, |
| 120 | $this->pageTitle->getPrefixedText(), |
| 121 | $this->numTemplateLinks |
| 122 | ), |
| 123 | $this->pageTitle, |
| 124 | $this->revision, |
| 125 | $this->forImport, |
| 126 | $type === self::CATEGORY_ADDITION |
| 127 | ); |
| 128 | } |
| 129 | |
| 130 | /** |
| 131 | * @param string $timestamp Timestamp of the recent change to occur in TS_MW format |
| 132 | * @param PageIdentity $categoryPage Page of the category a page is being added to or removed from |
| 133 | * @param UserIdentity|null $user User object of the user that made the change |
| 134 | * @param string $comment Change summary |
| 135 | * @param PageIdentity $page Page that is being added or removed |
| 136 | * @param RevisionRecord $revision |
| 137 | * @param bool $forImport Whether the associated revision was imported |
| 138 | * @param bool $added true, if the category was added, false for removed |
| 139 | */ |
| 140 | private function notifyCategorization( |
| 141 | $timestamp, |
| 142 | PageIdentity $categoryPage, |
| 143 | ?UserIdentity $user, |
| 144 | $comment, |
| 145 | PageIdentity $page, |
| 146 | RevisionRecord $revision, |
| 147 | bool $forImport, |
| 148 | $added |
| 149 | ) { |
| 150 | $deleted = $revision->getVisibility() & RevisionRecord::SUPPRESSED_USER; |
| 151 | $newRevId = $revision->getId(); |
| 152 | |
| 153 | /** |
| 154 | * T109700 - Default bot flag to true when there is no corresponding RC entry |
| 155 | * This means all changes caused by parser functions & Lua on reparse are marked as bot |
| 156 | * Also in the case no RC entry could be found due to replica DB lag |
| 157 | */ |
| 158 | $bot = 1; |
| 159 | $lastRevId = 0; |
| 160 | $ip = ''; |
| 161 | |
| 162 | $revisionStore = MediaWikiServices::getInstance()->getRevisionStore(); |
| 163 | $correspondingRc = $revisionStore->getRecentChange( $revision ) ?? |
| 164 | $revisionStore->getRecentChange( $revision, IDBAccessObject::READ_LATEST ); |
| 165 | if ( $correspondingRc !== null ) { |
| 166 | $bot = $correspondingRc->getAttribute( 'rc_bot' ) ?: 0; |
| 167 | $ip = $correspondingRc->getAttribute( 'rc_ip' ) ?: ''; |
| 168 | $lastRevId = $correspondingRc->getAttribute( 'rc_last_oldid' ) ?: 0; |
| 169 | } |
| 170 | |
| 171 | $rc = $this->recentChangeFactory->createCategorizationRecentChange( |
| 172 | $timestamp, |
| 173 | $categoryPage, |
| 174 | $user, |
| 175 | $comment, |
| 176 | $page, |
| 177 | $lastRevId, |
| 178 | $newRevId, |
| 179 | $bot, |
| 180 | $ip, |
| 181 | $deleted, |
| 182 | $added, |
| 183 | $forImport |
| 184 | ); |
| 185 | $this->recentChangeFactory->insertRecentChange( $rc ); |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Get the user associated with this change, or `null` if there is no valid author |
| 190 | * associated with this change. |
| 191 | * |
| 192 | * @return UserIdentity|null |
| 193 | */ |
| 194 | private function getUser(): ?UserIdentity { |
| 195 | return $this->revision->getUser( RevisionRecord::RAW ); |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * Returns the change message according to the type of category membership change |
| 200 | * |
| 201 | * The message keys created in this method may be one of: |
| 202 | * - recentchanges-page-added-to-category |
| 203 | * - recentchanges-page-added-to-category-bundled |
| 204 | * - recentchanges-page-removed-from-category |
| 205 | * - recentchanges-page-removed-from-category-bundled |
| 206 | * |
| 207 | * @param int $type may be CategoryMembershipChange::CATEGORY_ADDITION |
| 208 | * or CategoryMembershipChange::CATEGORY_REMOVAL |
| 209 | * @param string $prefixedText result of Title::->getPrefixedText() |
| 210 | * @param int $numTemplateLinks |
| 211 | * |
| 212 | * @return string |
| 213 | */ |
| 214 | private function getChangeMessageText( $type, $prefixedText, $numTemplateLinks ) { |
| 215 | $array = [ |
| 216 | self::CATEGORY_ADDITION => 'recentchanges-page-added-to-category', |
| 217 | self::CATEGORY_REMOVAL => 'recentchanges-page-removed-from-category', |
| 218 | ]; |
| 219 | |
| 220 | $msgKey = $array[$type]; |
| 221 | |
| 222 | if ( intval( $numTemplateLinks ) > 0 ) { |
| 223 | $msgKey .= '-bundled'; |
| 224 | } |
| 225 | |
| 226 | return wfMessage( $msgKey, $prefixedText )->inContentLanguage()->text(); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | /** @deprecated class alias since 1.44 */ |
| 231 | class_alias( CategoryMembershipChange::class, 'CategoryMembershipChange' ); |