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