MediaWiki master
ApiFeedWatchlist.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Api;
10
11use Exception;
21use Wikimedia\Timestamp\TimestampFormat as TS;
22
31
33 private $watchlistModule = null;
35 private $linkToSections = false;
36
37 private ParserFactory $parserFactory;
38
39 public function __construct(
40 ApiMain $main,
41 string $action,
42 ParserFactory $parserFactory
43 ) {
44 parent::__construct( $main, $action );
45 $this->parserFactory = $parserFactory;
46 }
47
53 public function getCustomPrinter() {
54 return new ApiFormatFeedWrapper( $this->getMain() );
55 }
56
61 public function execute() {
62 $config = $this->getConfig();
63 $feedClasses = $config->get( MainConfigNames::FeedClasses );
64 '@phan-var array<string,class-string<ChannelFeed>> $feedClasses';
65 $params = [];
66 $feedItems = [];
67 try {
68 $params = $this->extractRequestParams();
69
70 if ( !$config->get( MainConfigNames::Feed ) ) {
71 $this->dieWithError( 'feed-unavailable' );
72 }
73
74 if ( !isset( $feedClasses[$params['feedformat']] ) ) {
75 $this->dieWithError( 'feed-invalid' );
76 }
77
78 // limit to the number of hours going from now back
79 $endTime = wfTimestamp( TS::MW, time() - (int)$params['hours'] * 60 * 60 );
80
81 // Prepare parameters for nested request
82 $fauxReqArr = [
83 'action' => 'query',
84 'meta' => 'siteinfo',
85 'siprop' => 'general',
86 'list' => 'watchlist',
87 'wlprop' => 'title|user|comment|timestamp|ids|loginfo',
88 'wldir' => 'older', // reverse order - from newest to oldest
89 'wlend' => $endTime, // stop at this time
90 'wllimit' => min( 50, $this->getConfig()->get( MainConfigNames::FeedLimit ) )
91 ];
92
93 if ( $params['wlowner'] !== null ) {
94 $fauxReqArr['wlowner'] = $params['wlowner'];
95 }
96 if ( $params['wltoken'] !== null ) {
97 $fauxReqArr['wltoken'] = $params['wltoken'];
98 }
99 if ( $params['wlexcludeuser'] !== null ) {
100 $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser'];
101 }
102 if ( $params['wlshow'] !== null ) {
103 $fauxReqArr['wlshow'] = ParamValidator::implodeMultiValue( $params['wlshow'] );
104 }
105 if ( $params['wltype'] !== null ) {
106 $fauxReqArr['wltype'] = ParamValidator::implodeMultiValue( $params['wltype'] );
107 }
108
109 // Support linking directly to sections when possible
110 // (possible only if section name is present in comment)
111 if ( $params['linktosections'] ) {
112 $this->linkToSections = true;
113 }
114
115 // Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
116 if ( $params['allrev'] ) {
117 $fauxReqArr['wlallrev'] = '';
118 }
119
120 $fauxReq = new FauxRequest( $fauxReqArr );
121
122 $module = new ApiMain( $fauxReq );
123 $module->execute();
124
125 $data = $module->getResult()->getResultData( [ 'query', 'watchlist' ] );
126 foreach ( (array)$data as $key => $info ) {
127 if ( ApiResult::isMetadataKey( $key ) ) {
128 continue;
129 }
130 $feedItem = $this->createFeedItem( $info );
131 if ( $feedItem ) {
132 $feedItems[] = $feedItem;
133 }
134 }
135
136 $msg = $this->msg( 'watchlist' )->inContentLanguage()->text();
137
138 $feedTitle = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - ' . $msg .
139 ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
140 $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
141
142 $feed = new $feedClasses[$params['feedformat']] (
143 $feedTitle,
144 htmlspecialchars( $msg ),
145 $feedUrl
146 );
147
148 ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
149 } catch ( Exception $e ) {
150 // Error results should not be cached
151 $this->getMain()->setCacheMaxAge( 0 );
152
153 // @todo FIXME: Localise brackets
154 $feedTitle = $this->getConfig()->get( MainConfigNames::Sitename ) . ' - Error - ' .
155 $this->msg( 'watchlist' )->inContentLanguage()->text() .
156 ' [' . $this->getConfig()->get( MainConfigNames::LanguageCode ) . ']';
157 $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL();
158
159 $feedFormat = $params['feedformat'] ?? 'rss';
160 $msg = $this->msg( 'watchlist' )->inContentLanguage()->escaped();
161 $feed = new $feedClasses[$feedFormat]( $feedTitle, $msg, $feedUrl );
162
163 if ( $e instanceof ApiUsageException ) {
164 foreach ( $e->getStatusValue()->getMessages() as $msg ) {
165 // @phan-suppress-next-line PhanUndeclaredMethod
166 $msg = ApiMessage::create( $msg )
167 ->inLanguage( $this->getLanguage() );
168 $errorTitle = $this->msg( 'api-feed-error-title', $msg->getApiCode() )->text();
169 $errorText = $msg->text();
170 $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
171 }
172 } else {
173 // Something is seriously wrong
174 $errorCode = 'internal_api_error';
175 $errorTitle = $this->msg( 'api-feed-error-title', $errorCode )->text();
176 $errorText = $e->getMessage();
177 $feedItems[] = new FeedItem( $errorTitle, $errorText, '', '', '' );
178 }
179
180 ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
181 }
182 }
183
188 private function createFeedItem( $info ) {
189 if ( !isset( $info['title'] ) ) {
190 // Probably a revdeled log entry, skip it.
191 return null;
192 }
193
194 $titleStr = $info['title'];
195 $title = Title::newFromText( $titleStr );
196 $curidParam = [];
197 if ( !$title || $title->isExternal() ) {
198 // Probably a formerly-valid title that's now conflicting with an
199 // interwiki prefix or the like.
200 if ( isset( $info['pageid'] ) ) {
201 $title = Title::newFromID( $info['pageid'] );
202 $curidParam = [ 'curid' => $info['pageid'] ];
203 }
204 if ( !$title || $title->isExternal() ) {
205 return null;
206 }
207 }
208 if ( isset( $info['revid'] ) ) {
209 if ( $info['revid'] === 0 && isset( $info['logid'] ) ) {
210 $logTitle = Title::makeTitle( NS_SPECIAL, 'Log' );
211 $titleUrl = $logTitle->getFullURL( [ 'logid' => $info['logid'] ] );
212 } else {
213 $titleUrl = $title->getFullURL( [ 'diff' => $info['revid'] ] );
214 }
215 } else {
216 $titleUrl = $title->getFullURL( $curidParam );
217 }
218 $comment = $info['comment'] ?? null;
219
220 // Create an anchor to section.
221 // The anchor won't work for sections that have dupes on page
222 // as there's no way to strip that info from ApiWatchlist (apparently?).
223 // RegExp in the line below is equal to MediaWiki\CommentFormatter\CommentParser::doSectionLinks().
224 if ( $this->linkToSections && $comment !== null &&
225 preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches )
226 ) {
227 $titleUrl .= $this->parserFactory->getMainInstance()->guessSectionNameFromWikiText( $matches[ 2 ] );
228 }
229
230 $timestamp = $info['timestamp'];
231
232 if ( isset( $info['user'] ) ) {
233 $user = $info['user'];
234 $completeText = "$comment ($user)";
235 } else {
236 $user = '';
237 $completeText = (string)$comment;
238 }
239
240 return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
241 }
242
244 private function getWatchlistModule() {
245 $this->watchlistModule ??= $this->getMain()->getModuleManager()->getModule( 'query' )
246 ->getModuleManager()->getModule( 'watchlist' );
247
248 return $this->watchlistModule;
249 }
250
252 public function getAllowedParams( $flags = 0 ) {
253 $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) );
254 $ret = [
255 'feedformat' => [
256 ParamValidator::PARAM_DEFAULT => 'rss',
257 ParamValidator::PARAM_TYPE => $feedFormatNames
258 ],
259 'hours' => [
260 ParamValidator::PARAM_DEFAULT => 24,
261 ParamValidator::PARAM_TYPE => 'integer',
262 IntegerDef::PARAM_MIN => 1,
263 IntegerDef::PARAM_MAX => 72,
264 ],
265 'linktosections' => false,
266 ];
267
268 $copyParams = [
269 'allrev' => 'allrev',
270 'owner' => 'wlowner',
271 'token' => 'wltoken',
272 'show' => 'wlshow',
273 'type' => 'wltype',
274 'excludeuser' => 'wlexcludeuser',
275 ];
276 // @phan-suppress-next-line PhanParamTooMany
277 $wlparams = $this->getWatchlistModule()->getAllowedParams( $flags );
278 foreach ( $copyParams as $from => $to ) {
279 $p = $wlparams[$from];
280 if ( !is_array( $p ) ) {
281 $p = [ ParamValidator::PARAM_DEFAULT => $p ];
282 }
283 if ( !isset( $p[ApiBase::PARAM_HELP_MSG] ) ) {
284 $p[ApiBase::PARAM_HELP_MSG] = "apihelp-query+watchlist-param-$from";
285 }
286 if ( isset( $p[ParamValidator::PARAM_TYPE] ) && is_array( $p[ParamValidator::PARAM_TYPE] ) &&
288 ) {
289 foreach ( $p[ParamValidator::PARAM_TYPE] as $v ) {
290 if ( !isset( $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] ) ) {
291 $p[ApiBase::PARAM_HELP_MSG_PER_VALUE][$v] = "apihelp-query+watchlist-paramvalue-$from-$v";
292 }
293 }
294 }
295 $ret[$to] = $p;
296 }
297
298 return $ret;
299 }
300
302 protected function getExamplesMessages() {
303 return [
304 'action=feedwatchlist'
305 => 'apihelp-feedwatchlist-example-default',
306 'action=feedwatchlist&allrev=&hours=6'
307 => 'apihelp-feedwatchlist-example-all6hrs',
308 ];
309 }
310
312 public function getHelpUrls() {
313 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist_feed';
314 }
315}
316
318class_alias( ApiFeedWatchlist::class, 'ApiFeedWatchlist' );
const NS_SPECIAL
Definition Defines.php:40
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:61
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1511
getMain()
Get the main module.
Definition ApiBase.php:561
getResult()
Get the result object.
Definition ApiBase.php:682
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:207
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:167
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:823
This action allows users to get their watchlist items in RSS/Atom formats.
getCustomPrinter()
This module uses a custom feed wrapper printer.
__construct(ApiMain $main, string $action, ParserFactory $parserFactory)
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
execute()
Make a nested call to the API to request watchlist items in the last $hours.
This printer is used to wrap an instance of the Feed class.
static setResult( $result, $feed, $feedItems)
Call this method to initialize output data.
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:66
static create( $msg, $code=null, ?array $data=null)
Create an IApiMessage for the message.
static isMetadataKey( $key)
Test whether a key should be considered metadata.
Exception used to abort API execution with an error.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Class to support the outputting of syndication feeds in Atom and RSS format.
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:26
A class containing constants representing the names of configuration variables.
const FeedClasses
Name constant for the FeedClasses setting, for use with Config::get()
const Feed
Name constant for the Feed setting, for use with Config::get()
const Sitename
Name constant for the Sitename setting, for use with Config::get()
const LanguageCode
Name constant for the LanguageCode setting, for use with Config::get()
const FeedLimit
Name constant for the FeedLimit setting, for use with Config::get()
WebRequest clone which takes values from a provided array.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Represents a title within MediaWiki.
Definition Title.php:70
Service for formatting and validating API parameters.
Type definition for integer types.
Language name search API.