Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.86% covered (warning)
89.86%
124 / 138
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiMove
90.51% covered (success)
90.51%
124 / 137
66.67% covered (warning)
66.67%
6 / 9
40.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 execute
91.67% covered (success)
91.67%
77 / 84
0.00% covered (danger)
0.00%
0 / 1
28.45
 moveSubpages
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 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%
22 / 22
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 / 5
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 LogicException;
12use MediaWiki\FileRepo\RepoGroup;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Page\MovePageFactory;
15use MediaWiki\Status\Status;
16use MediaWiki\Title\Title;
17use MediaWiki\User\Options\UserOptionsLookup;
18use MediaWiki\Watchlist\WatchedItemStoreInterface;
19use MediaWiki\Watchlist\WatchlistManager;
20use Wikimedia\ParamValidator\ParamValidator;
21
22/**
23 * API Module to move pages
24 * @ingroup API
25 */
26class ApiMove extends ApiBase {
27
28    use ApiWatchlistTrait;
29
30    private MovePageFactory $movePageFactory;
31    private RepoGroup $repoGroup;
32
33    public function __construct(
34        ApiMain $mainModule,
35        string $moduleName,
36        MovePageFactory $movePageFactory,
37        RepoGroup $repoGroup,
38        WatchlistManager $watchlistManager,
39        WatchedItemStoreInterface $watchedItemStore,
40        UserOptionsLookup $userOptionsLookup
41    ) {
42        parent::__construct( $mainModule, $moduleName );
43
44        $this->movePageFactory = $movePageFactory;
45        $this->repoGroup = $repoGroup;
46
47        // Variables needed in ApiWatchlistTrait trait
48        $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
49        $this->watchlistMaxDuration =
50            $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
51        $this->watchlistManager = $watchlistManager;
52        $this->watchedItemStore = $watchedItemStore;
53        $this->userOptionsLookup = $userOptionsLookup;
54    }
55
56    public function execute() {
57        $this->useTransactionalTimeLimit();
58
59        $user = $this->getUser();
60        $params = $this->extractRequestParams();
61
62        $this->requireOnlyOneParameter( $params, 'from', 'fromid' );
63
64        if ( isset( $params['from'] ) ) {
65            $fromTitle = Title::newFromText( $params['from'] );
66            if ( !$fromTitle || $fromTitle->isExternal() ) {
67                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['from'] ) ] );
68            }
69        } elseif ( isset( $params['fromid'] ) ) {
70            $fromTitle = Title::newFromID( $params['fromid'] );
71            if ( !$fromTitle ) {
72                $this->dieWithError( [ 'apierror-nosuchpageid', $params['fromid'] ] );
73            }
74        } else {
75            throw new LogicException( 'Unreachable due to requireOnlyOneParameter' );
76        }
77
78        if ( !$fromTitle->exists() ) {
79            $this->dieWithError( 'apierror-missingtitle' );
80        }
81        $fromTalk = $fromTitle->getTalkPage();
82
83        $toTitle = Title::newFromText( $params['to'] );
84        if ( !$toTitle || $toTitle->isExternal() ) {
85            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] );
86        }
87        $toTalk = $toTitle->getTalkPageIfDefined();
88
89        if ( $toTitle->getNamespace() === NS_FILE
90            && !$this->repoGroup->getLocalRepo()->findFile( $toTitle )
91            && $this->repoGroup->findFile( $toTitle )
92        ) {
93            if ( !$params['ignorewarnings'] &&
94                $this->getAuthority()->isAllowed( 'reupload-shared' ) ) {
95                $this->dieWithError( 'apierror-fileexists-sharedrepo-perm' );
96            } elseif ( !$this->getAuthority()->isAllowed( 'reupload-shared' ) ) {
97                $this->dieWithError( 'apierror-cantoverwrite-sharedfile' );
98            }
99        }
100
101        // Move the page
102        $toTitleExists = $toTitle->exists();
103        $mp = $this->movePageFactory->newMovePage( $fromTitle, $toTitle );
104        $status = $mp->moveIfAllowed(
105            $this->getAuthority(),
106            $params['reason'],
107            !$params['noredirect'],
108            $params['tags'] ?: []
109        );
110        if ( !$status->isOK() ) {
111            $this->dieStatus( $status );
112        }
113
114        $r = [
115            'from' => $fromTitle->getPrefixedText(),
116            'to' => $toTitle->getPrefixedText(),
117            'reason' => $params['reason']
118        ];
119
120        // NOTE: we assume that if the old title exists, it's because it was re-created as
121        // a redirect to the new title. This is not safe, but what we did before was
122        // even worse: we just determined whether a redirect should have been created,
123        // and reported that it was created if it should have, without any checks.
124        $r['redirectcreated'] = $fromTitle->exists();
125
126        $r['moveoverredirect'] = $toTitleExists;
127
128        // Move the talk page
129        if ( $params['movetalk'] && $toTalk && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
130            $toTalkExists = $toTalk->exists();
131            $mp = $this->movePageFactory->newMovePage( $fromTalk, $toTalk );
132            $status = $mp->moveIfAllowed(
133                $this->getAuthority(),
134                $params['reason'],
135                !$params['noredirect'],
136                $params['tags'] ?: []
137            );
138            if ( $status->isOK() ) {
139                $r['talkfrom'] = $fromTalk->getPrefixedText();
140                $r['talkto'] = $toTalk->getPrefixedText();
141                $r['talkmoveoverredirect'] = $toTalkExists;
142            } else {
143                // We're not going to dieWithError() on failure, since we already changed something
144                $r['talkmove-errors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
145            }
146        }
147
148        $result = $this->getResult();
149
150        // Move subpages
151        if ( $params['movesubpages'] ) {
152            $r['subpages'] = $this->moveSubpages(
153                $fromTitle,
154                $toTitle,
155                $params['reason'],
156                $params['noredirect'],
157                $params['tags'] ?: []
158            );
159            ApiResult::setIndexedTagName( $r['subpages'], 'subpage' );
160
161            if ( $params['movetalk'] && $toTalk ) {
162                $r['subpages-talk'] = $this->moveSubpages(
163                    $fromTalk,
164                    $toTalk,
165                    $params['reason'],
166                    $params['noredirect'],
167                    $params['tags'] ?: []
168                );
169                ApiResult::setIndexedTagName( $r['subpages-talk'], 'subpage' );
170            }
171        }
172
173        $watch = $params['watchlist'] ?? 'preferences';
174        $watchlistExpiryFrom = $this->getExpiryFromParams( $params, $fromTitle, $user );
175        $watchlistExpiryTo = $this->getExpiryFromParams( $params, $toTitle, $user );
176
177        // Watch pages
178        $this->setWatch( $watch, $fromTitle, $user, 'watchmoves', $watchlistExpiryFrom );
179        $this->setWatch( $watch, $toTitle, $user, 'watchmoves', $watchlistExpiryTo );
180
181        $result->addValue( null, $this->getModuleName(), $r );
182    }
183
184    /**
185     * @param Title $fromTitle
186     * @param Title $toTitle
187     * @param string $reason
188     * @param bool $noredirect
189     * @param string[] $changeTags Applied to the entry in the move log and redirect page revisions
190     * @return array
191     */
192    public function moveSubpages( $fromTitle, $toTitle, $reason, $noredirect, $changeTags = [] ) {
193        $retval = [];
194
195        $mp = $this->movePageFactory->newMovePage( $fromTitle, $toTitle );
196        $result =
197            $mp->moveSubpagesIfAllowed( $this->getAuthority(), $reason, !$noredirect, $changeTags );
198        if ( !$result->isOK() ) {
199            // This means the whole thing failed
200            return [ 'errors' => $this->getErrorFormatter()->arrayFromStatus( $result ) ];
201        }
202
203        // At least some pages could be moved
204        // Report each of them separately
205        foreach ( $result->getValue() as $oldTitle => $status ) {
206            /** @var Status $status */
207            $r = [ 'from' => $oldTitle ];
208            if ( $status->isOK() ) {
209                $r['to'] = $status->getValue();
210            } else {
211                $r['errors'] = $this->getErrorFormatter()->arrayFromStatus( $status );
212            }
213            $retval[] = $r;
214        }
215
216        return $retval;
217    }
218
219    /** @inheritDoc */
220    public function mustBePosted() {
221        return true;
222    }
223
224    /** @inheritDoc */
225    public function isWriteMode() {
226        return true;
227    }
228
229    /** @inheritDoc */
230    public function getAllowedParams() {
231        $params = [
232            'from' => null,
233            'fromid' => [
234                ParamValidator::PARAM_TYPE => 'integer'
235            ],
236            'to' => [
237                ParamValidator::PARAM_TYPE => 'string',
238                ParamValidator::PARAM_REQUIRED => true
239            ],
240            'reason' => '',
241            'movetalk' => false,
242            'movesubpages' => false,
243            'noredirect' => false,
244        ];
245
246        // Params appear in the docs in the order they are defined,
247        // which is why this is here and not at the bottom.
248        $params += $this->getWatchlistParams();
249
250        return $params + [
251            'ignorewarnings' => false,
252            'tags' => [
253                ParamValidator::PARAM_TYPE => 'tags',
254                ParamValidator::PARAM_ISMULTI => true,
255            ],
256        ];
257    }
258
259    /** @inheritDoc */
260    public function needsToken() {
261        return 'csrf';
262    }
263
264    /** @inheritDoc */
265    protected function getExamplesMessages() {
266        return [
267            'action=move&from=Badtitle&to=Goodtitle&token=123ABC&' .
268                'reason=Misspelled%20title&movetalk=&noredirect='
269                => 'apihelp-move-example-move',
270        ];
271    }
272
273    /** @inheritDoc */
274    public function getHelpUrls() {
275        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Move';
276    }
277}
278
279/** @deprecated class alias since 1.43 */
280class_alias( ApiMove::class, 'ApiMove' );