Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.26% covered (warning)
75.26%
73 / 97
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPurge
75.26% covered (warning)
75.26%
73 / 97
37.50% covered (danger)
37.50%
3 / 8
29.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
81.69% covered (warning)
81.69%
58 / 71
0.00% covered (danger)
0.00%
0 / 1
15.20
 getPageSet
100.00% covered (success)
100.00%
2 / 2
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
 mustBePosted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
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 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Deferred\DeferredUpdates;
22use MediaWiki\Logger\LoggerFactory;
23use MediaWiki\Page\WikiPageFactory;
24use MediaWiki\Permissions\PermissionStatus;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27use MediaWiki\Title\TitleFormatter;
28
29/**
30 * API interface for page purging
31 * @ingroup API
32 */
33class ApiPurge extends ApiBase {
34    /** @var ApiPageSet|null */
35    private $mPageSet = null;
36
37    private WikiPageFactory $wikiPageFactory;
38    private TitleFormatter $titleFormatter;
39
40    /**
41     * @param ApiMain $mainModule
42     * @param string $moduleName
43     * @param WikiPageFactory $wikiPageFactory
44     * @param TitleFormatter $titleFormatter
45     */
46    public function __construct(
47        ApiMain $mainModule,
48        $moduleName,
49        WikiPageFactory $wikiPageFactory,
50        TitleFormatter $titleFormatter
51    ) {
52        parent::__construct( $mainModule, $moduleName );
53        $this->wikiPageFactory = $wikiPageFactory;
54        $this->titleFormatter = $titleFormatter;
55    }
56
57    /**
58     * Purges the cache of a page
59     */
60    public function execute() {
61        $authority = $this->getAuthority();
62
63        // Fail early if the user is sitewide blocked.
64        $block = $authority->getBlock();
65        if ( $block && $block->isSitewide() ) {
66            $this->dieBlocked( $block );
67        }
68
69        $params = $this->extractRequestParams();
70
71        $continuationManager = new ApiContinuationManager( $this, [], [] );
72        $this->setContinuationManager( $continuationManager );
73
74        $forceLinkUpdate = $params['forcelinkupdate'];
75        $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate'];
76        $pageSet = $this->getPageSet();
77        $pageSet->execute();
78
79        $result = $pageSet->getInvalidTitlesAndRevisions();
80        $userName = $authority->getUser()->getName();
81        $now = wfTimestampNow();
82
83        foreach ( $pageSet->getGoodPages() as $pageIdentity ) {
84            $title = $this->titleFormatter->getPrefixedText( $pageIdentity );
85            $r = [
86                'ns' => $pageIdentity->getNamespace(),
87                'title' => $title,
88            ];
89            $page = $this->wikiPageFactory->newFromTitle( $pageIdentity );
90
91            $purgeAuthStatus = PermissionStatus::newEmpty();
92            if ( $authority->authorizeAction( 'purge', $purgeAuthStatus ) ) {
93                // Directly purge and skip the UI part of purge()
94                $page->doPurge();
95                $r['purged'] = true;
96            } else {
97                if ( $purgeAuthStatus->isRateLimitExceeded() ) {
98                    $this->addWarning( 'apierror-ratelimited' );
99                } else {
100                    $this->addWarning( Status::wrap( $purgeAuthStatus )->getMessage() );
101                }
102            }
103
104            if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
105                $linkpurgeAuthStatus = PermissionStatus::newEmpty();
106                if ( $authority->authorizeAction( 'linkpurge', $linkpurgeAuthStatus ) ) {
107                    # Logging to better see expensive usage patterns
108                    if ( $forceRecursiveLinkUpdate ) {
109                        LoggerFactory::getInstance( 'RecursiveLinkPurge' )->info(
110                            "Recursive link purge enqueued for {title}",
111                            [
112                                'user' => $userName,
113                                'title' => $title
114                            ]
115                        );
116                    }
117
118                    $page->updateParserCache( [
119                        'causeAction' => 'api-purge',
120                        'causeAgent' => $userName,
121                    ] );
122                    $page->doSecondaryDataUpdates( [
123                        'recursive' => $forceRecursiveLinkUpdate,
124                        'causeAction' => 'api-purge',
125                        'causeAgent' => $userName,
126                        'defer' => DeferredUpdates::PRESEND,
127                        'freshness' => $now,
128                    ] );
129                    $r['linkupdate'] = true;
130                } else {
131                    if ( $linkpurgeAuthStatus->isRateLimitExceeded() ) {
132                        $this->addWarning( 'apierror-ratelimited' );
133                        $forceLinkUpdate = false;
134                        $forceRecursiveLinkUpdate = false;
135                    } else {
136                        $this->addWarning( Status::wrap( $linkpurgeAuthStatus )->getMessage() );
137                    }
138                }
139            }
140
141            $result[] = $r;
142        }
143        $apiResult = $this->getResult();
144        ApiResult::setIndexedTagName( $result, 'page' );
145        $apiResult->addValue( null, $this->getModuleName(), $result );
146
147        $values = $pageSet->getNormalizedTitlesAsResult( $apiResult );
148        if ( $values ) {
149            $apiResult->addValue( null, 'normalized', $values );
150        }
151        $values = $pageSet->getConvertedTitlesAsResult( $apiResult );
152        if ( $values ) {
153            $apiResult->addValue( null, 'converted', $values );
154        }
155        $values = $pageSet->getRedirectTitlesAsResult( $apiResult );
156        if ( $values ) {
157            $apiResult->addValue( null, 'redirects', $values );
158        }
159
160        $this->setContinuationManager( null );
161        $continuationManager->setContinuationIntoResult( $apiResult );
162    }
163
164    /**
165     * Get a cached instance of an ApiPageSet object
166     * @return ApiPageSet
167     */
168    private function getPageSet() {
169        $this->mPageSet ??= new ApiPageSet( $this );
170
171        return $this->mPageSet;
172    }
173
174    public function isWriteMode() {
175        return true;
176    }
177
178    public function mustBePosted() {
179        return true;
180    }
181
182    public function getAllowedParams( $flags = 0 ) {
183        $result = [
184            'forcelinkupdate' => false,
185            'forcerecursivelinkupdate' => false,
186            'continue' => [
187                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
188            ],
189        ];
190        if ( $flags ) {
191            $result += $this->getPageSet()->getFinalParams( $flags );
192        }
193
194        return $result;
195    }
196
197    protected function getExamplesMessages() {
198        $title = Title::newMainPage()->getPrefixedText();
199        $mp = rawurlencode( $title );
200
201        return [
202            "action=purge&titles={$mp}|API"
203                => 'apihelp-purge-example-simple',
204            'action=purge&generator=allpages&gapnamespace=0&gaplimit=10'
205                => 'apihelp-purge-example-generator',
206        ];
207    }
208
209    public function getHelpUrls() {
210        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Purge';
211    }
212}