Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.39% covered (warning)
73.39%
91 / 124
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiProtect
73.98% covered (warning)
73.98%
91 / 123
62.50% covered (warning)
62.50%
5 / 8
39.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 execute
75.00% covered (warning)
75.00%
54 / 72
0.00% covered (danger)
0.00%
0 / 1
26.25
 mustBePosted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWriteMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 needsToken
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 13
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 * Copyright © 2007 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use InvalidArgumentException;
12use MediaWiki\ChangeTags\ChangeTags;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Permissions\RestrictionStore;
15use MediaWiki\Title\Title;
16use MediaWiki\User\Options\UserOptionsLookup;
17use MediaWiki\Utils\MWTimestamp;
18use MediaWiki\Watchlist\WatchedItemStoreInterface;
19use MediaWiki\Watchlist\WatchlistManager;
20use Wikimedia\ParamValidator\ParamValidator;
21use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
22use Wikimedia\Timestamp\TimestampFormat as TS;
23
24/**
25 * @ingroup API
26 */
27class ApiProtect extends ApiBase {
28
29    use ApiWatchlistTrait;
30
31    public function __construct(
32        ApiMain $mainModule,
33        string $moduleName,
34        WatchlistManager $watchlistManager,
35        WatchedItemStoreInterface $watchedItemStore,
36        UserOptionsLookup $userOptionsLookup,
37        private readonly RestrictionStore $restrictionStore,
38    ) {
39        parent::__construct( $mainModule, $moduleName );
40
41        // Variables needed in ApiWatchlistTrait trait
42        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
43        $this->watchlistMaxDuration =
44            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
45        $this->watchlistManager = $watchlistManager;
46        $this->watchedItemStore = $watchedItemStore;
47        $this->userOptionsLookup = $userOptionsLookup;
48    }
49
50    public function execute() {
51        $params = $this->extractRequestParams();
52
53        $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
54        $titleObj = $pageObj->getTitle();
55        $this->getErrorFormatter()->setContextTitle( $titleObj );
56
57        $this->checkTitleUserPermissions( $titleObj, 'protect' );
58
59        $user = $this->getUser();
60        $tags = $params['tags'];
61
62        // Check if user can add tags
63        if ( $tags !== null ) {
64            $ableToTag = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->getAuthority() );
65            if ( !$ableToTag->isOK() ) {
66                $this->dieStatus( $ableToTag );
67            }
68        }
69
70        $expiry = (array)$params['expiry'];
71        if ( count( $expiry ) != count( $params['protections'] ) ) {
72            if ( count( $expiry ) == 1 ) {
73                $expiry = array_fill( 0, count( $params['protections'] ), $expiry[0] );
74            } else {
75                $this->dieWithError( [
76                    'apierror-toofewexpiries',
77                    count( $expiry ),
78                    count( $params['protections'] )
79                ] );
80            }
81        }
82
83        $restrictionTypes = $this->restrictionStore->listApplicableRestrictionTypes( $titleObj );
84        $levels = $this->getPermissionManager()->getNamespaceRestrictionLevels(
85            $titleObj->getNamespace(),
86            $user
87        );
88
89        $protections = [];
90        $expiries = [];
91        $resultProtections = [];
92        foreach ( $params['protections'] as $i => $prot ) {
93            $p = explode( '=', $prot );
94            $protections[$p[0]] = ( $p[1] == 'all' ? '' : $p[1] );
95
96            if ( $titleObj->exists() && $p[0] == 'create' ) {
97                $this->dieWithError( 'apierror-create-titleexists' );
98            }
99            if ( !$titleObj->exists() && $p[0] != 'create' ) {
100                $this->dieWithError( 'apierror-missingtitle-createonly' );
101            }
102
103            if ( !in_array( $p[0], $restrictionTypes ) && $p[0] != 'create' ) {
104                $this->dieWithError( [ 'apierror-protect-invalidaction', wfEscapeWikiText( $p[0] ) ] );
105            }
106            if ( !in_array( $p[1], $levels ) && $p[1] != 'all' ) {
107                $this->dieWithError( [ 'apierror-protect-invalidlevel', wfEscapeWikiText( $p[1] ) ] );
108            }
109
110            try {
111                $expiries[$p[0]] = ExpiryDef::normalizeExpiry( $expiry[$i], TS::MW );
112            } catch ( InvalidArgumentException ) {
113                $this->dieWithError( [ 'apierror-invalidexpiry', wfEscapeWikiText( $expiry[$i] ) ] );
114            }
115            if ( $expiries[$p[0]] < MWTimestamp::now( TS::MW ) ) {
116                $this->dieWithError( [ 'apierror-pastexpiry', wfEscapeWikiText( $expiry[$i] ) ] );
117            }
118
119            $resultProtections[] = [
120                $p[0] => $protections[$p[0]],
121                'expiry' => ApiResult::formatExpiry( $expiries[$p[0]], 'infinite' ),
122            ];
123        }
124
125        $cascade = $params['cascade'];
126
127        $watch = $params['watch'] ? 'watch' : $params['watchlist'];
128        $watchlistExpiry = $this->getExpiryFromParams( $params, $titleObj, $user );
129        $this->setWatch( $watch, $titleObj, $user, 'watchdefault', $watchlistExpiry );
130
131        $status = $pageObj->doUpdateRestrictions(
132            $protections,
133            $expiries,
134            $cascade,
135            $params['reason'],
136            $user,
137            $tags ?? []
138        );
139
140        if ( !$status->isOK() ) {
141            $this->dieStatus( $status );
142        }
143        $res = [
144            'title' => $titleObj->getPrefixedText(),
145            'reason' => $params['reason']
146        ];
147        if ( $cascade ) {
148            $res['cascade'] = true;
149        }
150        $res['protections'] = $resultProtections;
151        $result = $this->getResult();
152        ApiResult::setIndexedTagName( $res['protections'], 'protection' );
153        $result->addValue( null, $this->getModuleName(), $res );
154    }
155
156    /** @inheritDoc */
157    public function mustBePosted() {
158        return true;
159    }
160
161    /** @inheritDoc */
162    public function isWriteMode() {
163        return true;
164    }
165
166    /** @inheritDoc */
167    public function getAllowedParams() {
168        return [
169            'title' => [
170                ParamValidator::PARAM_TYPE => 'string',
171            ],
172            'pageid' => [
173                ParamValidator::PARAM_TYPE => 'integer',
174            ],
175            'protections' => [
176                ParamValidator::PARAM_ISMULTI => true,
177                ParamValidator::PARAM_REQUIRED => true,
178            ],
179            'expiry' => [
180                ParamValidator::PARAM_ISMULTI => true,
181                ParamValidator::PARAM_ALLOW_DUPLICATES => true,
182                ParamValidator::PARAM_DEFAULT => 'infinite',
183            ],
184            'reason' => '',
185            'tags' => [
186                ParamValidator::PARAM_TYPE => 'tags',
187                ParamValidator::PARAM_ISMULTI => true,
188            ],
189            'cascade' => false,
190            'watch' => [
191                ParamValidator::PARAM_DEFAULT => false,
192                ParamValidator::PARAM_DEPRECATED => true,
193            ],
194        ] + $this->getWatchlistParams();
195    }
196
197    /** @inheritDoc */
198    public function needsToken() {
199        return 'csrf';
200    }
201
202    /** @inheritDoc */
203    protected function getExamplesMessages() {
204        $title = Title::newMainPage()->getPrefixedText();
205        $mp = rawurlencode( $title );
206
207        return [
208            "action=protect&title={$mp}&token=123ABC&" .
209                'protections=edit=sysop|move=sysop&cascade=&expiry=20070901163000|never'
210                => 'apihelp-protect-example-protect',
211            "action=protect&title={$mp}&token=123ABC&" .
212                'protections=edit=all|move=all&reason=Lifting%20restrictions'
213                => 'apihelp-protect-example-unprotect',
214            "action=protect&title={$mp}&token=123ABC&" .
215                'protections=&reason=Lifting%20restrictions'
216                => 'apihelp-protect-example-unprotect2',
217        ];
218    }
219
220    /** @inheritDoc */
221    public function getHelpUrls() {
222        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Protect';
223    }
224}
225
226/** @deprecated class alias since 1.43 */
227class_alias( ApiProtect::class, 'ApiProtect' );