Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 150 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
ApiFeedContributions | |
0.00% |
0 / 149 |
|
0.00% |
0 / 9 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getCustomPrinter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
132 | |||
feedItem | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
feedItemAuthor | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
feedItemDesc | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getAllowedParams | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
6 | |||
getExamplesMessages | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2011 Sam Reed |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Api; |
24 | |
25 | use MediaWiki\Cache\LinkBatchFactory; |
26 | use MediaWiki\CommentFormatter\CommentFormatter; |
27 | use MediaWiki\Content\TextContent; |
28 | use MediaWiki\Feed\FeedItem; |
29 | use MediaWiki\HookContainer\HookContainer; |
30 | use MediaWiki\Linker\LinkRenderer; |
31 | use MediaWiki\MainConfigNames; |
32 | use MediaWiki\MediaWikiServices; |
33 | use MediaWiki\Pager\ContribsPager; |
34 | use MediaWiki\ParamValidator\TypeDef\UserDef; |
35 | use MediaWiki\Revision\RevisionAccessException; |
36 | use MediaWiki\Revision\RevisionRecord; |
37 | use MediaWiki\Revision\RevisionStore; |
38 | use MediaWiki\Revision\SlotRecord; |
39 | use MediaWiki\SpecialPage\SpecialPage; |
40 | use MediaWiki\Title\NamespaceInfo; |
41 | use MediaWiki\Title\Title; |
42 | use MediaWiki\User\ExternalUserNames; |
43 | use MediaWiki\User\UserFactory; |
44 | use MediaWiki\User\UserRigorOptions; |
45 | use Wikimedia\ParamValidator\ParamValidator; |
46 | use Wikimedia\Rdbms\IConnectionProvider; |
47 | |
48 | /** |
49 | * @ingroup API |
50 | */ |
51 | class ApiFeedContributions extends ApiBase { |
52 | |
53 | private RevisionStore $revisionStore; |
54 | private LinkRenderer $linkRenderer; |
55 | private LinkBatchFactory $linkBatchFactory; |
56 | private HookContainer $hookContainer; |
57 | private IConnectionProvider $dbProvider; |
58 | private NamespaceInfo $namespaceInfo; |
59 | private UserFactory $userFactory; |
60 | private CommentFormatter $commentFormatter; |
61 | private ApiHookRunner $hookRunner; |
62 | |
63 | public function __construct( |
64 | ApiMain $main, |
65 | string $action, |
66 | RevisionStore $revisionStore, |
67 | LinkRenderer $linkRenderer, |
68 | LinkBatchFactory $linkBatchFactory, |
69 | HookContainer $hookContainer, |
70 | IConnectionProvider $dbProvider, |
71 | NamespaceInfo $namespaceInfo, |
72 | UserFactory $userFactory, |
73 | CommentFormatter $commentFormatter |
74 | ) { |
75 | parent::__construct( $main, $action ); |
76 | $this->revisionStore = $revisionStore; |
77 | $this->linkRenderer = $linkRenderer; |
78 | $this->linkBatchFactory = $linkBatchFactory; |
79 | $this->hookContainer = $hookContainer; |
80 | $this->dbProvider = $dbProvider; |
81 | $this->namespaceInfo = $namespaceInfo; |
82 | $this->userFactory = $userFactory; |
83 | $this->commentFormatter = $commentFormatter; |
84 | |
85 | $this->hookRunner = new ApiHookRunner( $hookContainer ); |
86 | } |
87 | |
88 | /** |
89 | * This module uses a custom feed wrapper printer. |
90 | * |
91 | * @return ApiFormatFeedWrapper |
92 | */ |
93 | public function getCustomPrinter() { |
94 | return new ApiFormatFeedWrapper( $this->getMain() ); |
95 | } |
96 | |
97 | public function execute() { |
98 | $params = $this->extractRequestParams(); |
99 | |
100 | $config = $this->getConfig(); |
101 | if ( !$config->get( MainConfigNames::Feed ) ) { |
102 | $this->dieWithError( 'feed-unavailable' ); |
103 | } |
104 | |
105 | $feedClasses = $config->get( MainConfigNames::FeedClasses ); |
106 | if ( !isset( $feedClasses[$params['feedformat']] ) ) { |
107 | $this->dieWithError( 'feed-invalid' ); |
108 | } |
109 | |
110 | if ( $params['showsizediff'] && $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
111 | $this->dieWithError( 'apierror-sizediffdisabled' ); |
112 | } |
113 | |
114 | $msg = $this->msg( 'Contributions' )->inContentLanguage()->text(); |
115 | $feedTitle = $config->get( MainConfigNames::Sitename ) . ' - ' . $msg . |
116 | ' [' . $config->get( MainConfigNames::LanguageCode ) . ']'; |
117 | |
118 | $target = $params['user']; |
119 | if ( ExternalUserNames::isExternal( $target ) ) { |
120 | // Interwiki names make invalid titles, so put the target in the query instead. |
121 | $feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] ); |
122 | } else { |
123 | $feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL(); |
124 | } |
125 | |
126 | $feed = new $feedClasses[$params['feedformat']] ( |
127 | $feedTitle, |
128 | htmlspecialchars( $msg ), |
129 | $feedUrl |
130 | ); |
131 | |
132 | // Convert year/month parameters to end parameter |
133 | $params['start'] = ''; |
134 | $params['end'] = ''; |
135 | $params = ContribsPager::processDateFilter( $params ); |
136 | |
137 | $targetUser = $this->userFactory->newFromName( $target, UserRigorOptions::RIGOR_NONE ); |
138 | |
139 | $pager = new ContribsPager( |
140 | $this->getContext(), [ |
141 | 'target' => $target, |
142 | 'namespace' => $params['namespace'], |
143 | 'start' => $params['start'], |
144 | 'end' => $params['end'], |
145 | 'tagFilter' => $params['tagfilter'], |
146 | 'deletedOnly' => $params['deletedonly'], |
147 | 'topOnly' => $params['toponly'], |
148 | 'newOnly' => $params['newonly'], |
149 | 'hideMinor' => $params['hideminor'], |
150 | 'showSizeDiff' => $params['showsizediff'], |
151 | ], |
152 | $this->linkRenderer, |
153 | $this->linkBatchFactory, |
154 | $this->hookContainer, |
155 | $this->dbProvider, |
156 | $this->revisionStore, |
157 | $this->namespaceInfo, |
158 | $targetUser, |
159 | $this->commentFormatter |
160 | ); |
161 | |
162 | $feedLimit = $this->getConfig()->get( MainConfigNames::FeedLimit ); |
163 | if ( $pager->getLimit() > $feedLimit ) { |
164 | $pager->setLimit( $feedLimit ); |
165 | } |
166 | |
167 | $feedItems = []; |
168 | if ( $pager->getNumRows() > 0 ) { |
169 | $count = 0; |
170 | $limit = $pager->getLimit(); |
171 | foreach ( $pager->mResult as $row ) { |
172 | // ContribsPager selects one more row for navigation, skip that row |
173 | if ( ++$count > $limit ) { |
174 | break; |
175 | } |
176 | $item = $this->feedItem( $row ); |
177 | if ( $item !== null ) { |
178 | $feedItems[] = $item; |
179 | } |
180 | } |
181 | } |
182 | |
183 | ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems ); |
184 | } |
185 | |
186 | protected function feedItem( $row ) { |
187 | // This hook is the api contributions equivalent to the |
188 | // ContributionsLineEnding hook. Hook implementers may cancel |
189 | // the hook to signal the user is not allowed to read this item. |
190 | $feedItem = null; |
191 | $hookResult = $this->hookRunner->onApiFeedContributions__feedItem( |
192 | $row, $this->getContext(), $feedItem ); |
193 | // Hook returned a valid feed item |
194 | if ( $feedItem instanceof FeedItem ) { |
195 | return $feedItem; |
196 | // Hook was canceled and did not return a valid feed item |
197 | } elseif ( !$hookResult ) { |
198 | return null; |
199 | } |
200 | |
201 | // Hook completed and did not return a valid feed item |
202 | $title = Title::makeTitle( (int)$row->page_namespace, $row->page_title ); |
203 | |
204 | if ( $title && $this->getAuthority()->authorizeRead( 'read', $title ) ) { |
205 | $date = $row->rev_timestamp; |
206 | $comments = $title->getTalkPage()->getFullURL(); |
207 | $revision = $this->revisionStore->newRevisionFromRow( $row, 0, $title ); |
208 | |
209 | return new FeedItem( |
210 | $title->getPrefixedText(), |
211 | $this->feedItemDesc( $revision ), |
212 | $title->getFullURL( [ 'diff' => $revision->getId() ] ), |
213 | $date, |
214 | $this->feedItemAuthor( $revision ), |
215 | $comments |
216 | ); |
217 | } |
218 | |
219 | return null; |
220 | } |
221 | |
222 | /** |
223 | * @since 1.32, takes a RevisionRecord instead of a Revision |
224 | * @param RevisionRecord $revision |
225 | * @return string |
226 | */ |
227 | protected function feedItemAuthor( RevisionRecord $revision ) { |
228 | $user = $revision->getUser(); |
229 | return $user ? $user->getName() : ''; |
230 | } |
231 | |
232 | /** |
233 | * @since 1.32, takes a RevisionRecord instead of a Revision |
234 | * @param RevisionRecord $revision |
235 | * @return string |
236 | */ |
237 | protected function feedItemDesc( RevisionRecord $revision ) { |
238 | $msg = $this->msg( 'colon-separator' )->inContentLanguage()->text(); |
239 | try { |
240 | $content = $revision->getContent( SlotRecord::MAIN ); |
241 | } catch ( RevisionAccessException $e ) { |
242 | $content = null; |
243 | } |
244 | |
245 | if ( $content instanceof TextContent ) { |
246 | // only textual content has a "source view". |
247 | $html = nl2br( htmlspecialchars( $content->getText(), ENT_COMPAT ) ); |
248 | } else { |
249 | // XXX: we could get an HTML representation of the content via getParserOutput, but that may |
250 | // contain JS magic and generally may not be suitable for inclusion in a feed. |
251 | // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. |
252 | // Compare also MediaWiki\Feed\FeedUtils::formatDiffRow. |
253 | $html = ''; |
254 | } |
255 | |
256 | $comment = $revision->getComment(); |
257 | |
258 | return '<p>' . htmlspecialchars( $this->feedItemAuthor( $revision ) ) . $msg . |
259 | htmlspecialchars( FeedItem::stripComment( $comment->text ?? '' ) ) . |
260 | "</p>\n<hr />\n<div>" . $html . '</div>'; |
261 | } |
262 | |
263 | public function getAllowedParams() { |
264 | $feedFormatNames = array_keys( $this->getConfig()->get( MainConfigNames::FeedClasses ) ); |
265 | |
266 | $ret = [ |
267 | 'feedformat' => [ |
268 | ParamValidator::PARAM_DEFAULT => 'rss', |
269 | ParamValidator::PARAM_TYPE => $feedFormatNames |
270 | ], |
271 | 'user' => [ |
272 | ParamValidator::PARAM_TYPE => 'user', |
273 | UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'cidr', 'id', 'interwiki' ], |
274 | ParamValidator::PARAM_REQUIRED => true, |
275 | ], |
276 | 'namespace' => [ |
277 | ParamValidator::PARAM_TYPE => 'namespace' |
278 | ], |
279 | 'year' => [ |
280 | ParamValidator::PARAM_TYPE => 'integer' |
281 | ], |
282 | 'month' => [ |
283 | ParamValidator::PARAM_TYPE => 'integer' |
284 | ], |
285 | 'tagfilter' => [ |
286 | ParamValidator::PARAM_ISMULTI => true, |
287 | ParamValidator::PARAM_TYPE => array_values( MediaWikiServices::getInstance() |
288 | ->getChangeTagsStore()->listDefinedTags() |
289 | ), |
290 | ParamValidator::PARAM_DEFAULT => '', |
291 | ], |
292 | 'deletedonly' => false, |
293 | 'toponly' => false, |
294 | 'newonly' => false, |
295 | 'hideminor' => false, |
296 | 'showsizediff' => [ |
297 | ParamValidator::PARAM_DEFAULT => false, |
298 | ], |
299 | ]; |
300 | |
301 | if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
302 | $ret['showsizediff'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode'; |
303 | } |
304 | |
305 | return $ret; |
306 | } |
307 | |
308 | protected function getExamplesMessages() { |
309 | return [ |
310 | 'action=feedcontributions&user=Example' |
311 | => 'apihelp-feedcontributions-example-simple', |
312 | ]; |
313 | } |
314 | |
315 | public function getHelpUrls() { |
316 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Feedcontributions'; |
317 | } |
318 | } |
319 | |
320 | /** @deprecated class alias since 1.43 */ |
321 | class_alias( ApiFeedContributions::class, 'ApiFeedContributions' ); |