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