Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.96% covered (warning)
73.96%
71 / 96
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPurge
74.74% covered (warning)
74.74%
71 / 95
37.50% covered (danger)
37.50%
3 / 8
29.80
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
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 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Api;
8
9use MediaWiki\Deferred\DeferredUpdates;
10use MediaWiki\Logger\LoggerFactory;
11use MediaWiki\Page\WikiPageFactory;
12use MediaWiki\Permissions\PermissionStatus;
13use MediaWiki\Status\Status;
14use MediaWiki\Title\Title;
15use MediaWiki\Title\TitleFormatter;
16
17/**
18 * API interface for page purging
19 * @ingroup API
20 */
21class ApiPurge extends ApiBase {
22    /** @var ApiPageSet|null */
23    private $mPageSet = null;
24
25    public function __construct(
26        ApiMain $mainModule,
27        string $moduleName,
28        private readonly WikiPageFactory $wikiPageFactory,
29        private readonly TitleFormatter $titleFormatter,
30    ) {
31        parent::__construct( $mainModule, $moduleName );
32    }
33
34    /**
35     * Purges the cache of a page
36     */
37    public function execute() {
38        $authority = $this->getAuthority();
39
40        // Fail early if the user is sitewide blocked.
41        $block = $authority->getBlock();
42        if ( $block && $block->isSitewide() ) {
43            $this->dieBlocked( $block );
44        }
45
46        $params = $this->extractRequestParams();
47
48        $continuationManager = new ApiContinuationManager( $this, [], [] );
49        $this->setContinuationManager( $continuationManager );
50
51        $forceLinkUpdate = $params['forcelinkupdate'];
52        $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate'];
53        $pageSet = $this->getPageSet();
54        $pageSet->execute();
55
56        $result = $pageSet->getInvalidTitlesAndRevisions();
57        $userName = $authority->getUser()->getName();
58        $now = wfTimestampNow();
59
60        foreach ( $pageSet->getGoodPages() as $pageIdentity ) {
61            $title = $this->titleFormatter->getPrefixedText( $pageIdentity );
62            $r = [
63                'ns' => $pageIdentity->getNamespace(),
64                'title' => $title,
65            ];
66            $page = $this->wikiPageFactory->newFromTitle( $pageIdentity );
67
68            $purgeAuthStatus = PermissionStatus::newEmpty();
69            if ( $authority->authorizeAction( 'purge', $purgeAuthStatus ) ) {
70                // Directly purge and skip the UI part of purge()
71                $page->doPurge();
72                $r['purged'] = true;
73            } else {
74                if ( $purgeAuthStatus->isRateLimitExceeded() ) {
75                    $this->addWarning( 'apierror-ratelimited' );
76                } else {
77                    $this->addWarning( Status::wrap( $purgeAuthStatus )->getMessage() );
78                }
79            }
80
81            if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
82                $linkpurgeAuthStatus = PermissionStatus::newEmpty();
83                if ( $authority->authorizeAction( 'linkpurge', $linkpurgeAuthStatus ) ) {
84                    # Logging to better see expensive usage patterns
85                    if ( $forceRecursiveLinkUpdate ) {
86                        LoggerFactory::getInstance( 'RecursiveLinkPurge' )->info(
87                            "Recursive link purge enqueued for {title}",
88                            [
89                                'user' => $userName,
90                                'title' => $title
91                            ]
92                        );
93                    }
94
95                    $page->updateParserCache( [
96                        'causeAction' => 'api-purge',
97                        'causeAgent' => $userName,
98                    ] );
99                    $page->doSecondaryDataUpdates( [
100                        'recursive' => $forceRecursiveLinkUpdate,
101                        'causeAction' => 'api-purge',
102                        'causeAgent' => $userName,
103                        'defer' => DeferredUpdates::PRESEND,
104                        'freshness' => $now,
105                    ] );
106                    $r['linkupdate'] = true;
107                } else {
108                    if ( $linkpurgeAuthStatus->isRateLimitExceeded() ) {
109                        $this->addWarning( 'apierror-ratelimited' );
110                        $forceLinkUpdate = false;
111                        $forceRecursiveLinkUpdate = false;
112                    } else {
113                        $this->addWarning( Status::wrap( $linkpurgeAuthStatus )->getMessage() );
114                    }
115                }
116            }
117
118            $result[] = $r;
119        }
120        $apiResult = $this->getResult();
121        ApiResult::setIndexedTagName( $result, 'page' );
122        $apiResult->addValue( null, $this->getModuleName(), $result );
123
124        $values = $pageSet->getNormalizedTitlesAsResult( $apiResult );
125        if ( $values ) {
126            $apiResult->addValue( null, 'normalized', $values );
127        }
128        $values = $pageSet->getConvertedTitlesAsResult( $apiResult );
129        if ( $values ) {
130            $apiResult->addValue( null, 'converted', $values );
131        }
132        $values = $pageSet->getRedirectTitlesAsResult( $apiResult );
133        if ( $values ) {
134            $apiResult->addValue( null, 'redirects', $values );
135        }
136
137        $this->setContinuationManager( null );
138        $continuationManager->setContinuationIntoResult( $apiResult );
139    }
140
141    /**
142     * Get a cached instance of an ApiPageSet object
143     * @return ApiPageSet
144     */
145    private function getPageSet() {
146        $this->mPageSet ??= new ApiPageSet( $this );
147
148        return $this->mPageSet;
149    }
150
151    /** @inheritDoc */
152    public function isWriteMode() {
153        return true;
154    }
155
156    /** @inheritDoc */
157    public function mustBePosted() {
158        return true;
159    }
160
161    /** @inheritDoc */
162    public function getAllowedParams( $flags = 0 ) {
163        $result = [
164            'forcelinkupdate' => false,
165            'forcerecursivelinkupdate' => false,
166            'continue' => [
167                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
168            ],
169        ];
170        if ( $flags ) {
171            $result += $this->getPageSet()->getFinalParams( $flags );
172        }
173
174        return $result;
175    }
176
177    /** @inheritDoc */
178    protected function getExamplesMessages() {
179        $title = Title::newMainPage()->getPrefixedText();
180        $mp = rawurlencode( $title );
181
182        return [
183            "action=purge&titles={$mp}|API"
184                => 'apihelp-purge-example-simple',
185            'action=purge&generator=allpages&gapnamespace=0&gaplimit=10'
186                => 'apihelp-purge-example-generator',
187        ];
188    }
189
190    /** @inheritDoc */
191    public function getHelpUrls() {
192        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Purge';
193    }
194}
195
196/** @deprecated class alias since 1.43 */
197class_alias( ApiPurge::class, 'ApiPurge' );