Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 150
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFeedContributions
0.00% covered (danger)
0.00%
0 / 149
0.00% covered (danger)
0.00%
0 / 9
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 61
0.00% covered (danger)
0.00%
0 / 1
132
 feedItem
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 feedItemAuthor
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 feedItemDesc
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
6
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
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 * 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
23namespace MediaWiki\Api;
24
25use MediaWiki\Cache\LinkBatchFactory;
26use MediaWiki\CommentFormatter\CommentFormatter;
27use MediaWiki\Content\TextContent;
28use MediaWiki\Feed\FeedItem;
29use MediaWiki\HookContainer\HookContainer;
30use MediaWiki\Linker\LinkRenderer;
31use MediaWiki\MainConfigNames;
32use MediaWiki\MediaWikiServices;
33use MediaWiki\Pager\ContribsPager;
34use MediaWiki\ParamValidator\TypeDef\UserDef;
35use MediaWiki\Revision\RevisionAccessException;
36use MediaWiki\Revision\RevisionRecord;
37use MediaWiki\Revision\RevisionStore;
38use MediaWiki\Revision\SlotRecord;
39use MediaWiki\SpecialPage\SpecialPage;
40use MediaWiki\Title\NamespaceInfo;
41use MediaWiki\Title\Title;
42use MediaWiki\User\ExternalUserNames;
43use MediaWiki\User\UserFactory;
44use MediaWiki\User\UserRigorOptions;
45use Wikimedia\ParamValidator\ParamValidator;
46use Wikimedia\Rdbms\IConnectionProvider;
47
48/**
49 * @ingroup API
50 */
51class 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 */
321class_alias( ApiFeedContributions::class, 'ApiFeedContributions' );