Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 573
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiThreadAction
0.00% covered (danger)
0.00%
0 / 573
0.00% covered (danger)
0.00%
0 / 21
10506
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
110
 actionMarkRead
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
 actionMarkUnread
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 actionSplit
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
90
 actionMerge
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
72
 actionNewThread
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
182
 actionEdit
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
132
 actionReply
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
110
 renderThreadPostAction
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 actionSetSubject
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
72
 actionSetSortkey
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
56
 actionAddReaction
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 actionDeleteReaction
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 actionInlineEditForm
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getActions
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 needsToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 mustBePosted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isWriteMode
0.00% covered (danger)
0.00%
0 / 1
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
3namespace MediaWiki\Extension\LiquidThreads\Api;
4
5use ApiBase;
6use ApiEditPage;
7use ApiMain;
8use Article;
9use LqtDispatch;
10use LqtView;
11use MediaWiki\Extension\LiquidThreads\Hooks;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Request\DerivativeRequest;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\Title;
16use NewMessages;
17use Thread;
18use Threads;
19
20class ApiThreadAction extends ApiEditPage {
21    public function execute() {
22        $params = $this->extractRequestParams();
23
24        $allowedAllActions = [ 'markread' ];
25        $actionsAllowedOnNonLqtPage = [ 'markread', 'markunread' ];
26        $action = $params['threadaction'];
27
28        // Pull the threads from the parameters
29        $threads = [];
30        if ( !empty( $params['thread'] ) ) {
31            $wikiPageFactory = MediaWikiServices::getInstance()->getWikiPageFactory();
32            foreach ( $params['thread'] as $thread ) {
33                $threadObj = null;
34                if ( is_numeric( $thread ) ) {
35                    $threadObj = Threads::withId( $thread );
36                } elseif ( $thread == 'all' &&
37                        in_array( $action, $allowedAllActions ) ) {
38                    $threads = [ 'all' ];
39                } else {
40                    $threadObj = Threads::withRoot(
41                        $wikiPageFactory->newFromTitle(
42                            Title::newFromText( $thread )
43                        )
44                    );
45                }
46
47                if ( $threadObj instanceof Thread ) {
48                    $threads[] = $threadObj;
49
50                    if ( !in_array( $action, $actionsAllowedOnNonLqtPage )
51                        && !LqtDispatch::isLqtPage( $threadObj->getTitle() )
52                    ) {
53                        $articleTitleDBKey = $threadObj->getTitle()->getDBkey();
54                        $this->dieWithError( [
55                            'lqt-not-a-liquidthreads-page',
56                            wfEscapeWikiText( $articleTitleDBKey )
57                        ] );
58                    }
59                }
60            }
61        }
62
63        // HACK: Somewhere $wgOut->parse() is called, which breaks
64        // if a Title isn't set. So set one. See bug 71081.
65        global $wgTitle;
66        if ( !$wgTitle instanceof Title ) {
67            $wgTitle = Title::newFromText( 'LiquidThreads has a bug' );
68        }
69
70        // Find the appropriate module
71        $actions = $this->getActions();
72
73        $method = $actions[$action];
74
75        call_user_func_array( [ $this, $method ], [ $threads, $params ] );
76    }
77
78    /**
79     * @param (Thread|string)[] $threads
80     * @param array $params
81     */
82    public function actionMarkRead( array $threads, $params ) {
83        $user = $this->getUser();
84
85        $result = [];
86
87        if ( in_array( 'all', $threads ) ) {
88            NewMessages::markAllReadByUser( $user );
89            $result[] = [
90                'result' => 'Success',
91                'action' => 'markread',
92                'threads' => 'all',
93                'unreadlink' => [
94                    'href' => SpecialPage::getTitleFor( 'NewMessages' )->getLocalURL(),
95                    'text' => $this->msg( 'lqt-newmessages-n' )->numParams( 0 )->text(),
96                    'active' => false,
97                ]
98            ];
99        } else {
100            foreach ( $threads as $t ) {
101                NewMessages::markThreadAsReadByUser( $t, $user );
102                $result[] = [
103                    'result' => 'Success',
104                    'action' => 'markread',
105                    'id' => $t->id(),
106                    'title' => $t->title()->getPrefixedText()
107                ];
108            }
109            $newMessagesCount = NewMessages::newMessageCount( $user, DB_PRIMARY );
110            $msgNewMessages = 'lqt-newmessages-n';
111            // Only bother to put this on the last threadaction
112            $result[count( $result ) - 1]['unreadlink'] = [
113                'href' => SpecialPage::getTitleFor( 'NewMessages' )->getLocalURL(),
114                'text' => $this->msg( $msgNewMessages )->numParams( $newMessagesCount )->text(),
115                'active' => $newMessagesCount > 0,
116            ];
117        }
118
119        $this->getResult()->setIndexedTagName( $result, 'thread' );
120        $this->getResult()->addValue( null, 'threadactions', $result );
121    }
122
123    /**
124     * @param Thread[] $threads
125     * @param array $params
126     */
127    public function actionMarkUnread( array $threads, $params ) {
128        $result = [];
129
130        $user = $this->getUser();
131        foreach ( $threads as $t ) {
132            NewMessages::markThreadAsUnreadByUser( $t, $user );
133
134            $result[] = [
135                'result' => 'Success',
136                'action' => 'markunread',
137                'id' => $t->id(),
138                'title' => $t->title()->getPrefixedText()
139            ];
140        }
141
142        $this->getResult()->setIndexedTagName( $result, 'thread' );
143        $this->getResult()->addValue( null, 'threadaction', $result );
144    }
145
146    /**
147     * @param Thread[] $threads
148     * @param array $params
149     */
150    public function actionSplit( array $threads, $params ) {
151        if ( count( $threads ) > 1 ) {
152            $this->dieWithError( 'apierror-liquidthreads-onlyone', 'too-many-threads' );
153        } elseif ( count( $threads ) < 1 ) {
154            $this->dieWithError(
155                'apierror-liquidthreads-threadneeded', 'no-specified-threads' );
156        }
157
158        $thread = array_pop( $threads );
159
160        $status = $this->getPermissionManager()
161            ->getPermissionStatus( 'lqt-split', $this->getUser(), $thread->title() );
162        if ( !$status->isGood() ) {
163            $this->dieStatus( $status );
164        }
165
166        if ( $thread->isTopmostThread() ) {
167            $this->dieWithError( 'apierror-liquidthreads-alreadytop', 'already-top-level' );
168        }
169
170        $title = null;
171        $article = $thread->article();
172        if ( empty( $params['subject'] ) ||
173            !Thread::validateSubject(
174                $params['subject'],
175                $this->getUser(),
176                $title,
177                null,
178                $article
179            )
180        ) {
181            $this->dieWithError( 'apierror-liquidthreads-nosubject', 'no-valid-subject' );
182        }
183
184        $subject = $params['subject'];
185
186        // Pull a reason, if applicable.
187        $reason = '';
188        if ( !empty( $params['reason'] ) ) {
189            $reason = $params['reason'];
190        }
191
192        // Check if they specified a sortkey
193        $sortkey = null;
194        if ( !empty( $params['sortkey'] ) ) {
195            $ts = $params['sortkey'];
196            $ts = wfTimestamp( TS_MW, $ts );
197
198            $sortkey = $ts;
199        }
200
201        // Do the split
202        $thread->split( $subject, $reason, $sortkey );
203
204        $result = [];
205        $result[] = [
206            'result' => 'Success',
207            'action' => 'split',
208            'id' => $thread->id(),
209            'title' => $thread->title()->getPrefixedText(),
210            'newsubject' => $subject,
211        ];
212
213        $this->getResult()->setIndexedTagName( $result, 'thread' );
214        $this->getResult()->addValue( null, 'threadaction', $result );
215    }
216
217    /**
218     * @param Thread[] $threads
219     * @param array $params
220     */
221    public function actionMerge( array $threads, $params ) {
222        if ( count( $threads ) < 1 ) {
223            $this->dieWithError( 'apihelp-liquidthreads-threadneeded', 'no-specified-threads' );
224        }
225
226        if ( empty( $params['newparent'] ) ) {
227            $this->dieWithError( 'apierror-liquidthreads-noparent', 'no-parent-thread' );
228        }
229
230        $newParent = $params['newparent'];
231        if ( is_numeric( $newParent ) ) {
232            $newParent = Threads::withId( $newParent );
233        } else {
234            $newParent = Threads::withRoot(
235                MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle(
236                    Title::newFromText( $newParent )
237                )
238            );
239        }
240
241        if ( !$newParent ) {
242            $this->dieWithError( 'apierror-liquidthreads-badparent', 'invalid-parent-thread' );
243        }
244
245        $status = $this->getPermissionManager()
246            ->getPermissionStatus( 'lqt-merge', $this->getUser(), $newParent->title() );
247        if ( !$status->isGood() ) {
248            $this->dieStatus( $status );
249        }
250
251        // Pull a reason, if applicable.
252        $reason = '';
253        if ( !empty( $params['reason'] ) ) {
254            $reason = $params['reason'];
255        }
256
257        $result = [];
258
259        foreach ( $threads as $thread ) {
260            $thread->moveToParent( $newParent, $reason );
261            $result[] = [
262                'result' => 'Success',
263                'action' => 'merge',
264                'id' => $thread->id(),
265                'title' => $thread->title()->getPrefixedText(),
266                'new-parent-id' => $newParent->id(),
267                'new-parent-title' => $newParent->title()->getPrefixedText(),
268                'new-ancestor-id' => $newParent->topmostThread()->id(),
269                'new-ancestor-title' => $newParent->topmostThread()->title()->getPrefixedText(),
270            ];
271        }
272
273        $this->getResult()->setIndexedTagName( $result, 'thread' );
274        $this->getResult()->addValue( null, 'threadaction', $result );
275    }
276
277    /**
278     * @param Thread[] $threads
279     * @param array $params
280     */
281    public function actionNewThread( $threads, $params ) {
282        // T206901: Validate talkpage parameter
283        if ( $params['talkpage'] === null ) {
284            $this->dieWithError( [ 'apierror-missingparam', 'talkpage' ] );
285        }
286
287        $talkpageTitle = Title::newFromText( $params['talkpage'] );
288
289        if ( !$talkpageTitle || !LqtDispatch::isLqtPage( $talkpageTitle ) ) {
290            $this->dieWithError( 'apierror-liquidthreads-invalidtalkpage', 'invalid-talkpage' );
291        }
292        $talkpage = new Article( $talkpageTitle, 0 );
293
294        // Check if we can post.
295        $user = $this->getUser();
296        if ( Thread::canUserPost( $user, $talkpage ) !== true ) {
297            $this->dieWithError(
298                'apierror-liquidthreads-talkpageprotected', 'talkpage-protected' );
299        }
300
301        // Validate subject, generate a title
302        if ( empty( $params['subject'] ) ) {
303            $this->dieWithError( [ 'apierror-missingparam', 'subject' ] );
304        }
305
306        $subject = $params['subject'];
307        $title = null;
308        $subjectOk = Thread::validateSubject( $subject, $user, $title, null, $talkpage );
309
310        if ( !$subjectOk ) {
311            $this->dieWithError( 'apierror-liquidthreads-badsubject', 'invalid-subject' );
312        }
313        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
314        $article = new Article( $title, 0 );
315
316        // Check for text
317        if ( empty( $params['text'] ) ) {
318            $this->dieWithError( 'apierror-liquidthreads-notext', 'no-text' );
319        }
320        $text = $params['text'];
321
322        // Generate or pull summary
323        $summary = $this->msg( 'lqt-newpost-summary', $subject )->inContentLanguage()->text();
324        if ( !empty( $params['reason'] ) ) {
325            $summary = $params['reason'];
326        }
327
328        $signature = null;
329        if ( isset( $params['signature'] ) ) {
330            $signature = $params['signature'];
331        }
332
333        // Inform hooks what we're doing
334        Hooks::$editTalkpage = $talkpage;
335        Hooks::$editArticle = $article;
336        Hooks::$editThread = null;
337        Hooks::$editType = 'new';
338        Hooks::$editAppliesTo = null;
339
340        $token = $params['token'];
341
342        // All seems in order. Construct an API edit request
343        $requestData = [
344            'action' => 'edit',
345            'title' => $title->getPrefixedText(),
346            'text' => $text,
347            'summary' => $summary,
348            'token' => $token,
349            'basetimestamp' => wfTimestampNow(),
350            'minor' => 0,
351            'format' => 'json',
352        ];
353
354        if ( $user->isAllowed( 'bot' ) ) {
355            $requestData['bot'] = true;
356        }
357        $editReq = new DerivativeRequest( $this->getRequest(), $requestData, true );
358        $internalApi = new ApiMain( $editReq, true );
359        $internalApi->execute();
360
361        $editResult = $internalApi->getResult()->getResultData();
362
363        if ( $editResult['edit']['result'] != 'Success' ) {
364            $result = [ 'result' => 'EditFailure', 'details' => $editResult ];
365            $this->getResult()->addValue( null, $this->getModuleName(), $result );
366            return;
367        }
368
369        $articleId = $editResult['edit']['pageid'];
370
371        $article->getTitle()->resetArticleID( $articleId );
372        $title->resetArticleID( $articleId );
373
374        $thread = LqtView::newPostMetadataUpdates(
375            $user,
376            [
377                'root' => $article,
378                'talkpage' => $talkpage,
379                'subject' => $subject,
380                'signature' => $signature,
381                'summary' => $summary,
382                'text' => $text,
383            ] );
384
385        $result = [
386            'result' => 'Success',
387            'thread-id' => $thread->id(),
388            'thread-title' => $title->getPrefixedText(),
389            'modified' => $thread->modified(),
390        ];
391
392        if ( !empty( $params['render'] ) ) {
393            $result['html'] = $this->renderThreadPostAction( $thread );
394        }
395
396        $result = [ 'thread' => $result ];
397
398        $this->getResult()->addValue( null, $this->getModuleName(), $result );
399    }
400
401    /**
402     * @param Thread[] $threads
403     * @param array $params
404     */
405    public function actionEdit( array $threads, $params ) {
406        if ( count( $threads ) > 1 ) {
407            $this->dieWithError( 'apierror-liquidthreads-onlyone', 'too-many-threads' );
408        } elseif ( count( $threads ) < 1 ) {
409            $this->dieWithError(
410                'apierror-liquidthreads-threadneeded', 'no-specified-threads' );
411        }
412
413        $thread = array_pop( $threads );
414        $talkpage = $thread->article();
415
416        $bump = $params['bump'] ?? null;
417
418        // Validate subject
419        $subjectOk = true;
420        if ( !empty( $params['subject'] ) ) {
421            $subject = $params['subject'];
422            $title = null;
423            $subjectOk = Thread::validateSubject(
424                $subject,
425                $this->getUser(),
426                $title,
427                null,
428                $talkpage
429            );
430        } else {
431            $subject = $thread->subject();
432        }
433
434        if ( !$subjectOk ) {
435            $this->dieWithError( 'apierror-liquidthreads-badsubject', 'invalid-subject' );
436        }
437
438        // Check for text
439        if ( empty( $params['text'] ) ) {
440            $this->dieWithError( 'apierror-liquidthreads-notext', 'no-text' );
441        }
442        $text = $params['text'];
443
444        $summary = '';
445        if ( !empty( $params['reason'] ) ) {
446            $summary = $params['reason'];
447        }
448
449        $article = $thread->root();
450        $title = $article->getTitle();
451
452        $signature = null;
453        if ( isset( $params['signature'] ) ) {
454            $signature = $params['signature'];
455        }
456
457        // Inform hooks what we're doing
458        Hooks::$editTalkpage = $talkpage;
459        Hooks::$editArticle = $article;
460        Hooks::$editThread = $thread;
461        Hooks::$editType = 'edit';
462        Hooks::$editAppliesTo = null;
463
464        $token = $params['token'];
465
466        // All seems in order. Construct an API edit request
467        $requestData = [
468            'action' => 'edit',
469            'title' => $title->getPrefixedText(),
470            'text' => $text,
471            'summary' => $summary,
472            'token' => $token,
473            'minor' => 0,
474            'basetimestamp' => wfTimestampNow(),
475            'format' => 'json',
476        ];
477
478        if ( $this->getUser()->isAllowed( 'bot' ) ) {
479            $requestData['bot'] = true;
480        }
481
482        $editReq = new DerivativeRequest( $this->getRequest(), $requestData, true );
483        $internalApi = new ApiMain( $editReq, true );
484        $internalApi->execute();
485
486        $editResult = $internalApi->getResult()->getResultData();
487
488        if ( $editResult['edit']['result'] != 'Success' ) {
489            $result = [ 'result' => 'EditFailure', 'details' => $editResult ];
490            $this->getResult()->addValue( null, $this->getModuleName(), $result );
491            return;
492        }
493
494        $thread = LqtView::editMetadataUpdates(
495            [
496                'root' => $article,
497                'thread' => $thread,
498                'subject' => $subject,
499                'signature' => $signature,
500                'summary' => $summary,
501                'text' => $text,
502                'bump' => $bump,
503            ] );
504
505        $result = [
506            'result' => 'Success',
507            'thread-id' => $thread->id(),
508            'thread-title' => $title->getPrefixedText(),
509            'modified' => $thread->modified(),
510        ];
511
512        if ( !empty( $params['render'] ) ) {
513            $result['html'] = $this->renderThreadPostAction( $thread );
514        }
515
516        $result = [ 'thread' => $result ];
517
518        $this->getResult()->addValue( null, $this->getModuleName(), $result );
519    }
520
521    /**
522     * @param Thread[] $threads
523     * @param array $params
524     */
525    public function actionReply( array $threads, $params ) {
526        // Validate thread parameter
527        if ( count( $threads ) > 1 ) {
528            $this->dieWithError( 'apierror-liquidthreads-onlyone', 'too-many-threads' );
529        } elseif ( count( $threads ) < 1 ) {
530            $this->dieWithError(
531                'apierror-liquidthreads-threadneeded', 'no-specified-threads' );
532        }
533        $replyTo = array_pop( $threads );
534
535        // Check if we can reply to that thread.
536        $user = $this->getUser();
537        $perm_result = $replyTo->canUserReply( $user );
538        if ( $perm_result !== true ) {
539            // Messages: apierror-liquidthreads-noreplies-talkpage,
540            // apierror-liquidthreads-noreplies-thread
541            $this->dieWithError(
542                "apierror-liquidthreads-noreplies-{$perm_result}", "{$perm_result}-protected"
543            );
544        }
545
546        // Validate text parameter
547        if ( empty( $params['text'] ) ) {
548            $this->dieWithError( 'apierror-liquidthreads-notext', 'no-text' );
549        }
550
551        $text = $params['text'];
552
553        $bump = $params['bump'] ?? null;
554
555        // Generate/pull summary
556        $summary = $this->msg( 'lqt-reply-summary', $replyTo->subject(),
557                $replyTo->title()->getPrefixedText() )->inContentLanguage()->text();
558
559        if ( !empty( $params['reason'] ) ) {
560            $summary = $params['reason'];
561        }
562
563        $signature = null;
564        if ( isset( $params['signature'] ) ) {
565            $signature = $params['signature'];
566        }
567
568        // Grab data from parent
569        $talkpage = $replyTo->article();
570
571        // Generate a reply title.
572        $title = Threads::newReplyTitle( $replyTo, $user );
573        $article = new Article( $title, 0 );
574
575        // Inform hooks what we're doing
576        Hooks::$editTalkpage = $talkpage;
577        Hooks::$editArticle = $article;
578        Hooks::$editThread = null;
579        Hooks::$editType = 'reply';
580        Hooks::$editAppliesTo = $replyTo;
581
582        // Pull token in
583        $token = $params['token'];
584
585        // All seems in order. Construct an API edit request
586        $requestData = [
587            'action' => 'edit',
588            'title' => $title->getPrefixedText(),
589            'text' => $text,
590            'summary' => $summary,
591            'token' => $token,
592            'basetimestamp' => wfTimestampNow(),
593            'minor' => 0,
594            'format' => 'json',
595        ];
596
597        if ( $user->isAllowed( 'bot' ) ) {
598            $requestData['bot'] = true;
599        }
600
601        $editReq = new DerivativeRequest( $this->getRequest(), $requestData, true );
602        $internalApi = new ApiMain( $editReq, true );
603        $internalApi->execute();
604
605        $editResult = $internalApi->getResult()->getResultData();
606
607        if ( $editResult['edit']['result'] != 'Success' ) {
608            $result = [ 'result' => 'EditFailure', 'details' => $editResult ];
609            $this->getResult()->addValue( null, $this->getModuleName(), $result );
610            return;
611        }
612
613        $articleId = $editResult['edit']['pageid'];
614        $article->getTitle()->resetArticleID( $articleId );
615        $title->resetArticleID( $articleId );
616
617        $thread = LqtView::replyMetadataUpdates(
618            $user,
619            [
620                'root' => $article,
621                'replyTo' => $replyTo,
622                'signature' => $signature,
623                'summary' => $summary,
624                'text' => $text,
625                'bump' => $bump,
626            ] );
627
628        $result = [
629            'action' => 'reply',
630            'result' => 'Success',
631            'thread-id' => $thread->id(),
632            'thread-title' => $title->getPrefixedText(),
633            'parent-id' => $replyTo->id(),
634            'parent-title' => $replyTo->title()->getPrefixedText(),
635            'ancestor-id' => $replyTo->topmostThread()->id(),
636            'ancestor-title' => $replyTo->topmostThread()->title()->getPrefixedText(),
637            'modified' => $thread->modified(),
638        ];
639
640        if ( !empty( $params['render'] ) ) {
641            $result['html'] = $this->renderThreadPostAction( $thread );
642        }
643
644        $result = [ 'thread' => $result ];
645
646        $this->getResult()->addValue( null, $this->getModuleName(), $result );
647    }
648
649    /**
650     * @param Thread $thread
651     * @return string
652     */
653    protected function renderThreadPostAction( Thread $thread ) {
654        $thread = $thread->topmostThread();
655
656        // Set up OutputPage
657        $out = $this->getOutput();
658        $oldOutputText = $out->getHTML();
659        $out->clearHTML();
660
661        // Setup
662        $article = $thread->root();
663        $title = $article->getTitle();
664        $user = $this->getUser();
665        $request = $this->getRequest();
666        $view = new LqtView( $out, $article, $title, $user, $request );
667
668        $view->showThread( $thread );
669
670        $result = $out->getHTML();
671        $out->clearHTML();
672        $out->addHTML( $oldOutputText );
673
674        return $result;
675    }
676
677    /**
678     * @param Thread[] $threads
679     * @param array $params
680     */
681    public function actionSetSubject( array $threads, $params ) {
682        // Validate thread parameter
683        if ( count( $threads ) > 1 ) {
684            $this->dieWithError( 'apierror-liquidthreads-onlyone', 'too-many-threads' );
685        } elseif ( count( $threads ) < 1 ) {
686            $this->dieWithError(
687                'apierror-liquidthreads-threadneeded', 'no-specified-threads' );
688        }
689        $thread = array_pop( $threads );
690
691        $status = $this->getPermissionManager()
692            ->getPermissionStatus( 'edit', $this->getUser(), $thread->title() );
693        if ( !$status->isGood() ) {
694            $this->dieStatus( $status );
695        }
696
697        // Validate subject
698        if ( empty( $params['subject'] ) ) {
699            $this->dieWithError( [ 'apierror-missingparam', 'subject' ] );
700        }
701
702        $talkpage = $thread->article();
703
704        $subject = $params['subject'];
705        $title = null;
706        $subjectOk = Thread::validateSubject(
707            $subject,
708            $this->getUser(),
709            $title,
710            null,
711            $talkpage
712        );
713
714        if ( !$subjectOk ) {
715            $this->dieWithError( 'apierror-liquidthreads-badsubject', 'invalid-subject' );
716        }
717
718        $reason = null;
719
720        if ( isset( $params['reason'] ) ) {
721            $reason = $params['reason'];
722        }
723
724        if ( $thread->dbVersion->subject() !== $subject ) {
725            $thread->setSubject( $subject );
726            $thread->commitRevision(
727                Threads::CHANGE_EDITED_SUBJECT,
728                $this->getUser(),
729                $thread,
730                $reason
731            );
732        }
733
734        $result = [
735            'action' => 'setsubject',
736            'result' => 'success',
737            'thread-id' => $thread->id(),
738            'thread-title' => $thread->title()->getPrefixedText(),
739            'new-subject' => $subject,
740        ];
741
742        $result = [ 'thread' => $result ];
743
744        $this->getResult()->addValue( null, $this->getModuleName(), $result );
745    }
746
747    /**
748     * @param Thread[] $threads
749     * @param array $params
750     */
751    public function actionSetSortkey( array $threads, $params ) {
752        // First check for threads
753        if ( !count( $threads ) ) {
754            $this->dieWithError( 'apihelp-liquidthreads-threadneeded', 'no-specified-threads' );
755        }
756
757        // Validate timestamp
758        if ( empty( $params['sortkey'] ) ) {
759            $this->dieWithError( 'apierror-liquidthreads-badsortkey', 'invalid-sortkey' );
760        }
761
762        $ts = $params['sortkey'];
763
764        if ( $ts == 'now' ) {
765            $ts = wfTimestampNow();
766        }
767
768        $ts = wfTimestamp( TS_MW, $ts );
769
770        if ( !$ts ) {
771            $this->dieWithError( 'apierror-liquidthreads-badsortkey', 'invalid-sortkey' );
772        }
773
774        $reason = null;
775
776        if ( isset( $params['reason'] ) ) {
777            $reason = $params['reason'];
778        }
779
780        $thread = array_pop( $threads );
781
782        $status = $this->getPermissionManager()
783            ->getPermissionStatus( 'edit', $this->getUser(), $thread->title() );
784        if ( !$status->isGood() ) {
785            $this->dieStatus( $status );
786        }
787
788        $thread->setSortkey( $ts );
789        $thread->commitRevision(
790            Threads::CHANGE_ADJUSTED_SORTKEY,
791            $this->getUser(),
792            null,
793            $reason
794        );
795
796        $result = [
797            'action' => 'setsortkey',
798            'result' => 'success',
799            'thread-id' => $thread->id(),
800            'thread-title' => $thread->title()->getPrefixedText(),
801            'new-sortkey' => $ts,
802        ];
803
804        $result = [ 'thread' => $result ];
805
806        $this->getResult()->addValue( null, $this->getModuleName(), $result );
807    }
808
809    /**
810     * @param Thread[] $threads
811     * @param array $params
812     */
813    public function actionAddReaction( array $threads, $params ) {
814        if ( !count( $threads ) ) {
815            $this->dieWithError( 'apihelp-liquidthreads-threadneeded', 'no-specified-threads' );
816        }
817
818        $this->checkUserRightsAny( 'lqt-react' );
819
820        $required = [ 'type', 'value' ];
821
822        if ( count( array_diff( $required, array_keys( $params ) ) ) ) {
823            $this->dieWithError( 'apierror-liquidthreads-badreaction', 'missing-parameter' );
824        }
825
826        $result = [];
827
828        foreach ( $threads as $thread ) {
829            $thread->addReaction( $this->getUser(), $params['type'], $params['value'] );
830
831            $result[] = [
832                'result' => 'Success',
833                'action' => 'addreaction',
834                'id' => $thread->id(),
835            ];
836        }
837
838        $this->getResult()->setIndexedTagName( $result, 'thread' );
839        $this->getResult()->addValue( null, 'threadaction', $result );
840    }
841
842    /**
843     * @param Thread[] $threads
844     * @param array $params
845     */
846    public function actionDeleteReaction( array $threads, $params ) {
847        if ( !count( $threads ) ) {
848            $this->dieWithError( 'apihelp-liquidthreads-threadneeded', 'no-specified-threads' );
849        }
850
851        $user = $this->getUser();
852        $this->checkUserRightsAny( 'lqt-react' );
853
854        $required = [ 'type', 'value' ];
855
856        if ( count( array_diff( $required, array_keys( $params ) ) ) ) {
857            $this->dieWithError( 'apierror-liquidthreads-badreaction', 'missing-parameter' );
858        }
859
860        $result = [];
861
862        foreach ( $threads as $thread ) {
863            $thread->deleteReaction( $user, $params['type'] );
864
865            $result[] = [
866                'result' => 'Success',
867                'action' => 'deletereaction',
868                'id' => $thread->id(),
869            ];
870        }
871
872        $this->getResult()->setIndexedTagName( $result, 'thread' );
873        $this->getResult()->addValue( null, 'threadaction', $result );
874    }
875
876    /**
877     * @param Thread[] $threads
878     * @param array $params
879     */
880    public function actionInlineEditForm( array $threads, $params ) {
881        $method = $talkpage = $operand = null;
882
883        if ( isset( $params['method'] ) ) {
884            $method = $params['method'];
885        }
886
887        if ( isset( $params['talkpage'] ) ) {
888            $talkpage = $params['talkpage'];
889        }
890
891        if ( $talkpage ) {
892            $talkpage = new Article( Title::newFromText( $talkpage ), 0 );
893        } else {
894            $talkpage = null;
895        }
896
897        if ( count( $threads ) ) {
898            $operand = $threads[0];
899            $operand = $operand->id();
900        }
901
902        $output = LqtView::getInlineEditForm( $talkpage, $method, $operand, $this->getUser() );
903
904        $result = [ 'inlineeditform' => [ 'html' => $output ] ];
905
906        /* FIXME
907        $result['resources'] = LqtView::getJSandCSS();
908        $result['resources']['messages'] = LqtView::exportJSLocalisation();
909        */
910
911        $this->getResult()->addValue( null, 'threadaction', $result );
912    }
913
914    public function getActions() {
915        return [
916            'markread' => 'actionMarkRead',
917            'markunread' => 'actionMarkUnread',
918            'split' => 'actionSplit',
919            'merge' => 'actionMerge',
920            'reply' => 'actionReply',
921            'newthread' => 'actionNewThread',
922            'setsubject' => 'actionSetSubject',
923            'setsortkey' => 'actionSetSortkey',
924            'edit' => 'actionEdit',
925            'addreaction' => 'actionAddReaction',
926            'deletereaction' => 'actionDeleteReaction',
927            'inlineeditform' => 'actionInlineEditForm',
928        ];
929    }
930
931    /**
932     * @see ApiBase::getExamplesMessages()
933     * @return array
934     */
935    protected function getExamplesMessages() {
936        return [
937        ];
938    }
939
940    public function needsToken() {
941        return 'csrf';
942    }
943
944    public function getAllowedParams() {
945        return [
946            'thread' => [
947                ApiBase::PARAM_ISMULTI => true,
948            ],
949            'talkpage' => null,
950            'threadaction' => [
951                ApiBase::PARAM_REQUIRED => true,
952                ApiBase::PARAM_TYPE => array_keys( $this->getActions() ),
953            ],
954            'token' => null,
955            'subject' => null,
956            'reason' => null,
957            'newparent' => null,
958            'text' => null,
959            'render' => null,
960            'bump' => null,
961            'sortkey' => null,
962            'signature' => null,
963            'type' => null,
964            'value' => null,
965            'method' => null,
966            'operand' => null,
967        ];
968    }
969
970    public function mustBePosted() {
971        return true;
972    }
973
974    public function isWriteMode() {
975        return true;
976    }
977
978    public function getHelpUrls() {
979        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Threadaction';
980    }
981}