Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
41.32% |
50 / 121 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
RevisionCheck | |
41.32% |
50 / 121 |
|
0.00% |
0 / 5 |
225.15 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
2 | |||
getUndoContent | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
revertPreCheck | |
74.47% |
35 / 47 |
|
0.00% |
0 / 1 |
23.39 | |||
doRevert | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
maybeRevert | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
4.00 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
16 | * |
17 | * @file |
18 | */ |
19 | |
20 | namespace AutoModerator; |
21 | |
22 | use Content; |
23 | use ContentHandler; |
24 | use Language; |
25 | use MediaWiki\CommentStore\CommentStoreComment; |
26 | use MediaWiki\Config\Config; |
27 | use MediaWiki\Page\WikiPageFactory; |
28 | use MediaWiki\Permissions\RestrictionStore; |
29 | use MediaWiki\Revision\RevisionRecord; |
30 | use MediaWiki\Revision\RevisionStore; |
31 | use MediaWiki\Revision\SlotRecord; |
32 | use MediaWiki\Storage\PageUpdater; |
33 | use MediaWiki\StubObject\StubUserLang; |
34 | use MediaWiki\User\ExternalUserNames; |
35 | use MediaWiki\User\User; |
36 | use MediaWiki\User\UserGroupManager; |
37 | use MediaWiki\User\UserIdentity; |
38 | use Psr\Log\LoggerInterface; |
39 | use WikiPage; |
40 | |
41 | class RevisionCheck { |
42 | |
43 | /** @var int */ |
44 | private $wikiPageId; |
45 | |
46 | /** @var WikiPageFactory */ |
47 | private $wikiPageFactory; |
48 | |
49 | /** @var int */ |
50 | private $revId; |
51 | |
52 | /** @var int|false */ |
53 | private $originalRevId; |
54 | |
55 | /** @var UserIdentity */ |
56 | private $user; |
57 | |
58 | /** @var string[] */ |
59 | private $tags; |
60 | |
61 | /** @var User */ |
62 | private $autoModeratorUser; |
63 | |
64 | /** @var RevisionStore */ |
65 | private $revisionStore; |
66 | |
67 | /** @var Config */ |
68 | private $config; |
69 | |
70 | /** @var Config */ |
71 | private $wikiConfig; |
72 | |
73 | /** @var string */ |
74 | public string $undoSummary; |
75 | |
76 | /** @var ContentHandler */ |
77 | private $contentHandler; |
78 | |
79 | /** @var LoggerInterface */ |
80 | private $logger; |
81 | |
82 | /** @var UserGroupManager */ |
83 | private $userGroupManager; |
84 | |
85 | /** @var RestrictionStore */ |
86 | private $restrictionStore; |
87 | |
88 | /** @var bool */ |
89 | private bool $enforce; |
90 | |
91 | /** @var Language|StubUserLang|string */ |
92 | private $lang; |
93 | |
94 | /** @var bool */ |
95 | public bool $passedPreCheck; |
96 | |
97 | /** |
98 | * @param int $wikiPageId WikiPage ID of |
99 | * @param WikiPageFactory $wikiPageFactory |
100 | * @param int $revId New revision ID |
101 | * @param int|false $originalRevId If the edit restores or repeats an earlier revision (such as a |
102 | * rollback or a null revision), the ID of that earlier revision. False otherwise. |
103 | * (Used to be called $baseID.) |
104 | * @param UserIdentity $user Editing user |
105 | * @param string[] &$tags Tags applied to the revison. |
106 | * @param User $autoModeratorUser reverting user |
107 | * @param RevisionStore $revisionStore |
108 | * @param Config $config |
109 | * @param Config $wikiConfig |
110 | * @param ContentHandler $contentHandler |
111 | * @param LoggerInterface $logger |
112 | * @param UserGroupManager $userGroupManager |
113 | * @param RestrictionStore $restrictionStore |
114 | * @param Language|StubUserLang|string $lang |
115 | * @param string $undoSummary |
116 | * @param bool $enforce Perform reverts if true, take no action if false |
117 | */ |
118 | public function __construct( |
119 | int $wikiPageId, |
120 | WikiPageFactory $wikiPageFactory, |
121 | int $revId, |
122 | $originalRevId, |
123 | UserIdentity $user, |
124 | array &$tags, |
125 | User $autoModeratorUser, |
126 | RevisionStore $revisionStore, |
127 | Config $config, |
128 | $wikiConfig, |
129 | ContentHandler $contentHandler, |
130 | LoggerInterface $logger, |
131 | UserGroupManager $userGroupManager, |
132 | RestrictionStore $restrictionStore, |
133 | $lang, |
134 | string $undoSummary, |
135 | bool $enforce = false |
136 | ) { |
137 | $this->wikiPageId = $wikiPageId; |
138 | $this->wikiPageFactory = $wikiPageFactory; |
139 | $this->revId = $revId; |
140 | $this->originalRevId = $originalRevId; |
141 | $this->user = $user; |
142 | $this->tags = $tags; |
143 | $this->autoModeratorUser = $autoModeratorUser; |
144 | $this->revisionStore = $revisionStore; |
145 | $this->config = $config; |
146 | $this->wikiConfig = $wikiConfig; |
147 | $this->contentHandler = $contentHandler; |
148 | $this->logger = $logger; |
149 | $this->userGroupManager = $userGroupManager; |
150 | $this->restrictionStore = $restrictionStore; |
151 | $this->enforce = $enforce; |
152 | $this->lang = $lang; |
153 | $this->passedPreCheck = $this->revertPreCheck( |
154 | $user, |
155 | $autoModeratorUser, |
156 | $logger, |
157 | $revisionStore, |
158 | $tags, |
159 | $restrictionStore, |
160 | $wikiPageFactory, |
161 | $userGroupManager, |
162 | $wikiConfig, |
163 | $revId, |
164 | $wikiPageId |
165 | ); |
166 | $this->undoSummary = $undoSummary; |
167 | } |
168 | |
169 | /** |
170 | * Cribbed from EditPage.php |
171 | * Returns the result of a three-way merge when undoing changes. |
172 | * |
173 | * @param RevisionRecord $oldRev Revision that is being restored. Corresponds to |
174 | * `undoafter` URL parameter. |
175 | * @param ?string &$error If false is returned, this will be set to "norev" |
176 | * if the revision failed to load, or "failure" if the content handler |
177 | * failed to merge the required changes. |
178 | * @param WikiPage $wikiPage |
179 | * @param RevisionRecord $rev |
180 | * |
181 | * @return false|Content |
182 | */ |
183 | private function getUndoContent( |
184 | RevisionRecord $oldRev, |
185 | &$error, |
186 | WikiPage $wikiPage, |
187 | RevisionRecord $rev |
188 | ) { |
189 | $currentContent = $wikiPage->getRevisionRecord() |
190 | ->getContent( SlotRecord::MAIN ); |
191 | $undoContent = $rev->getContent( SlotRecord::MAIN ); |
192 | $undoAfterContent = $oldRev->getContent( SlotRecord::MAIN ); |
193 | $undoIsLatest = $wikiPage->getRevisionRecord()->getId() === $this->revId; |
194 | if ( $currentContent === null |
195 | || $undoContent === null |
196 | || $undoAfterContent === null |
197 | ) { |
198 | $error = 'norev'; |
199 | return false; |
200 | } |
201 | |
202 | $content = $this->contentHandler->getUndoContent( |
203 | $currentContent, |
204 | $undoContent, |
205 | $undoAfterContent, |
206 | $undoIsLatest, |
207 | ); |
208 | if ( $content === false ) { |
209 | $error = 'failure'; |
210 | } |
211 | return $content; |
212 | } |
213 | |
214 | /** |
215 | * Precheck a revision; if any of the checks don't pass, |
216 | * a revision won't be scored |
217 | * @param UserIdentity $user |
218 | * @param User $autoModeratorUser |
219 | * @param LoggerInterface $logger |
220 | * @param RevisionStore $revisionStore |
221 | * @param string[] $tags |
222 | * @param RestrictionStore $restrictionStore |
223 | * @param WikiPageFactory $wikiPageFactory |
224 | * @param UserGroupManager $userGroupManager |
225 | * @param Config $wikiConfig |
226 | * @param int $revId |
227 | * @param int $wikiPageId |
228 | * @return bool |
229 | */ |
230 | public static function revertPreCheck( UserIdentity $user, User $autoModeratorUser, LoggerInterface $logger, |
231 | RevisionStore $revisionStore, array $tags, RestrictionStore $restrictionStore, |
232 | WikiPageFactory $wikiPageFactory, UserGroupManager $userGroupManager, Config $wikiConfig, |
233 | int $revId, int $wikiPageId ): bool { |
234 | // Skip AutoModerator edits |
235 | if ( $user->equals( $autoModeratorUser ) ) { |
236 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - AutoMod edits" ); |
237 | return false; |
238 | } |
239 | $rev = $revisionStore->getRevisionById( $revId ); |
240 | $parentId = $rev->getParentId(); |
241 | // Skip new page creations |
242 | if ( $parentId === null || $parentId === 0 ) { |
243 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - new page creation" ); |
244 | return false; |
245 | } |
246 | |
247 | // Skip reverts made to an AutoModerator bot revert or if |
248 | // the user reverts their own edit |
249 | $revertTags = [ 'mw-manual-revert', 'mw-rollback', 'mw-undo', 'mw-reverted' ]; |
250 | $parentRev = $revisionStore->getRevisionById( $parentId ); |
251 | foreach ( $revertTags as $revertTag ) { |
252 | if ( in_array( $revertTag, $tags ) ) { |
253 | if ( !$parentRev ) { |
254 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - parent revision not found" ); |
255 | return false; |
256 | } |
257 | $parentRevUser = $parentRev->getUser(); |
258 | if ( $parentRevUser === null ) { |
259 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - parent revision user is null" ); |
260 | return false; |
261 | } |
262 | if ( $parentRev->getUser()->equals( $autoModeratorUser ) ) { |
263 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - AutoModerator reverts" ); |
264 | return false; |
265 | } |
266 | if ( $parentRev->getUser()->equals( $user ) ) { |
267 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - own reverts" ); |
268 | return false; |
269 | } |
270 | } |
271 | } |
272 | |
273 | // Skip page moves |
274 | $moveTags = [ 'mw-new-redirect', 'mw-removed-redirect', 'mw-changed-redirect-target' ]; |
275 | foreach ( $moveTags as $moveTag ) { |
276 | if ( in_array( $moveTag, $tags ) ) { |
277 | return false; |
278 | } |
279 | } |
280 | // Skip sysop and bot user edits |
281 | // @todo: Move bot skip to check on recent changes rc_bot field |
282 | $skipGroups = $wikiConfig->get( 'AutoModeratorSkipUserGroups' ); |
283 | $userGroups = $userGroupManager->getUserGroupMemberships( $user ); |
284 | foreach ( $skipGroups as $skipGroup ) { |
285 | if ( array_key_exists( $skipGroup, $userGroups ) ) { |
286 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - trusted user group edits" ); |
287 | return false; |
288 | } |
289 | } |
290 | // Skip imported revisions |
291 | if ( ExternalUserNames::isExternal( $user->getName() ) ) { |
292 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - imported edits" ); |
293 | return false; |
294 | } |
295 | $wikiPage = $wikiPageFactory->newFromID( $wikiPageId ); |
296 | // Skip non-mainspace edit |
297 | if ( $wikiPage->getNamespace() !== NS_MAIN ) { |
298 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - non-mainspace edits" ); |
299 | return false; |
300 | } |
301 | // Skip protected pages that only admins can edit. |
302 | // Automoderator should be able to revert semi-protected pages, |
303 | // so we won't be skipping those on pre-check. |
304 | if ( $restrictionStore->isProtected( $wikiPage ) |
305 | && !$restrictionStore->isSemiProtected( $wikiPage ) ) { |
306 | $logger->debug( "AutoModerator skip rev" . __METHOD__ . " - protected page" ); |
307 | return false; |
308 | } |
309 | return true; |
310 | } |
311 | |
312 | /** |
313 | * Perform revert |
314 | * @param PageUpdater $pageUpdater |
315 | * @param Content $content |
316 | * @param RevisionRecord $prevRev |
317 | */ |
318 | private function doRevert( $pageUpdater, $content, $prevRev ) { |
319 | $pageUpdater->setContent( SlotRecord::MAIN, $content ); |
320 | $pageUpdater->setOriginalRevisionId( $prevRev->getId() ); |
321 | $comment = CommentStoreComment::newUnsavedComment( $this->undoSummary ); |
322 | // REVERT_UNDO 1 |
323 | // REVERT_ROLLBACK 2 |
324 | // REVERT_MANUAL 3 |
325 | $pageUpdater->markAsRevert( 1, $this->revId, $prevRev->getId() ); |
326 | if ( $this->wikiConfig->get( 'AutoModeratorUseEditFlagMinor' ) ) { |
327 | $pageUpdater->setFlags( EDIT_MINOR ); |
328 | } |
329 | if ( $this->wikiConfig->get( 'AutoModeratorEnableBotFlag' ) ) { |
330 | $pageUpdater->setFlags( EDIT_FORCE_BOT ); |
331 | } |
332 | $pageUpdater->saveRevision( $comment, EDIT_UPDATE ); |
333 | } |
334 | |
335 | /** |
336 | * Check revision; revert if it meets configured critera |
337 | * @param array $score |
338 | * |
339 | * @return array |
340 | */ |
341 | public function maybeRevert( $score ) { |
342 | $reverted = 0; |
343 | $status = 'Not reverted'; |
344 | $probability = $score[ 'output' ][ 'probabilities' ][ 'true' ]; |
345 | $wikiPage = $this->wikiPageFactory->newFromID( $this->wikiPageId ); |
346 | $rev = $this->revisionStore->getRevisionById( $this->revId ); |
347 | // Automoderator system user may perform updates |
348 | $pageUpdater = $wikiPage->newPageUpdater( $this->autoModeratorUser ); |
349 | if ( $probability > Util::getRevertThreshold( $this->config ) ) { |
350 | $prevRev = $this->revisionStore->getPreviousRevision( $rev ); |
351 | $content = $this->getUndoContent( $prevRev, $this->undoSummary, $wikiPage, $rev ); |
352 | if ( !$content ) { |
353 | return [ $reverted => $this->undoSummary ]; |
354 | } |
355 | if ( $this->enforce ) { |
356 | $this->doRevert( $pageUpdater, $content, $prevRev ); |
357 | } |
358 | $reverted = 1; |
359 | $status = 'success'; |
360 | } |
361 | return [ $reverted => $status ]; |
362 | } |
363 | } |