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 * @since 1.27
21 * @ingroup RecentChanges
22 * @author Kai Nissen
23 * @author Addshore
24 */
25class 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 */
231class_alias( CategoryMembershipChange::class, 'CategoryMembershipChange' );