Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.62% covered (danger)
47.62%
50 / 105
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFeedRecentChanges
48.08% covered (danger)
48.08%
50 / 104
28.57% covered (danger)
28.57%
2 / 7
57.46
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
 getCustomPrinter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 getFeedObject
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getAllowedParams
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 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 * @license GPL-2.0-or-later
4 * @file
5 * @since 1.23
6 */
7
8namespace MediaWiki\Api;
9
10use MediaWiki\Context\DerivativeContext;
11use MediaWiki\Feed\ChannelFeed;
12use MediaWiki\MainConfigNames;
13use MediaWiki\RecentChanges\ChangesFeed;
14use MediaWiki\Request\DerivativeRequest;
15use MediaWiki\SpecialPage\SpecialPage;
16use MediaWiki\SpecialPage\SpecialPageFactory;
17use MediaWiki\Title\Title;
18use MediaWiki\User\TempUser\TempUserConfig;
19use RuntimeException;
20use Wikimedia\ParamValidator\ParamValidator;
21use Wikimedia\ParamValidator\TypeDef\IntegerDef;
22
23/**
24 * Recent changes feed.
25 *
26 * @ingroup RecentChanges
27 * @ingroup API
28 */
29class ApiFeedRecentChanges extends ApiBase {
30
31    /** @var array */
32    private $params;
33
34    private SpecialPageFactory $specialPageFactory;
35    private TempUserConfig $tempUserConfig;
36
37    /**
38     * @param ApiMain $mainModule
39     * @param string $moduleName
40     * @param SpecialPageFactory $specialPageFactory
41     * @param TempUserConfig $tempUserConfig
42     */
43    public function __construct(
44        ApiMain $mainModule,
45        string $moduleName,
46        SpecialPageFactory $specialPageFactory,
47        TempUserConfig $tempUserConfig
48    ) {
49        parent::__construct( $mainModule, $moduleName );
50        $this->specialPageFactory = $specialPageFactory;
51        $this->tempUserConfig = $tempUserConfig;
52    }
53
54    /**
55     * This module uses a custom feed wrapper printer.
56     *
57     * @return ApiFormatFeedWrapper
58     */
59    public function getCustomPrinter() {
60        return new ApiFormatFeedWrapper( $this->getMain() );
61    }
62
63    /**
64     * Format the rows (generated by SpecialRecentchanges or SpecialRecentchangeslinked)
65     * as an RSS/Atom feed.
66     */
67    public function execute() {
68        $config = $this->getConfig();
69
70        $this->params = $this->extractRequestParams();
71
72        if ( !$config->get( MainConfigNames::Feed ) ) {
73            $this->dieWithError( 'feed-unavailable' );
74        }
75
76        $feedClasses = $config->get( MainConfigNames::FeedClasses );
77        '@phan-var array<string,class-string<ChannelFeed>> $feedClasses';
78        if ( !isset( $feedClasses[$this->params['feedformat']] ) ) {
79            $this->dieWithError( 'feed-invalid' );
80        }
81
82        $this->getMain()->setCacheMode( 'public' );
83        if ( !$this->getMain()->getParameter( 'smaxage' ) ) {
84            // T65249: This page gets hit a lot, cache at least 15 seconds.
85            $this->getMain()->setCacheMaxAge( 15 );
86        }
87
88        $feedFormat = $this->params['feedformat'];
89        $specialPageName = $this->params['target'] !== null
90            ? 'Recentchangeslinked'
91            : 'Recentchanges';
92
93        $formatter = $this->getFeedObject( $feedFormat, $specialPageName );
94
95        // Parameters are passed via the request in the context… :(
96        $context = new DerivativeContext( $this );
97        $context->setRequest( new DerivativeRequest(
98            $this->getRequest(),
99            $this->params,
100            $this->getRequest()->wasPosted()
101        ) );
102
103        // The row-getting functionality should be factored out of ChangesListSpecialPage too…
104        $rc = $this->specialPageFactory->getPage( $specialPageName );
105        if ( $rc === null ) {
106            throw new RuntimeException( __METHOD__ . ' not able to instance special page ' . $specialPageName );
107        }
108        '@phan-var \MediaWiki\SpecialPage\ChangesListSpecialPage $rc';
109        $rc->setContext( $context );
110        $rows = $rc->getRows();
111
112        $feedItems = $rows ? ChangesFeed::buildItems( $rows ) : [];
113
114        ApiFormatFeedWrapper::setResult( $this->getResult(), $formatter, $feedItems );
115    }
116
117    /**
118     * Return a MediaWiki\Feed\ChannelFeed object.
119     *
120     * @param string $feedFormat Feed's format (either 'rss' or 'atom')
121     * @param string $specialPageName Relevant special page name (either 'Recentchanges' or
122     *     'Recentchangeslinked')
123     * @return ChannelFeed
124     */
125    private function getFeedObject( $feedFormat, $specialPageName ) {
126        if ( $specialPageName === 'Recentchangeslinked' ) {
127            $title = Title::newFromText( $this->params['target'] );
128            if ( !$title || $title->isExternal() ) {
129                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['target'] ) ] );
130            }
131
132            $feed = new ChangesFeed( $feedFormat );
133            $feedObj = $feed->getFeedObject(
134                $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() )
135                    ->inContentLanguage()->text(),
136                $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(),
137                SpecialPage::getTitleFor( 'Recentchangeslinked' )->getFullURL()
138            );
139        } else {
140            $feed = new ChangesFeed( $feedFormat );
141            $feedObj = $feed->getFeedObject(
142                $this->msg( 'recentchanges' )->inContentLanguage()->text(),
143                $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(),
144                SpecialPage::getTitleFor( 'Recentchanges' )->getFullURL()
145            );
146        }
147
148        return $feedObj;
149    }
150
151    /** @inheritDoc */
152    public function getAllowedParams() {
153        $config = $this->getConfig();
154        $feedFormatNames = array_keys( $config->get( MainConfigNames::FeedClasses ) );
155
156        return [
157            'feedformat' => [
158                ParamValidator::PARAM_DEFAULT => 'rss',
159                ParamValidator::PARAM_TYPE => $feedFormatNames,
160            ],
161
162            'namespace' => [
163                ParamValidator::PARAM_TYPE => 'namespace',
164            ],
165            // TODO: Rename this option to 'invertnamespaces'?
166            'invert' => false,
167            'associated' => false,
168
169            'days' => [
170                ParamValidator::PARAM_DEFAULT => 7,
171                IntegerDef::PARAM_MIN => 1,
172                ParamValidator::PARAM_TYPE => 'integer',
173            ],
174            'limit' => [
175                ParamValidator::PARAM_DEFAULT => 50,
176                IntegerDef::PARAM_MIN => 1,
177                IntegerDef::PARAM_MAX => $config->get( MainConfigNames::FeedLimit ),
178                ParamValidator::PARAM_TYPE => 'integer',
179            ],
180            'from' => [
181                ParamValidator::PARAM_TYPE => 'timestamp',
182            ],
183
184            'hideminor' => false,
185            'hidebots' => false,
186            'hideanons' => [
187                ParamValidator::PARAM_DEFAULT => false,
188                ApiBase::PARAM_HELP_MSG => $this->tempUserConfig->isKnown() ?
189                    'apihelp-feedrecentchanges-param-hideanons-temp' :
190                    'apihelp-feedrecentchanges-param-hideanons',
191            ],
192            'hideliu' => false,
193            'hidepatrolled' => false,
194            'hidemyself' => false,
195            'hidecategorization' => false,
196
197            'tagfilter' => [
198                ParamValidator::PARAM_TYPE => 'string',
199            ],
200            'inverttags' => false,
201
202            'target' => [
203                ParamValidator::PARAM_TYPE => 'string',
204            ],
205            'showlinkedto' => false,
206        ];
207    }
208
209    /** @inheritDoc */
210    protected function getExamplesMessages() {
211        return [
212            'action=feedrecentchanges'
213                => 'apihelp-feedrecentchanges-example-simple',
214            'action=feedrecentchanges&days=30'
215                => 'apihelp-feedrecentchanges-example-30days',
216        ];
217    }
218
219    /** @inheritDoc */
220    public function getHelpUrls() {
221        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Feedrecentchanges';
222    }
223}
224
225/** @deprecated class alias since 1.43 */
226class_alias( ApiFeedRecentChanges::class, 'ApiFeedRecentChanges' );