Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 573 |
|
0.00% |
0 / 21 |
CRAP | |
0.00% |
0 / 1 |
ApiThreadAction | |
0.00% |
0 / 573 |
|
0.00% |
0 / 21 |
10506 | |
0.00% |
0 / 1 |
execute | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
110 | |||
actionMarkRead | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
12 | |||
actionMarkUnread | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
actionSplit | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
90 | |||
actionMerge | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
actionNewThread | |
0.00% |
0 / 76 |
|
0.00% |
0 / 1 |
182 | |||
actionEdit | |
0.00% |
0 / 79 |
|
0.00% |
0 / 1 |
132 | |||
actionReply | |
0.00% |
0 / 80 |
|
0.00% |
0 / 1 |
110 | |||
renderThreadPostAction | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
actionSetSubject | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
72 | |||
actionSetSortkey | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
56 | |||
actionAddReaction | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
actionDeleteReaction | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
actionInlineEditForm | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getActions | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
needsToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAllowedParams | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
2 | |||
mustBePosted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\LiquidThreads\Api; |
4 | |
5 | use ApiBase; |
6 | use ApiEditPage; |
7 | use ApiMain; |
8 | use Article; |
9 | use LqtDispatch; |
10 | use LqtView; |
11 | use MediaWiki\Extension\LiquidThreads\Hooks; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Request\DerivativeRequest; |
14 | use MediaWiki\SpecialPage\SpecialPage; |
15 | use MediaWiki\Title\Title; |
16 | use NewMessages; |
17 | use Thread; |
18 | use Threads; |
19 | |
20 | class 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 | } |