Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.04% covered (success)
91.04%
61 / 67
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PatrolManager
91.04% covered (success)
91.04%
61 / 67
50.00% covered (danger)
50.00%
2 / 4
20.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 markPatrolled
86.21% covered (warning)
86.21%
25 / 29
0.00% covered (danger)
0.00%
0 / 1
15.59
 reallyMarkPatrolled
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
 createPatrolLog
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\RecentChanges;
8
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\HookContainer\HookContainer;
11use MediaWiki\HookContainer\HookRunner;
12use MediaWiki\Logging\ManualLogEntry;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Page\PageReferenceValue;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\Permissions\PermissionStatus;
17use MediaWiki\Storage\RevertedTagUpdateManager;
18use MediaWiki\User\UserFactory;
19use MediaWiki\User\UserIdentity;
20use Wikimedia\Rdbms\IConnectionProvider;
21
22/**
23 * @since 1.45
24 * @ingroup RecentChanges
25 */
26class PatrolManager {
27
28    public const PRC_UNPATROLLED = 0;
29    public const PRC_PATROLLED = 1;
30    public const PRC_AUTOPATROLLED = 2;
31
32    /**
33     * @internal For use by ServiceWiring only
34     */
35    public const CONSTRUCTOR_OPTIONS = [
36        MainConfigNames::UseRCPatrol,
37        MainConfigNames::UseNPPatrol,
38        MainConfigNames::UseFilePatrol,
39    ];
40
41    private IConnectionProvider $connectionProvider;
42    private UserFactory $userFactory;
43    private HookContainer $hookContainer;
44    private RevertedTagUpdateManager $revertedTagUpdateManager;
45
46    private bool $useRCPatrol;
47    private bool $useNPPatrol;
48    private bool $useFilePatrol;
49
50    public function __construct(
51        ServiceOptions $options,
52        IConnectionProvider $connectionProvider,
53        UserFactory $userFactory,
54        HookContainer $hookContainer,
55        RevertedTagUpdateManager $revertedTagUpdateManager
56    ) {
57        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
58
59        $this->connectionProvider = $connectionProvider;
60        $this->userFactory = $userFactory;
61        $this->hookContainer = $hookContainer;
62        $this->revertedTagUpdateManager = $revertedTagUpdateManager;
63
64        $this->useRCPatrol = $options->get( MainConfigNames::UseRCPatrol );
65        $this->useNPPatrol = $options->get( MainConfigNames::UseNPPatrol );
66        $this->useFilePatrol = $options->get( MainConfigNames::UseFilePatrol );
67    }
68
69    /**
70     * Mark this RecentChange as patrolled
71     *
72     * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and
73     * 'markedaspatrollederror-noautopatrol' as errors
74     *
75     * @param RecentChange $recentChange
76     * @param Authority $performer User performing the action
77     * @param string|string[]|null $tags Change tags to add to the patrol log entry
78     *   ($performer should be able to add the specified tags before this is called)
79     * @return PermissionStatus
80     */
81    public function markPatrolled(
82        RecentChange $recentChange,
83        Authority $performer,
84        $tags = null
85    ): PermissionStatus {
86        // Fix up $tags so that the MarkPatrolled hook below always gets an array
87        if ( $tags === null ) {
88            $tags = [];
89        } elseif ( is_string( $tags ) ) {
90            $tags = [ $tags ];
91        }
92
93        // If recentchanges patrol is disabled, only new pages or new file versions
94        // can be patrolled, provided the appropriate config variable is set
95        if ( !$this->useRCPatrol &&
96            ( !$this->useNPPatrol || $recentChange->getAttribute( 'rc_source' ) != RecentChange::SRC_NEW ) &&
97            ( !$this->useFilePatrol || !( $recentChange->getAttribute( 'rc_source' ) == RecentChange::SRC_LOG &&
98                $recentChange->getAttribute( 'rc_log_type' ) == 'upload' ) )
99        ) {
100            return PermissionStatus::newFatal( 'rcpatroldisabled' );
101        }
102
103        // Users without the 'autopatrol' right can't patrol their own revisions
104        if ( $performer->getUser()->equals( $recentChange->getPerformerIdentity() )
105            && !$performer->isAllowed( 'autopatrol' )
106        ) {
107            return PermissionStatus::newFatal( 'markedaspatrollederror-noautopatrol' );
108        }
109
110        $status = PermissionStatus::newEmpty();
111        $performer->authorizeWrite( 'patrol', $recentChange->getTitle(), $status );
112        if ( !$status->isGood() ) {
113            return $status;
114        }
115
116        $user = $this->userFactory->newFromAuthority( $performer );
117        $hookRunner = new HookRunner( $this->hookContainer );
118
119        if ( !$hookRunner->onMarkPatrolled( $recentChange->getAttribute( 'rc_id' ), $user, false, false, $tags ) ) {
120            return PermissionStatus::newFatal( 'hookaborted' );
121        }
122
123        // If the change was patrolled already, do nothing
124        if ( $recentChange->getAttribute( 'rc_patrolled' ) ) {
125            return $status;
126        }
127
128        // Attempt to set the 'patrolled' flag in RC database
129        $affectedRowCount = $this->reallyMarkPatrolled( $recentChange );
130
131        if ( $affectedRowCount === 0 ) {
132            // Query succeeded but no rows change, e.g. another request
133            // patrolled the same change just before us.
134            // Avoid duplicate log entry (T196182).
135            return $status;
136        }
137
138        // Log this patrol event
139        $logId = $this->createPatrolLog( $recentChange, $user, $tags );
140
141        $hookRunner->onMarkPatrolledComplete( $recentChange->getAttribute( 'rc_id' ), $user, false, false );
142        $hookRunner->onMarkPatrolledAudit( $recentChange, $user, $logId );
143
144        return $status;
145    }
146
147    /**
148     * Mark this RecentChange patrolled, without error checking
149     *
150     * @param RecentChange $recentChange
151     * @return int Number of database rows changed, usually 1, but 0 if
152     * another request already patrolled it in the mean time.
153     */
154    public function reallyMarkPatrolled( RecentChange $recentChange ): int {
155        $dbw = $this->connectionProvider->getPrimaryDatabase();
156        $dbw->newUpdateQueryBuilder()
157            ->update( 'recentchanges' )
158            ->set( [ 'rc_patrolled' => self::PRC_PATROLLED ] )
159            ->where( [
160                'rc_id' => $recentChange->getAttribute( 'rc_id' ),
161                'rc_patrolled' => self::PRC_UNPATROLLED,
162            ] )
163            ->caller( __METHOD__ )->execute();
164
165        $affectedRowCount = $dbw->affectedRows();
166
167        // The change was patrolled already, do nothing
168        if ( $affectedRowCount === 0 ) {
169            return 0;
170        }
171
172        // Invalidate the page cache after the page has been patrolled
173        // to make sure that the Patrol link isn't visible any longer!
174        $recentChange->getTitle()->invalidateCache();
175
176        // Enqueue a reverted tag update (in case the edit was a revert)
177        $revisionId = $recentChange->getAttribute( 'rc_this_oldid' );
178        if ( $revisionId ) {
179            $this->revertedTagUpdateManager->approveRevertedTagForRevision( $revisionId );
180        }
181
182        return $affectedRowCount;
183    }
184
185    /**
186     * Create a logging entry for a change being patrolled.
187     *
188     * @since 1.47
189     *
190     * @param RecentChange $recentChange RecentChange object for the entry that was patrolled
191     * @param UserIdentity $user User performing the patrol action
192     * @param string|string[]|null $tags Change tags to add to the patrol log entry
193     *    ($user should be able to add the specified tags before this is called)
194     */
195    public function createPatrolLog(
196        RecentChange $recentChange,
197        UserIdentity $user,
198        $tags = null
199    ): int {
200        $entry = new ManualLogEntry( 'patrol', 'patrol' );
201
202        $page = $recentChange->getPage() ?? PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle' );
203        $entry->setTarget( $page );
204        $entry->setParameters( [
205            '4::curid' => $recentChange->getAttribute( 'rc_this_oldid' ),
206            '5::previd' => $recentChange->getAttribute( 'rc_last_oldid' ),
207            '6::auto' => 0,
208        ] );
209        $entry->setPerformer( $user );
210        $entry->addTags( $tags );
211
212        $logId = $entry->insert();
213        $entry->publish( $logId, 'udp' );
214
215        return $logId;
216    }
217}