Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 249
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryLQTThreads
0.00% covered (danger)
0.00%
0 / 249
0.00% covered (danger)
0.00%
0 / 11
2070
0.00% covered (danger)
0.00%
0 / 1
 __construct
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 / 77
0.00% covered (danger)
0.00%
0 / 1
240
 addSubItems
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 renderThread
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 formatProperty
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 addPageCond
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getPageCond
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 handleCondition
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getCacheMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\LiquidThreads\Api;
4
5use ApiBase;
6use ApiQueryBase;
7use ApiResult;
8use LqtView;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Title\Title;
11use stdClass;
12use Thread;
13use Threads;
14use Wikimedia\ParamValidator\ParamValidator;
15use Wikimedia\ParamValidator\TypeDef\IntegerDef;
16
17/**
18 * LiquidThreads API Query module
19 *
20 * Data that can be returned:
21 * - ID
22 * - Subject
23 * - "host page"
24 * - parent
25 * - ancestor
26 * - creation time
27 * - modification time
28 * - author
29 * - summary article ID
30 * - "root" page ID
31 * - type
32 * - replies
33 * - reactions
34 */
35
36class ApiQueryLQTThreads extends ApiQueryBase {
37    /**
38     * @var (null|string|string[])[] Property definitions
39     */
40    public static $propRelations = [
41        'id' => 'thread_id',
42        'subject' => 'thread_subject',
43        'page' => [
44            'namespace' => 'thread_article_namespace',
45            'title' => 'thread_article_title'
46        ],
47        'parent' => 'thread_parent',
48        'ancestor' => 'thread_ancestor',
49        'created' => 'thread_created',
50        'modified' => 'thread_modified',
51        'author' => [
52            'id' => 'thread_author_id',
53            'name' => 'thread_author_name'
54        ],
55        'summaryid' => 'thread_summary_page',
56        'rootid' => 'thread_root',
57        'type' => 'thread_type',
58        'signature' => 'thread_signature',
59        'reactions' => null, // Handled elsewhere
60        'replies' => null, // Handled elsewhere
61    ];
62
63    /** @var array */
64    protected $threadIds;
65
66    public function __construct( $query, $moduleName ) {
67        parent::__construct( $query, $moduleName, 'th' );
68    }
69
70    public function execute() {
71        $params = $this->extractRequestParams();
72        $prop = array_flip( $params['prop'] );
73        $result = $this->getResult();
74        $this->addTables( 'thread' );
75        $this->addFields( 'thread_id' );
76
77        foreach ( self::$propRelations as $name => $fields ) {
78            // Pass a straight array rather than one with string
79            // keys, to be sure that merging it into other added
80            // arrays doesn't mess stuff up
81            $this->addFieldsIf( array_values( (array)$fields ), isset( $prop[$name] ) );
82        }
83
84        // Check for conditions
85        $conditionFields = [ 'page', 'root', 'summary', 'author', 'id' ];
86        foreach ( $conditionFields as $field ) {
87            if ( isset( $params[$field] ) ) {
88                $this->handleCondition( $field, $params[$field] );
89            }
90        }
91
92        $this->addOption( 'LIMIT', $params['limit'] + 1 );
93        $this->addWhereRange( 'thread_id', $params['dir'],
94            $params['startid'], $params['endid'] );
95
96        if ( !$params['showdeleted'] ) {
97            $this->addWhere( $this->getDB()->expr( 'thread_type', '!=', Threads::TYPE_DELETED ) );
98        }
99
100        if ( $params['render'] ) {
101            // All fields
102            $allFields = [
103                'thread_id', 'thread_root', 'thread_article_namespace',
104                'thread_article_title', 'thread_summary_page', 'thread_ancestor',
105                'thread_parent', 'thread_modified', 'thread_created', 'thread_type',
106                'thread_editedness', 'thread_subject', 'thread_author_id',
107                'thread_author_name', 'thread_signature'
108            ];
109
110            $this->addFields( $allFields );
111        }
112
113        $res = $this->select( __METHOD__ );
114
115        $ids = [];
116        $count = 0;
117        foreach ( $res as $row ) {
118            if ( ++$count > $params['limit'] ) {
119                // We've had enough
120                $this->setContinueEnumParameter( 'startid', $row->thread_id );
121                break;
122            }
123
124            $entry = [];
125            foreach ( $prop as $name => $nothing ) {
126                $fields = self::$propRelations[$name];
127                self::formatProperty( $name, $fields, $row, $entry );
128            }
129
130            if ( isset( $entry['reactions'] ) ) {
131                ApiResult::setIndexedTagName( $entry['reactions'], 'reaction' );
132            }
133
134            // Render if requested
135            if ( $params['render'] ) {
136                $this->renderThread( $row, $params, $entry );
137            }
138
139            $ids[$row->thread_id] = $row->thread_id;
140
141            if ( $entry ) {
142                $fit = $result->addValue( [ 'query',
143                        $this->getModuleName() ],
144                    $row->thread_id, $entry );
145                if ( !$fit ) {
146                    $this->setContinueEnumParameter( 'startid', $row->thread_id );
147                    break;
148                }
149            }
150        }
151
152        $this->threadIds = $ids;
153
154        if ( isset( $prop['reactions'] ) ) {
155            $this->addSubItems(
156                'thread_reaction',
157                '*',
158                'tr_thread',
159                'reactions',
160                static function ( $row ) {
161                    return [ "{$row->tr_user}_{$row->tr_type}" => [
162                        'type' => $row->tr_type,
163                        'user-id' => $row->tr_user,
164                        'user-name' => $row->tr_user_text,
165                        'value' => $row->tr_value,
166                    ] ];
167                },
168                'reaction'
169            );
170        }
171
172        if ( isset( $prop['replies'] ) ) {
173            $this->addSubItems(
174                'thread',
175                'thread_id',
176                'thread_parent',
177                'replies',
178                static function ( $row ) {
179                    return [ $row->thread_id => [ 'id' => $row->thread_id ] ];
180                },
181                'reply'
182            );
183        }
184
185        $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'thread' );
186    }
187
188    protected function addSubItems(
189        $tableName, $fields, $joinField, $subitemName, /*callable*/ $handleRow, $tagName
190    ) {
191        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
192        $result = $this->getResult();
193
194        $fields = array_merge( (array)$fields, (array)$joinField );
195
196        $res = $dbr->newSelectQueryBuilder()
197            ->select( $fields )
198            ->from( $tableName )
199            ->where( [
200                $joinField => $this->threadIds
201            ] )
202            ->caller( __METHOD__ )
203            ->fetchResultSet();
204
205        foreach ( $res as $row ) {
206            $output = $handleRow( $row );
207
208            $path = [
209                'query',
210                $this->getModuleName(),
211                $row->$joinField,
212            ];
213
214            $result->addValue(
215                $path,
216                $subitemName,
217                $output
218            );
219
220            $result->addIndexedTagName( array_merge( $path, [ $subitemName ] ), $tagName );
221        }
222    }
223
224    /**
225     * @param stdClass $row
226     * @param array $params
227     * @param array &$entry
228     */
229    protected function renderThread( $row, $params, &$entry ) {
230        // Set up OutputPage
231        $out = $this->getOutput();
232        $oldOutputText = $out->getHTML();
233        $out->clearHTML();
234
235        // Setup
236        $thread = Thread::newFromRow( $row );
237        $article = $thread->root();
238
239        if ( !$article ) {
240            return;
241        }
242
243        $title = $article->getTitle();
244        $user = $this->getUser();
245        $request = $this->getRequest();
246        $view = new LqtView( $out, $article, $title, $user, $request );
247
248        // Parameters
249        $view->threadNestingLevel = $params['renderlevel'];
250
251        $renderpos = $params['renderthreadpos'];
252        $rendercount = $params['renderthreadcount'];
253
254        $options = [];
255        if ( isset( $params['rendermaxthreadcount'] ) ) {
256            $options['maxCount'] = $params['rendermaxthreadcount'];
257        }
258        if ( isset( $params['rendermaxdepth'] ) ) {
259            $options['maxDepth'] = $params['rendermaxdepth'];
260        }
261        if ( isset( $params['renderstartrepliesat'] ) ) {
262            $options['startAt' ] = $params['renderstartrepliesat'];
263        }
264
265        $view->showThread( $thread, $renderpos, $rendercount, $options );
266
267        $result = $out->getHTML();
268        $out->clearHTML();
269        $out->addHTML( $oldOutputText );
270
271        $entry['content'] = $result;
272    }
273
274    private static function formatProperty( $name, $fields, $row, &$entry ) {
275        if ( $fields === null ) {
276            $entry[$name] = [];
277        } elseif ( !is_array( $fields ) ) {
278            // Common case.
279            $entry[$name] = $row->$fields;
280        } elseif ( $name == 'page' ) {
281            // Special cases
282            $nsField = $fields['namespace'];
283            $tField = $fields['title'];
284            $title = Title::makeTitle( $row->$nsField, $row->$tField );
285            ApiQueryBase::addTitleInfo( $entry, $title, 'page' );
286        } else {
287            // Complicated case.
288            foreach ( $fields as $part => $field ) {
289                $entry[$name][$part] = $row->$field;
290            }
291        }
292    }
293
294    private function addPageCond( $prop, $value ) {
295        if ( count( $value ) === 1 ) {
296            $cond = $this->getPageCond( $prop, $value[0] );
297            $this->addWhere( $cond );
298        } else {
299            $conds = [];
300            $dbr = $this->getDB();
301            foreach ( $value as $page ) {
302                $conds[] = $dbr->andExpr( $this->getPageCond( $prop, $page ) );
303            }
304
305            $this->addWhere( $dbr->orExpr( $conds ) );
306        }
307    }
308
309    private function getPageCond( $prop, $value ) {
310        $fieldMappings = [
311            'page' => [
312                'namespace' => 'thread_article_namespace',
313                'title' => 'thread_article_title',
314            ],
315            'root' => [ 'id' => 'thread_root' ],
316            'summary' => [ 'id' => 'thread_summary_id' ],
317        ];
318
319        // Split.
320        $t = Title::newFromText( $value );
321        $cond = [];
322        foreach ( $fieldMappings[$prop] as $type => $field ) {
323            switch ( $type ) {
324                case 'namespace':
325                    $cond[$field] = $t->getNamespace();
326                    break;
327                case 'title':
328                    $cond[$field] = $t->getDBkey();
329                    break;
330                case 'id':
331                    $cond[$field] = $t->getArticleID();
332                    break;
333                default:
334                    ApiBase::dieDebug( __METHOD__, "Unknown condition type $type" );
335            }
336        }
337        return $cond;
338    }
339
340    private function handleCondition( $prop, $value ) {
341        $titleParams = [ 'page', 'root', 'summary' ];
342        $fields = self::$propRelations[$prop];
343
344        if ( in_array( $prop, $titleParams ) ) {
345            // Special cases
346            $this->addPageCond( $prop, $value );
347        } elseif ( $prop == 'author' ) {
348            $this->addWhereFld( 'thread_author_name', $value );
349        } elseif ( is_string( $fields ) ) {
350            // Common case
351            $this->addWhereFld( $fields, $value );
352        }
353    }
354
355    public function getCacheMode( $params ) {
356        if ( $params['render'] ) {
357            // Rendering uses the context user
358            return 'anon-public-user-private';
359        } else {
360            return 'public';
361        }
362    }
363
364    public function getAllowedParams() {
365        return [
366            'startid' => [
367                ParamValidator::PARAM_TYPE => 'integer'
368            ],
369            'endid' => [
370                ParamValidator::PARAM_TYPE => 'integer'
371            ],
372            'dir' => [
373                ParamValidator::PARAM_TYPE => [
374                    'newer',
375                    'older'
376                ],
377                ParamValidator::PARAM_DEFAULT => 'newer',
378                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
379            ],
380            'showdeleted' => false,
381            'limit' => [
382                ParamValidator::PARAM_DEFAULT => 10,
383                ParamValidator::PARAM_TYPE => 'limit',
384                IntegerDef::PARAM_MIN => 1,
385                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
386                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
387            ],
388            'prop' => [
389                ParamValidator::PARAM_DEFAULT => 'id|subject|page|parent|author',
390                ParamValidator::PARAM_TYPE => array_keys( self::$propRelations ),
391                ParamValidator::PARAM_ISMULTI => true
392            ],
393
394            'page' => [
395                ParamValidator::PARAM_ISMULTI => true
396            ],
397            'author' => [
398                ParamValidator::PARAM_ISMULTI => true
399            ],
400            'root' => [
401                ParamValidator::PARAM_ISMULTI => true
402            ],
403            'summary' => [
404                ParamValidator::PARAM_ISMULTI => true
405            ],
406            'id' => [
407                ParamValidator::PARAM_ISMULTI => true
408            ],
409            'render' => false,
410            'renderlevel' => [
411                ParamValidator::PARAM_DEFAULT => 0,
412            ],
413            'renderthreadpos' => [
414                ParamValidator::PARAM_DEFAULT => 1,
415            ],
416            'renderthreadcount' => [
417                ParamValidator::PARAM_DEFAULT => 1,
418            ],
419            'rendermaxthreadcount' => [
420                ParamValidator::PARAM_DEFAULT => null,
421            ],
422            'rendermaxdepth' => [
423                ParamValidator::PARAM_DEFAULT => null,
424            ],
425            'renderstartrepliesat' => [
426                ParamValidator::PARAM_DEFAULT => null,
427            ],
428        ];
429    }
430
431    /**
432     * @see ApiBase::getExamplesMessages()
433     * @return array
434     */
435    protected function getExamplesMessages() {
436        return [
437            'action=query&list=threads&thpage=Talk:Main_Page'
438                => 'apihelp-query+threads-example-1',
439            'action=query&list=threads&thid=1|2|3|4&thprop=id|subject|modified'
440                => 'apihelp-query+threads-example-2',
441        ];
442    }
443}