Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiTag
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 10
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
132
 validateLogId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 processIndividual
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
272
 mustBePosted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWriteMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * @license GPL-2.0-or-later
5 * @file
6 */
7
8namespace MediaWiki\Api;
9
10use MediaWiki\ChangeTags\ChangeTags;
11use MediaWiki\ChangeTags\ChangeTagsStore;
12use MediaWiki\RecentChanges\RecentChangeLookup;
13use MediaWiki\Revision\RevisionStore;
14use Wikimedia\ParamValidator\ParamValidator;
15use Wikimedia\Rdbms\IConnectionProvider;
16use Wikimedia\Rdbms\IReadableDatabase;
17
18/**
19 * @ingroup API
20 * @since 1.25
21 */
22class ApiTag extends ApiBase {
23
24    use ApiBlockInfoTrait;
25
26    private IReadableDatabase $dbr;
27    private RevisionStore $revisionStore;
28    private ChangeTagsStore $changeTagsStore;
29    private RecentChangeLookup $recentChangeLookup;
30
31    public function __construct(
32        ApiMain $main,
33        string $action,
34        IConnectionProvider $dbProvider,
35        RevisionStore $revisionStore,
36        ChangeTagsStore $changeTagsStore,
37        RecentChangeLookup $recentChangeLookup
38    ) {
39        parent::__construct( $main, $action );
40        $this->dbr = $dbProvider->getReplicaDatabase();
41        $this->revisionStore = $revisionStore;
42        $this->changeTagsStore = $changeTagsStore;
43        $this->recentChangeLookup = $recentChangeLookup;
44    }
45
46    public function execute() {
47        $params = $this->extractRequestParams();
48        $user = $this->getUser();
49
50        // make sure the user is allowed
51        $this->checkUserRightsAny( 'changetags' );
52
53        // Fail early if the user is sitewide blocked.
54        $block = $user->getBlock();
55        if ( $block && $block->isSitewide() ) {
56            $this->dieBlocked( $block );
57        }
58
59        // Check if user can add tags
60        if ( $params['tags'] ) {
61            $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
62            if ( !$ableToTag->isOK() ) {
63                $this->dieStatus( $ableToTag );
64            }
65        }
66
67        // validate and process each revid, rcid and logid
68        $this->requireAtLeastOneParameter( $params, 'revid', 'rcid', 'logid' );
69        $ret = [];
70        if ( $params['revid'] ) {
71            foreach ( $params['revid'] as $id ) {
72                $ret[] = $this->processIndividual( 'revid', $params, $id );
73            }
74        }
75        if ( $params['rcid'] ) {
76            foreach ( $params['rcid'] as $id ) {
77                $ret[] = $this->processIndividual( 'rcid', $params, $id );
78            }
79        }
80        if ( $params['logid'] ) {
81            foreach ( $params['logid'] as $id ) {
82                $ret[] = $this->processIndividual( 'logid', $params, $id );
83            }
84        }
85
86        ApiResult::setIndexedTagName( $ret, 'result' );
87        $this->getResult()->addValue( null, $this->getModuleName(), $ret );
88    }
89
90    protected function validateLogId( int $logid ): bool {
91        $result = $this->dbr->newSelectQueryBuilder()
92            ->select( 'log_id' )
93            ->from( 'logging' )
94            ->where( [ 'log_id' => $logid ] )
95            ->caller( __METHOD__ )->fetchField();
96        return (bool)$result;
97    }
98
99    protected function processIndividual( string $type, array $params, int $id ): array {
100        $user = $this->getUser();
101        $idResult = [ $type => $id ];
102
103        // validate the ID
104        $valid = false;
105        switch ( $type ) {
106            case 'rcid':
107                $valid = $this->recentChangeLookup->getRecentChangeById( $id );
108                // TODO: replace use of PermissionManager
109                if ( $valid && $this->getPermissionManager()->isBlockedFrom( $user, $valid->getTitle() ) ) {
110                    $idResult['status'] = 'error';
111                    // @phan-suppress-next-line PhanTypeMismatchArgument
112                    $idResult += $this->getErrorFormatter()->formatMessage( ApiMessage::create(
113                        'apierror-blocked',
114                        'blocked',
115                        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
116                        [ 'blockinfo' => $this->getBlockDetails( $user->getBlock() ) ]
117                    ) );
118                    return $idResult;
119                }
120                break;
121            case 'revid':
122                $valid = $this->revisionStore->getRevisionById( $id );
123                // TODO: replace use of PermissionManager
124                if (
125                    $valid &&
126                    $this->getPermissionManager()->isBlockedFrom( $user, $valid->getPageAsLinkTarget() )
127                ) {
128                    $idResult['status'] = 'error';
129                    // @phan-suppress-next-line PhanTypeMismatchArgument
130                    $idResult += $this->getErrorFormatter()->formatMessage( ApiMessage::create(
131                            'apierror-blocked',
132                            'blocked',
133                            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
134                            [ 'blockinfo' => $this->getBlockDetails( $user->getBlock() ) ]
135                    ) );
136                    return $idResult;
137                }
138                break;
139            case 'logid':
140                $valid = $this->validateLogId( $id );
141                break;
142        }
143
144        if ( !$valid ) {
145            $idResult['status'] = 'error';
146            // Messages: apierror-nosuchrcid apierror-nosuchrevid apierror-nosuchlogid
147            $idResult += $this->getErrorFormatter()->formatMessage( [ "apierror-nosuch$type", $id ] );
148            return $idResult;
149        }
150
151        $status = ChangeTags::updateTagsWithChecks( $params['add'],
152            $params['remove'],
153            ( $type === 'rcid' ? $id : null ),
154            ( $type === 'revid' ? $id : null ),
155            ( $type === 'logid' ? $id : null ),
156            null,
157            $params['reason'],
158            $this->getAuthority()
159        );
160
161        if ( !$status->isOK() ) {
162            if ( $status->hasMessage( 'actionthrottledtext' ) ) {
163                $idResult['status'] = 'skipped';
164            } else {
165                $idResult['status'] = 'failure';
166                $idResult['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status, 'error' );
167            }
168        } else {
169            $idResult['status'] = 'success';
170            if ( $status->value->logId === null ) {
171                $idResult['noop'] = true;
172            } else {
173                $idResult['actionlogid'] = $status->value->logId;
174                $idResult['added'] = $status->value->addedTags;
175                ApiResult::setIndexedTagName( $idResult['added'], 't' );
176                $idResult['removed'] = $status->value->removedTags;
177                ApiResult::setIndexedTagName( $idResult['removed'], 't' );
178
179                if ( $params['tags'] ) {
180                    $this->changeTagsStore->addTags( $params['tags'], null, null, $status->value->logId );
181                }
182            }
183        }
184        return $idResult;
185    }
186
187    /** @inheritDoc */
188    public function mustBePosted() {
189        return true;
190    }
191
192    /** @inheritDoc */
193    public function isWriteMode() {
194        return true;
195    }
196
197    /** @inheritDoc */
198    public function getAllowedParams() {
199        return [
200            'rcid' => [
201                ParamValidator::PARAM_TYPE => 'integer',
202                ParamValidator::PARAM_ISMULTI => true,
203            ],
204            'revid' => [
205                ParamValidator::PARAM_TYPE => 'integer',
206                ParamValidator::PARAM_ISMULTI => true,
207            ],
208            'logid' => [
209                ParamValidator::PARAM_TYPE => 'integer',
210                ParamValidator::PARAM_ISMULTI => true,
211            ],
212            'add' => [
213                ParamValidator::PARAM_TYPE => 'tags',
214                ParamValidator::PARAM_ISMULTI => true,
215            ],
216            'remove' => [
217                ParamValidator::PARAM_TYPE => 'string',
218                ParamValidator::PARAM_ISMULTI => true,
219            ],
220            'reason' => [
221                ParamValidator::PARAM_TYPE => 'string',
222                ParamValidator::PARAM_DEFAULT => '',
223            ],
224            'tags' => [
225                ParamValidator::PARAM_TYPE => 'tags',
226                ParamValidator::PARAM_ISMULTI => true,
227            ],
228        ];
229    }
230
231    /** @inheritDoc */
232    public function needsToken() {
233        return 'csrf';
234    }
235
236    /** @inheritDoc */
237    protected function getExamplesMessages() {
238        return [
239            'action=tag&revid=123&add=vandalism&token=123ABC'
240                => 'apihelp-tag-example-rev',
241            'action=tag&logid=123&remove=spam&reason=Wrongly+applied&token=123ABC'
242                => 'apihelp-tag-example-log',
243        ];
244    }
245
246    /** @inheritDoc */
247    public function getHelpUrls() {
248        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tag';
249    }
250}
251
252/** @deprecated class alias since 1.43 */
253class_alias( ApiTag::class, 'ApiTag' );