Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFeedLQTThreads
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 8
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
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 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 createFeedItem
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
12
 createFeedTitle
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 getConditions
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 */
18
19namespace MediaWiki\Extension\LiquidThreads\Api;
20
21use ApiBase;
22use ApiFormatFeedWrapper;
23use ApiMain;
24use MediaWiki\Feed\FeedItem;
25use MediaWiki\Linker\Linker;
26use MediaWiki\Linker\LinkRenderer;
27use MediaWiki\MediaWikiServices;
28use MediaWiki\Page\WikiPageFactory;
29use MediaWiki\Title\Title;
30use TextContent;
31use Thread;
32use Threads;
33use Wikimedia\ParamValidator\ParamValidator;
34use Wikimedia\ParamValidator\TypeDef\IntegerDef;
35use Wikimedia\Rdbms\SelectQueryBuilder;
36
37/**
38 * This action returns LiquidThreads threads/posts in RSS/Atom formats.
39 *
40 * @ingroup API
41 */
42class ApiFeedLQTThreads extends ApiBase {
43    /** @var LinkRenderer */
44    private $linkRenderer;
45    /** @var WikiPageFactory */
46    private $wikiPageFactory;
47
48    public function __construct(
49        ApiMain $main,
50        $action,
51        LinkRenderer $linkRenderer,
52        WikiPageFactory $wikiPageFactory
53    ) {
54        parent::__construct( $main, $action );
55        $this->linkRenderer = $linkRenderer;
56        $this->wikiPageFactory = $wikiPageFactory;
57    }
58
59    /**
60     * This module uses a custom feed wrapper printer.
61     * @return ApiFormatFeedWrapper
62     */
63    public function getCustomPrinter() {
64        return new ApiFormatFeedWrapper( $this->getMain() );
65    }
66
67    /**
68     * Make a nested call to the API to request items in the last $hours.
69     * Wrap the result as an RSS/Atom feed.
70     */
71    public function execute() {
72        global $wgFeedClasses;
73
74        $params = $this->extractRequestParams();
75
76        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
77
78        $feedTitle = $this->createFeedTitle( $params );
79        $feedClass = $wgFeedClasses[$params['feedformat']];
80        $feedItems = [];
81
82        $feedUrl = Title::newMainPage()->getFullURL();
83
84        $res = $dbr->newSelectQueryBuilder()
85            ->select( '*' )
86            ->from( 'thread' )
87            ->where( $this->getConditions( $params, $dbr ) )
88            ->limit( 200 )
89            ->orderBy( 'thread_created', SelectQueryBuilder::SORT_DESC )
90            ->caller( __METHOD__ )
91            ->fetchResultSet();
92
93        foreach ( $res as $row ) {
94            $feedItems[] = $this->createFeedItem( $row );
95        }
96
97        $feed = new $feedClass( $feedTitle, '', $feedUrl );
98
99        ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems );
100    }
101
102    private function createFeedItem( $row ) {
103        $thread = Thread::newFromRow( $row );
104
105        $titleStr = $thread->subject();
106        $content = $thread->root()->getPage()->getContent();
107        $completeText = ( $content instanceof TextContent ) ? $content->getText() : '';
108        $completeText = $this->getOutput()->parseAsContent( $completeText );
109        $threadTitle = clone $thread->topmostThread()->title();
110        $threadTitle->setFragment( '#' . $thread->getAnchorName() );
111        $titleUrl = $threadTitle->getFullURL();
112        $timestamp = $thread->created();
113        $user = $thread->author()->getName();
114
115        // Prefix content with a quick description
116        $userLink = Linker::userLink( $thread->author()->getId(), $user );
117        $talkpageLink = $this->linkRenderer->makeLink( $thread->getTitle() );
118        if ( $thread->hasSuperthread() ) {
119            $stTitle = clone $thread->topmostThread()->title();
120            $stTitle->setFragment( '#' . $thread->superthread()->getAnchorName() );
121            $superthreadLink = $this->linkRenderer->makeLink( $stTitle );
122            $description = $this->msg( 'lqt-feed-reply-intro' )
123                ->rawParams( $talkpageLink, $userLink, $superthreadLink )
124                ->params( $user )
125                ->parseAsBlock();
126        } else {
127            // Third param is unused
128            $description = $this->msg( 'lqt-feed-new-thread-intro' )
129                ->rawParams( $talkpageLink, $userLink, '' )
130                ->params( $user )
131                ->parseAsBlock();
132        }
133
134        $completeText = $description . $completeText;
135
136        return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
137    }
138
139    public function createFeedTitle( $params ) {
140        $fromPlaces = [];
141
142        foreach ( (array)$params['thread'] as $thread ) {
143            $t = Title::newFromText( $thread );
144            if ( !$t ) {
145                continue;
146            }
147            $fromPlaces[] = $t->getPrefixedText();
148        }
149
150        foreach ( (array)$params['talkpage'] as $talkpage ) {
151            $t = Title::newFromText( $talkpage );
152            if ( !$t ) {
153                continue;
154            }
155            $fromPlaces[] = $t->getPrefixedText();
156        }
157
158        $fromCount = count( $fromPlaces );
159        $fromPlaces = $this->getLanguage()->commaList( $fromPlaces );
160
161        // What's included?
162        $types = (array)$params['type'];
163
164        if ( !count( array_diff( [ 'replies', 'newthreads' ], $types ) ) ) {
165            $msg = 'lqt-feed-title-all';
166        } elseif ( in_array( 'replies', $types ) ) {
167            $msg = 'lqt-feed-title-replies';
168        } elseif ( in_array( 'newthreads', $types ) ) {
169            $msg = 'lqt-feed-title-new-threads';
170        } else {
171            $msg = 'lqt-feed-title-all';
172        }
173
174        if ( $fromCount ) {
175            $msg .= '-from';
176        }
177
178        return $this->msg( $msg, $fromPlaces )->numParams( $fromCount )->text();
179    }
180
181    /**
182     * @param array $params
183     * @param \Wikimedia\Rdbms\IReadableDatabase $dbr
184     * @return array
185     */
186    private function getConditions( $params, $dbr ) {
187        $conds = [];
188
189        // Types
190        $conds['thread_type'] = Threads::TYPE_NORMAL;
191
192        // Limit
193        $cutoff = time() - intval( $params['days'] * 24 * 3600 );
194        $conds[] = $dbr->expr( 'thread_created', '>', $dbr->timestamp( $cutoff ) );
195
196        // Talkpage conditions
197        $pageConds = [];
198
199        $talkpages = (array)$params['talkpage'];
200        foreach ( $talkpages as $page ) {
201            $title = Title::newFromText( $page );
202            if ( !$title ) {
203                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $page ) ] );
204            }
205            $pageConds[] = $dbr->andExpr( [
206                'thread_article_namespace' => $title->getNamespace(),
207                'thread_article_title' => $title->getDBkey(),
208            ] );
209        }
210
211        // Thread conditions
212        $threads = (array)$params['thread'];
213        foreach ( $threads as $threadName ) {
214            $thread = null;
215            $threadTitle = Title::newFromText( $threadName );
216            if ( $threadTitle ) {
217                $thread = Threads::withRoot( $this->wikiPageFactory->newFromTitle( $threadTitle ) );
218            }
219
220            if ( !$thread ) {
221                continue;
222            }
223
224            $pageConds[] = $dbr->orExpr( [
225                'thread_ancestor' => $thread->id(),
226                'thread_id' => $thread->id()
227            ] );
228        }
229        if ( count( $pageConds ) ) {
230            $conds[] = $dbr->orExpr( $pageConds );
231        }
232
233        // New thread v. Reply
234        $types = (array)$params['type'];
235        if ( !in_array( 'replies', $types ) ) {
236            $conds[] = Threads::topLevelClause();
237        } elseif ( !in_array( 'newthreads', $types ) ) {
238            $conds[] = '!' . Threads::topLevelClause();
239        }
240
241        return $conds;
242    }
243
244    public function getAllowedParams() {
245        global $wgFeedClasses;
246        $feedFormatNames = array_keys( $wgFeedClasses );
247        return [
248            'feedformat' => [
249                ParamValidator::PARAM_DEFAULT => 'rss',
250                ParamValidator::PARAM_TYPE => $feedFormatNames
251            ],
252            'days' => [
253                ParamValidator::PARAM_DEFAULT => 7,
254                ParamValidator::PARAM_TYPE => 'integer',
255                IntegerDef::PARAM_MIN => 1,
256                IntegerDef::PARAM_MAX => 30,
257            ],
258            'type' => [
259                ParamValidator::PARAM_DEFAULT => 'newthreads',
260                ParamValidator::PARAM_TYPE => [ 'replies', 'newthreads' ],
261                ParamValidator::PARAM_ISMULTI => true,
262            ],
263            'talkpage' => [
264                ParamValidator::PARAM_ISMULTI => true,
265            ],
266            'thread' => [
267                ParamValidator::PARAM_ISMULTI => true,
268            ],
269        ];
270    }
271
272    /**
273     * @see ApiBase::getExamplesMessages()
274     * @return array
275     */
276    protected function getExamplesMessages() {
277        return [
278            'action=feedthreads'
279                => 'apihelp-feedthreads-example-1',
280            'action=feedthreads&type=replies&thread=Thread:Foo'
281                => 'apihelp-feedthreads-example-2',
282            'action=feedthreads&type=newthreads&talkpage=Talk:Main_Page'
283                => 'apihelp-feedthreads-example-3',
284        ];
285    }
286}