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