Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 249 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryLQTThreads | |
0.00% |
0 / 249 |
|
0.00% |
0 / 11 |
2070 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 77 |
|
0.00% |
0 / 1 |
240 | |||
addSubItems | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
renderThread | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
formatProperty | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
addPageCond | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getPageCond | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
42 | |||
handleCondition | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getCacheMode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getAllowedParams | |
0.00% |
0 / 63 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\LiquidThreads\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiQueryBase; |
7 | use ApiResult; |
8 | use LqtView; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\Title\Title; |
11 | use stdClass; |
12 | use Thread; |
13 | use Threads; |
14 | use Wikimedia\ParamValidator\ParamValidator; |
15 | use 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 | |
36 | class 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 | } |