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