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