Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 1322 |
|
0.00% |
0 / 70 |
CRAP | |
0.00% |
0 / 1 |
LqtView | |
0.00% |
0 / 1322 |
|
0.00% |
0 / 70 |
75350 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
methodAppliesToThread | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
methodApplies | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
permalinkUrl | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
permalinkData | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
permalinkUrlWithQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
permalink | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
linkInContextData | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
linkInContext | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
linkInContextFullURL | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
linkInContextCanonicalURL | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
diffQuery | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
diffPermalinkURL | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
diffPermalink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
talkpageLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
talkpageLinkData | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
90 | |||
talkpageUrl | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
perpetuate | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
showReplyProtectedNotice | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
doInlineEditForm | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
30 | |||
fixFauxRequestSession | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getInlineEditForm | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
showNewThreadForm | |
0.00% |
0 / 79 |
|
0.00% |
0 / 1 |
156 | |||
showReplyForm | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
90 | |||
showPostEditingForm | |
0.00% |
0 / 77 |
|
0.00% |
0 / 1 |
132 | |||
showSummarizeForm | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
56 | |||
checkNonce | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
consumeNonce | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getSubjectEditor | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getSignatureEditor | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
replyMetadataUpdates | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
summarizeMetadataUpdates | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
editMetadataUpdates | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
newPostMetadataUpdates | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
newThreadTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newSummaryTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newReplyTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
scratchTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
threadCommands | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
110 | |||
threadMajorCommands | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
56 | |||
topLevelThreadCommands | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
72 | |||
showPostBody | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
showThreadToolbar | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
12 | |||
listItemsForCommands | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
contentForCommand | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
showThreadBody | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
72 | |||
threadSignature | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
2 | |||
threadInfoPanel | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
42 | |||
showThreadHeading | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
postDivClass | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
anchorName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showMovedThread | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
showDeletedThread | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
showSingleThread | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
getMustShowThreads | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
42 | |||
getShowMore | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getShowReplies | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
threadContainsRepliesWithContent | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
72 | |||
showThreadReplies | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
156 | |||
showThread | |
0.00% |
0 / 142 |
|
0.00% |
0 / 1 |
992 | |||
showReplyBox | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
threadDivClass | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getSummary | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
12 | |||
getSignature | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getUserSignature | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
parseSignature | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
signaturePST | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
customizeNavigation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
show | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatSubject | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * @file |
4 | * @ingroup LiquidThreads |
5 | * @author David McCabe <davemccabe@gmail.com> |
6 | * @license GPL-2.0-or-later |
7 | */ |
8 | |
9 | use MediaWiki\Context\RequestContext; |
10 | use MediaWiki\EditPage\EditPage; |
11 | use MediaWiki\Extension\LiquidThreads\Hooks; |
12 | use MediaWiki\Html\Html; |
13 | use MediaWiki\Linker\Linker; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Output\OutputPage; |
16 | use MediaWiki\Parser\Parser; |
17 | use MediaWiki\Parser\Sanitizer; |
18 | use MediaWiki\Request\FauxRequest; |
19 | use MediaWiki\Request\WebRequest; |
20 | use MediaWiki\SpecialPage\SpecialPage; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use Wikimedia\IPUtils; |
24 | |
25 | class LqtView { |
26 | /** |
27 | * @var Article|null |
28 | */ |
29 | public $article; |
30 | |
31 | /** |
32 | * @var OutputPage |
33 | */ |
34 | public $output; |
35 | |
36 | /** |
37 | * @var User |
38 | */ |
39 | public $user; |
40 | |
41 | /** |
42 | * @var Title|null |
43 | */ |
44 | public $title; |
45 | |
46 | /** |
47 | * @var WebRequest |
48 | */ |
49 | public $request; |
50 | |
51 | /** @var int h1, h2, h3, etc. */ |
52 | protected $headerLevel = 2; |
53 | /** @var array */ |
54 | protected $user_colors; |
55 | /** @var int */ |
56 | protected $user_color_index; |
57 | |
58 | /** @var int */ |
59 | public $threadNestingLevel = 0; |
60 | |
61 | public function __construct( $output, $article, $title, $user, $request ) { |
62 | $this->article = $article; |
63 | $this->output = $output; |
64 | $this->user = $user; |
65 | $this->title = $title; |
66 | $this->request = $request; |
67 | $this->user_colors = []; |
68 | $this->user_color_index = 1; |
69 | } |
70 | |
71 | /************************* |
72 | * (1) linking to liquidthreads pages and |
73 | * (2) figuring out what page you're on and what you need to do. |
74 | */ |
75 | |
76 | /** |
77 | * @param string $method |
78 | * @param Thread $thread |
79 | * @return bool |
80 | */ |
81 | public function methodAppliesToThread( $method, Thread $thread ) { |
82 | return $this->request->getVal( 'lqt_method' ) == $method && |
83 | $this->request->getVal( 'lqt_operand' ) == $thread->id(); |
84 | } |
85 | |
86 | /** |
87 | * @param string $method |
88 | * @return bool |
89 | */ |
90 | public function methodApplies( $method ) { |
91 | return $this->request->getVal( 'lqt_method' ) == $method; |
92 | } |
93 | |
94 | public static function permalinkUrl( |
95 | Thread $thread, |
96 | $method = null, |
97 | $operand = null, |
98 | array $uquery = [], |
99 | $relative = true |
100 | ) { |
101 | [ $title, $query ] = self::permalinkData( $thread, $method, $operand ); |
102 | |
103 | $query = array_merge( $query, $uquery ); |
104 | |
105 | $queryString = wfArrayToCgi( $query ); |
106 | |
107 | if ( $relative ) { |
108 | return $title->getLocalUrl( $queryString ); |
109 | } else { |
110 | return $title->getCanonicalUrl( $queryString ); |
111 | } |
112 | } |
113 | |
114 | /** |
115 | * Gets an array of (title, query-parameters) for a permalink |
116 | * @param Thread $thread |
117 | * @param string|null $method |
118 | * @param string|null $operand |
119 | * @return array |
120 | */ |
121 | public static function permalinkData( Thread $thread, $method = null, $operand = null ) { |
122 | $query = []; |
123 | |
124 | if ( $method ) { |
125 | $query['lqt_method'] = $method; |
126 | } |
127 | if ( $operand ) { |
128 | $query['lqt_operand'] = $operand; |
129 | } |
130 | |
131 | $root = $thread->root(); |
132 | if ( !$root ) { |
133 | // XXX Perhaps this should be replaced with a checked exception |
134 | throw new RuntimeException( "No root in " . __METHOD__ ); |
135 | } |
136 | |
137 | return [ $root->getTitle(), $query ]; |
138 | } |
139 | |
140 | /** |
141 | * This is used for action=history so that the history tab works, which is |
142 | * why we break the lqt_method paradigm. |
143 | * |
144 | * @param Thread $thread |
145 | * @param string[] $query |
146 | * @param bool $relative |
147 | * @return string |
148 | */ |
149 | public static function permalinkUrlWithQuery( Thread $thread, array $query, $relative = true ) { |
150 | return self::permalinkUrl( $thread, null, null, $query, $relative ); |
151 | } |
152 | |
153 | public static function permalink( Thread $thread, $text = null, $method = null, $operand = null, |
154 | $linker = null, $attribs = [], $uquery = [] ) { |
155 | [ $title, $query ] = self::permalinkData( $thread, $method, $operand ); |
156 | |
157 | $query = array_merge( $query, $uquery ); |
158 | |
159 | return MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
160 | $title, |
161 | $text, |
162 | $attribs, |
163 | $query |
164 | ); |
165 | } |
166 | |
167 | /** |
168 | * @param Thread $thread |
169 | * @param string $contextType |
170 | * @throws Exception |
171 | * @return array |
172 | */ |
173 | public static function linkInContextData( Thread $thread, $contextType = 'page' ) { |
174 | $query = []; |
175 | |
176 | if ( $contextType == 'page' ) { |
177 | $title = clone $thread->getTitle(); |
178 | |
179 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
180 | $offset = $thread->topmostThread()->sortkey(); |
181 | $offset = (int)wfTimestamp( TS_UNIX, $offset ) + 1; |
182 | $offset = $dbr->timestamp( $offset ); |
183 | $query['offset'] = $offset; |
184 | } else { |
185 | $title = clone $thread->title(); |
186 | } |
187 | |
188 | $query['lqt_mustshow'] = $thread->id(); |
189 | |
190 | $title->setFragment( '#' . $thread->getAnchorName() ); |
191 | |
192 | return [ $title, $query ]; |
193 | } |
194 | |
195 | /** |
196 | * @param thread $thread |
197 | * @param string $contextType |
198 | * @param string|null $text |
199 | * @return mixed |
200 | */ |
201 | public static function linkInContext( Thread $thread, $contextType = 'page', $text = null ) { |
202 | [ $title, $query ] = self::linkInContextData( $thread, $contextType ); |
203 | |
204 | if ( $text === null ) { |
205 | $text = Threads::stripHTML( $thread->formattedSubject() ); |
206 | } |
207 | |
208 | return MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
209 | $title, |
210 | $text, |
211 | [], |
212 | $query |
213 | ); |
214 | } |
215 | |
216 | public static function linkInContextFullURL( Thread $thread, $contextType = 'page' ) { |
217 | [ $title, $query ] = self::linkInContextData( $thread, $contextType ); |
218 | |
219 | return $title->getFullURL( $query ); |
220 | } |
221 | |
222 | public static function linkInContextCanonicalURL( Thread $thread, $contextType = 'page' ) { |
223 | [ $title, $query ] = self::linkInContextData( $thread, $contextType ); |
224 | |
225 | return $title->getCanonicalURL( $query ); |
226 | } |
227 | |
228 | /** |
229 | * @param Thread $thread |
230 | * @param ThreadRevision $revision |
231 | * @return array |
232 | */ |
233 | public static function diffQuery( Thread $thread, ThreadRevision $revision ) { |
234 | $changed_thread = $revision->getChangeObject(); |
235 | $curr_rev_id = $changed_thread->rootRevision(); |
236 | |
237 | $revLookup = MediaWikiServices::getInstance()->getRevisionLookup(); |
238 | $curr_rev_record = $revLookup->getRevisionById( $curr_rev_id ); |
239 | |
240 | $oldid = ''; |
241 | if ( $curr_rev_record ) { |
242 | $prev_rev_record = $revLookup->getPreviousRevision( $curr_rev_record ); |
243 | $oldid = $prev_rev_record ? $prev_rev_record->getId() : ''; |
244 | } |
245 | |
246 | $query = [ |
247 | 'lqt_method' => 'diff', |
248 | 'diff' => $curr_rev_id, |
249 | 'oldid' => $oldid |
250 | ]; |
251 | |
252 | return $query; |
253 | } |
254 | |
255 | public static function diffPermalinkURL( Thread $thread, ThreadRevision $revision ) { |
256 | $query = self::diffQuery( $thread, $revision ); |
257 | return wfExpandUrl( self::permalinkUrl( $thread, null, null, $query ), PROTO_RELATIVE ); |
258 | } |
259 | |
260 | public static function diffPermalink( Thread $thread, $text, ThreadRevision $revision ) { |
261 | $query = self::diffQuery( $thread, $revision ); |
262 | return self::permalink( $thread, $text, null, null, null, [], $query ); |
263 | } |
264 | |
265 | public static function talkpageLink( $title, $text = null, $method = null, $operand = null, |
266 | $includeFragment = true, $attribs = [], |
267 | $options = [], $perpetuateOffset = true |
268 | ) { |
269 | [ $title, $query ] = self::talkpageLinkData( |
270 | $title, $method, $operand, |
271 | $includeFragment, |
272 | $perpetuateOffset |
273 | ); |
274 | |
275 | $linkRenderer = MediaWikiServices::getInstance() |
276 | ->getLinkRendererFactory() |
277 | ->createFromLegacyOptions( $options ); |
278 | return $linkRenderer->makeLink( $title, $text, $attribs, $query ); |
279 | } |
280 | |
281 | /** |
282 | * @param Title $title |
283 | * @param string|null $method |
284 | * @param Thread|null $operand |
285 | * @param bool $includeFragment |
286 | * @param bool|WebRequest $perpetuateOffset |
287 | * @return array |
288 | */ |
289 | public static function talkpageLinkData( $title, $method = null, $operand = null, |
290 | $includeFragment = true, $perpetuateOffset = true |
291 | ) { |
292 | global $wgRequest; |
293 | $query = []; |
294 | |
295 | if ( $method ) { |
296 | $query['lqt_method'] = $method; |
297 | } |
298 | |
299 | if ( $operand ) { |
300 | $query['lqt_operand'] = $operand->id(); |
301 | } |
302 | |
303 | $oldid = $wgRequest->getVal( 'oldid', null ); |
304 | |
305 | if ( $oldid !== null ) { |
306 | // this is an immensely ugly hack to make editing old revisions work. |
307 | $query['oldid'] = $oldid; |
308 | } |
309 | |
310 | $request = $perpetuateOffset; |
311 | if ( $request === true ) { |
312 | global $wgRequest; |
313 | $request = $wgRequest; |
314 | } |
315 | |
316 | if ( $perpetuateOffset ) { |
317 | $offset = $request->getVal( 'offset' ); |
318 | |
319 | if ( $offset ) { |
320 | $query['offset'] = $offset; |
321 | } |
322 | } |
323 | |
324 | // Add fragment if appropriate. |
325 | if ( $operand && $includeFragment ) { |
326 | $title->setFragment( $operand->getAnchorName() ); |
327 | } |
328 | |
329 | return [ $title, $query ]; |
330 | } |
331 | |
332 | /** |
333 | * If you want $perpetuateOffset to perpetuate from a specific request, |
334 | * pass that instead of true |
335 | * @param Title $title |
336 | * @param string|null $method |
337 | * @param Thread|null $operand |
338 | * @param bool $includeFragment |
339 | * @param bool|WebRequest $perpetuateOffset |
340 | * @return string |
341 | */ |
342 | public static function talkpageUrl( $title, $method = null, $operand = null, |
343 | $includeFragment = true, $perpetuateOffset = true |
344 | ) { |
345 | [ $title, $query ] = |
346 | self::talkpageLinkData( $title, $method, $operand, $includeFragment, |
347 | $perpetuateOffset ); |
348 | |
349 | return $title->getLinkUrl( $query ); |
350 | } |
351 | |
352 | /************************************************************* |
353 | * Editing methods (here be dragons) * |
354 | * Forget dragons: This section distorts the rest of the code * |
355 | * like a star bending spacetime around itself. * |
356 | */ |
357 | |
358 | /** |
359 | * Return an HTML form element whose value is gotten from the request. |
360 | * @todo Figure out a clean way to expand this to other forms. |
361 | * @param string $name |
362 | * @param string $as |
363 | * @return string |
364 | */ |
365 | public function perpetuate( $name, $as = 'hidden' ) { |
366 | $value = $this->request->getVal( $name, '' ); |
367 | if ( $as == 'hidden' ) { |
368 | return Html::hidden( $name, $value ); |
369 | } |
370 | |
371 | return ''; |
372 | } |
373 | |
374 | /** |
375 | * @param Thread $thread |
376 | */ |
377 | public function showReplyProtectedNotice( Thread $thread ) { |
378 | $log_url = SpecialPage::getTitleFor( 'Log' )->getLocalURL( |
379 | "type=protect&user=&page={$thread->title()->getPrefixedURL()}" ); |
380 | $link = '<a href="' . $log_url . '">' . |
381 | wfMessage( 'lqt_protectedfromreply_link' )->escaped() . '</a>'; |
382 | $this->output->addHTML( '<p>' . wfMessage( 'lqt_protectedfromreply' ) |
383 | ->rawParams( $link )->escaped() ); |
384 | } |
385 | |
386 | public function doInlineEditForm() { |
387 | $method = $this->request->getVal( 'lqt_method' ); |
388 | $operand = $this->request->getVal( 'lqt_operand' ); |
389 | |
390 | $thread = Threads::withId( intval( $operand ) ); |
391 | |
392 | // Yuck. |
393 | global $wgOut, $wgRequest, $wgTitle; |
394 | $oldOut = $wgOut; |
395 | $oldRequest = $wgRequest; |
396 | $oldTitle = $wgTitle; |
397 | // And override the main context too... (T143889) |
398 | $context = RequestContext::getMain(); |
399 | $oldCOut = $context->getOutput(); |
400 | $oldCRequest = $context->getRequest(); |
401 | $oldCTitle = $context->getTitle(); |
402 | $context->setOutput( $this->output ); |
403 | $context->setRequest( $this->request ); |
404 | $context->setTitle( $this->title ); |
405 | $wgOut = $this->output; |
406 | $wgRequest = $this->request; |
407 | $wgTitle = $this->title; |
408 | |
409 | $hookResult = MediaWikiServices::getInstance()->getHookContainer()->run( 'LiquidThreadsDoInlineEditForm', |
410 | [ |
411 | $thread, |
412 | $this->request, |
413 | &$this->output |
414 | ] ); |
415 | |
416 | if ( !$hookResult ) { |
417 | // Handled by a hook. |
418 | } elseif ( $method == 'reply' ) { |
419 | $this->showReplyForm( $thread ); |
420 | } elseif ( $method == 'talkpage_new_thread' ) { |
421 | $this->showNewThreadForm( $this->article ); |
422 | } elseif ( $method == 'edit' ) { |
423 | $this->showPostEditingForm( $thread ); |
424 | } else { |
425 | throw new LogicException( "Invalid thread method $method" ); |
426 | } |
427 | |
428 | $wgOut = $oldOut; |
429 | $wgRequest = $oldRequest; |
430 | $wgTitle = $oldTitle; |
431 | $context->setOutput( $oldCOut ); |
432 | $context->setRequest( $oldCRequest ); |
433 | $context->setTitle( $oldCTitle ); |
434 | |
435 | $this->output->setArticleBodyOnly( true ); |
436 | } |
437 | |
438 | /** |
439 | * Workaround for bug 27887 caused by r82686 |
440 | * @param FauxRequest $request FauxRequest object to have session data injected into. |
441 | */ |
442 | public static function fixFauxRequestSession( $request ) { |
443 | // This is sometimes called before session_start (bug 28826). |
444 | if ( !isset( $_SESSION ) ) { |
445 | return; |
446 | } |
447 | |
448 | foreach ( $_SESSION as $k => $v ) { |
449 | $request->setSessionData( $k, $v ); |
450 | } |
451 | } |
452 | |
453 | /** |
454 | * @param Article|null $talkpage |
455 | * @param string $method |
456 | * @param int|null $operand |
457 | * @param User $user |
458 | * @return string |
459 | * @throws Exception |
460 | */ |
461 | public static function getInlineEditForm( $talkpage, $method, $operand, User $user ) { |
462 | $req = new RequestContext; |
463 | $output = $req->getOutput(); |
464 | $request = new FauxRequest( [] ); |
465 | |
466 | // Workaround for loss of session data when using FauxRequest |
467 | global $wgRequest; |
468 | self::fixFauxRequestSession( $request ); |
469 | |
470 | $title = null; |
471 | |
472 | if ( $talkpage ) { |
473 | $title = $talkpage->getTitle(); |
474 | } elseif ( $operand ) { |
475 | $thread = Threads::withId( $operand ); |
476 | if ( $thread ) { |
477 | $talkpage = $thread->article(); |
478 | $title = $talkpage->getTitle(); |
479 | } else { |
480 | throw new LogicException( "Cannot get title" ); |
481 | } |
482 | } |
483 | |
484 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
485 | $output->setTitle( $title ); |
486 | $request->setVal( 'lqt_method', $method ); |
487 | $request->setVal( 'lqt_operand', $operand ); |
488 | |
489 | $view = new LqtView( $output, $talkpage, $title, $user, $request ); |
490 | |
491 | $view->doInlineEditForm(); |
492 | |
493 | foreach ( $request->getSessionArray() ?? [] as $k => $v ) { |
494 | $wgRequest->setSessionData( $k, $v ); |
495 | } |
496 | |
497 | return $output->getHTML(); |
498 | } |
499 | |
500 | /** |
501 | * @param Article $talkpage |
502 | */ |
503 | public function showNewThreadForm( $talkpage ) { |
504 | $submitted_nonce = $this->request->getVal( 'lqt_nonce' ); |
505 | if ( $this->request->wasPosted() && !$this->checkNonce( $submitted_nonce ) ) { |
506 | return; |
507 | } |
508 | |
509 | if ( Thread::canUserPost( $this->user, $this->article ) !== true ) { |
510 | $this->output->addWikiMsg( 'lqt-protected-newthread' ); |
511 | return; |
512 | } |
513 | $subject = $this->request->getVal( 'lqt_subject_field' ) ?? false; |
514 | |
515 | $t = null; |
516 | |
517 | $subjectOk = Thread::validateSubject( |
518 | $subject, |
519 | $this->user, |
520 | $t, |
521 | null, |
522 | $this->article |
523 | ); |
524 | if ( !$subjectOk ) { |
525 | try { |
526 | $t = $this->newThreadTitle( $subject ); |
527 | } catch ( Exception $excep ) { |
528 | $t = $this->scratchTitle(); |
529 | } |
530 | } |
531 | |
532 | $html = Xml::openElement( 'div', |
533 | [ 'class' => 'lqt-edit-form lqt-new-thread' ] ); |
534 | $this->output->addHTML( $html ); |
535 | |
536 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
537 | $article = new Article( $t, 0 ); |
538 | |
539 | Hooks::$editTalkpage = $talkpage; |
540 | Hooks::$editArticle = $article; |
541 | Hooks::$editThread = null; |
542 | Hooks::$editType = 'new'; |
543 | Hooks::$editAppliesTo = null; |
544 | |
545 | $e = new EditPage( $article ); |
546 | $e->setContextTitle( $article->getTitle() ); |
547 | $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); |
548 | $hookContainer->run( 'LiquidThreadsShowNewThreadForm', [ &$e, $talkpage ] ); |
549 | |
550 | global $wgRequest; |
551 | // Quietly force a preview if no subject has been specified. |
552 | if ( !$subjectOk ) { |
553 | // Dirty hack to prevent saving from going ahead |
554 | $wgRequest->setVal( 'wpPreview', true ); |
555 | |
556 | if ( $this->request->wasPosted() ) { |
557 | if ( !$subject ) { |
558 | $msg = 'lqt_empty_subject'; |
559 | } else { |
560 | $msg = 'lqt_invalid_subject'; |
561 | } |
562 | |
563 | $e->editFormPageTop .= |
564 | Xml::tags( 'div', [ 'class' => 'error' ], |
565 | wfMessage( $msg )->parseAsBlock() ); |
566 | } |
567 | } |
568 | |
569 | $e->suppressIntro = true; |
570 | $e->editFormTextBeforeContent .= |
571 | $this->perpetuate( 'lqt_method', 'hidden' ) . |
572 | $this->perpetuate( 'lqt_operand', 'hidden' ) . |
573 | Html::hidden( 'lqt_nonce', MWCryptRand::generateHex( 32 ) ); |
574 | |
575 | $e->mShowSummaryField = false; |
576 | |
577 | $summary = wfMessage( 'lqt-newpost-summary', $subject )->inContentLanguage()->text(); |
578 | $wgRequest->setVal( 'wpSummary', $summary ); |
579 | |
580 | [ $signatureEditor, $signatureHTML ] = $this->getSignatureEditor( $this->user ); |
581 | |
582 | $e->editFormTextAfterContent .= |
583 | $signatureEditor; |
584 | $e->previewTextAfterContent .= |
585 | Xml::tags( 'p', null, $signatureHTML ); |
586 | |
587 | $e->editFormTextBeforeContent .= $this->getSubjectEditor( '', $subject ); |
588 | |
589 | $hookContainer->run( 'LiquidThreadsAfterShowNewThreadForm', [ &$e, $talkpage ] ); |
590 | |
591 | $e->edit(); |
592 | |
593 | if ( $e->didSave ) { |
594 | $signature = $this->request->getVal( 'wpLqtSignature', null ); |
595 | |
596 | $info = |
597 | [ |
598 | 'talkpage' => $talkpage, |
599 | 'text' => $e->textbox1, |
600 | 'summary' => $e->summary, |
601 | 'signature' => $signature, |
602 | 'root' => $article, |
603 | 'subject' => $subject, |
604 | ]; |
605 | |
606 | $hookContainer->run( 'LiquidThreadsSaveNewThread', |
607 | [ &$info, &$e, &$talkpage ] ); |
608 | |
609 | $thread = self::newPostMetadataUpdates( $this->user, $info ); |
610 | self::consumeNonce( $submitted_nonce ); |
611 | } |
612 | |
613 | if ( $this->output->getRedirect() != '' ) { |
614 | $redirectTitle = clone $talkpage->getTitle(); |
615 | if ( !empty( $thread ) ) { |
616 | $redirectTitle->setFragment( '#' . $this->anchorName( $thread ) ); |
617 | } |
618 | $this->output->redirect( $this->title->getLocalURL() ); |
619 | } |
620 | |
621 | $this->output->addHTML( '</div>' ); |
622 | } |
623 | |
624 | /** |
625 | * @param Thread $thread |
626 | */ |
627 | public function showReplyForm( Thread $thread ) { |
628 | global $wgRequest; |
629 | |
630 | $submitted_nonce = $this->request->getVal( 'lqt_nonce' ); |
631 | if ( $this->request->wasPosted() && !$this->checkNonce( $submitted_nonce ) ) { |
632 | return; |
633 | } |
634 | |
635 | $perm_result = $thread->canUserReply( $this->user, 'quick' ); |
636 | if ( $perm_result !== true ) { |
637 | $this->showReplyProtectedNotice( $thread ); |
638 | return; |
639 | } |
640 | |
641 | $html = Xml::openElement( 'div', |
642 | [ 'class' => 'lqt-reply-form lqt-edit-form' ] ); |
643 | $this->output->addHTML( $html ); |
644 | |
645 | try { |
646 | $t = $this->newReplyTitle( null, $thread ); |
647 | } catch ( Exception $excep ) { |
648 | $t = $this->scratchTitle(); |
649 | } |
650 | |
651 | $article = new Article( $t, 0 ); |
652 | $talkpage = $thread->article(); |
653 | |
654 | Hooks::$editTalkpage = $talkpage; |
655 | Hooks::$editArticle = $article; |
656 | Hooks::$editThread = $thread; |
657 | Hooks::$editType = 'reply'; |
658 | Hooks::$editAppliesTo = $thread; |
659 | |
660 | $e = new EditPage( $article ); |
661 | $e->setContextTitle( $article->getTitle() ); |
662 | |
663 | $e->mShowSummaryField = false; |
664 | |
665 | $reply_subject = $thread->subject(); |
666 | $reply_title = $thread->title()->getPrefixedText(); |
667 | $summary = wfMessage( |
668 | 'lqt-reply-summary', |
669 | $reply_subject, |
670 | $reply_title |
671 | )->inContentLanguage()->text(); |
672 | $wgRequest->setVal( 'wpSummary', $summary ); |
673 | |
674 | // Add an offset so it works if it's on the wrong page. |
675 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
676 | $offset = wfTimestamp( TS_UNIX, $thread->topmostThread()->sortkey() ); |
677 | $offset++; |
678 | $offset = $dbr->timestamp( $offset ); |
679 | |
680 | $e->suppressIntro = true; |
681 | $e->editFormTextBeforeContent .= |
682 | $this->perpetuate( 'lqt_method', 'hidden' ) . |
683 | $this->perpetuate( 'lqt_operand', 'hidden' ) . |
684 | Html::hidden( 'lqt_nonce', MWCryptRand::generateHex( 32 ) ) . |
685 | Html::hidden( 'offset', $offset ) . |
686 | Html::hidden( 'wpMinorEdit', '' ); |
687 | |
688 | [ $signatureEditor, $signatureHTML ] = $this->getSignatureEditor( $this->user ); |
689 | |
690 | $e->editFormTextAfterContent .= |
691 | $signatureEditor; |
692 | $e->previewTextAfterContent .= |
693 | Xml::tags( 'p', null, $signatureHTML ); |
694 | |
695 | $wgRequest->setVal( 'wpWatchThis', false ); |
696 | |
697 | $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); |
698 | $hookContainer->run( 'LiquidThreadsShowReplyForm', [ &$e, $thread ] ); |
699 | |
700 | $e->edit(); |
701 | |
702 | if ( $e->didSave ) { |
703 | $bump = !$this->request->getCheck( 'wpBumpThread' ) || |
704 | $this->request->getBool( 'wpBumpThread' ); |
705 | $signature = $this->request->getVal( 'wpLqtSignature', null ); |
706 | |
707 | $info = [ |
708 | 'replyTo' => $thread, |
709 | 'text' => $e->textbox1, |
710 | 'summary' => $e->summary, |
711 | 'bump' => $bump, |
712 | 'signature' => $signature, |
713 | 'root' => $article, |
714 | ]; |
715 | |
716 | $hookContainer->run( 'LiquidThreadsSaveReply', |
717 | [ &$info, &$e, &$thread ] ); |
718 | |
719 | $newThread = self::replyMetadataUpdates( $this->user, $info ); |
720 | self::consumeNonce( $submitted_nonce ); |
721 | } |
722 | |
723 | if ( $this->output->getRedirect() != '' ) { |
724 | $redirectTitle = clone $talkpage->getTitle(); |
725 | if ( !empty( $newThread ) ) { |
726 | $redirectTitle->setFragment( '#' . $this->anchorName( $newThread ) ); |
727 | } |
728 | $this->output->redirect( $this->title->getLocalURL() ); |
729 | } |
730 | |
731 | $this->output->addHTML( '</div>' ); |
732 | } |
733 | |
734 | /** |
735 | * @param Thread $thread |
736 | */ |
737 | public function showPostEditingForm( Thread $thread ) { |
738 | $submitted_nonce = $this->request->getVal( 'lqt_nonce' ); |
739 | if ( $this->request->wasPosted() && !$this->checkNonce( $submitted_nonce ) ) { |
740 | return; |
741 | } |
742 | |
743 | $html = Xml::openElement( 'div', |
744 | [ 'class' => 'lqt-edit-form' ] ); |
745 | $this->output->addHTML( $html ); |
746 | |
747 | $subject = $this->request->getVal( 'lqt_subject_field', '' ); |
748 | |
749 | if ( !$subject ) { |
750 | $subject = $thread->subject() ?? ''; |
751 | } |
752 | |
753 | $t = null; |
754 | $subjectOk = Thread::validateSubject( |
755 | $subject, |
756 | $this->user, |
757 | $t, |
758 | $thread->superthread(), |
759 | $this->article |
760 | ); |
761 | if ( !$subjectOk ) { |
762 | $subject = false; |
763 | } |
764 | |
765 | $article = $thread->root(); |
766 | $talkpage = $thread->article(); |
767 | |
768 | MediaWikiServices::getInstance()->getHookContainer() |
769 | ->run( 'LiquidThreadsEditFormContent', [ $thread, &$article, $talkpage ] ); |
770 | |
771 | Hooks::$editTalkpage = $talkpage; |
772 | Hooks::$editArticle = $article; |
773 | Hooks::$editThread = $thread; |
774 | Hooks::$editType = 'edit'; |
775 | Hooks::$editAppliesTo = $thread; |
776 | |
777 | $e = new EditPage( $article ); |
778 | $e->setContextTitle( $article->getTitle() ); |
779 | |
780 | global $wgRequest; |
781 | // Quietly force a preview if no subject has been specified. |
782 | if ( !$subjectOk ) { |
783 | // Dirty hack to prevent saving from going ahead |
784 | $wgRequest->setVal( 'wpPreview', true ); |
785 | |
786 | if ( $this->request->wasPosted() ) { |
787 | $e->editFormPageTop .= |
788 | Xml::tags( 'div', [ 'class' => 'error' ], |
789 | wfMessage( 'lqt_invalid_subject' )->parse() ); |
790 | } |
791 | } |
792 | |
793 | // Add an offset so it works if it's on the wrong page. |
794 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
795 | $offset = wfTimestamp( TS_UNIX, $thread->topmostThread()->sortkey() ); |
796 | $offset++; |
797 | $offset = $dbr->timestamp( $offset ); |
798 | |
799 | $e->suppressIntro = true; |
800 | $e->editFormTextBeforeContent .= |
801 | $this->perpetuate( 'lqt_method', 'hidden' ) . |
802 | $this->perpetuate( 'lqt_operand', 'hidden' ) . |
803 | Html::hidden( 'lqt_nonce', MWCryptRand::generateHex( 32 ) ) . |
804 | Html::hidden( 'offset', $offset ); |
805 | |
806 | [ $signatureEditor, $signatureHTML ] = $this->getSignatureEditor( $thread ); |
807 | |
808 | $e->editFormTextAfterContent .= |
809 | $signatureEditor; |
810 | $e->previewTextAfterContent .= |
811 | Xml::tags( 'p', null, $signatureHTML ); |
812 | |
813 | if ( $thread->isTopmostThread() ) { |
814 | $e->editFormTextBeforeContent .= |
815 | $this->getSubjectEditor( $thread->subject(), $subject ); |
816 | } |
817 | |
818 | $e->edit(); |
819 | |
820 | if ( $e->didSave ) { |
821 | $bump = !$this->request->getCheck( 'wpBumpThread' ) || |
822 | $this->request->getBool( 'wpBumpThread' ); |
823 | $signature = $this->request->getVal( 'wpLqtSignature', null ); |
824 | |
825 | self::editMetadataUpdates( |
826 | [ |
827 | 'thread' => $thread, |
828 | 'text' => $e->textbox1, |
829 | 'summary' => $e->summary, |
830 | 'bump' => $bump, |
831 | 'subject' => $subject, |
832 | 'signature' => $signature, |
833 | 'root' => $article, |
834 | ] |
835 | ); |
836 | self::consumeNonce( $submitted_nonce ); |
837 | } |
838 | |
839 | if ( $this->output->getRedirect() != '' ) { |
840 | $redirectTitle = clone $talkpage->getTitle(); |
841 | $redirectTitle->setFragment( '#' . $this->anchorName( $thread ) ); |
842 | $this->output->redirect( $this->title->getLocalURL() ); |
843 | } |
844 | |
845 | $this->output->addHTML( '</div>' ); |
846 | } |
847 | |
848 | /** |
849 | * @param Thread $thread |
850 | */ |
851 | public function showSummarizeForm( Thread $thread ) { |
852 | $submitted_nonce = $this->request->getVal( 'lqt_nonce' ); |
853 | if ( $this->request->wasPosted() && !$this->checkNonce( $submitted_nonce ) ) { |
854 | return; |
855 | } |
856 | |
857 | if ( $thread->summary() ) { |
858 | $article = $thread->summary(); |
859 | $summarizeMsg = 'lqt-update-summary-intro'; |
860 | } else { |
861 | $t = $this->newSummaryTitle( $thread ); |
862 | $article = new Article( $t, 0 ); |
863 | $summarizeMsg = 'lqt-summarize-intro'; |
864 | } |
865 | |
866 | $html = Xml::openElement( 'div', |
867 | [ 'class' => 'lqt-edit-form lqt-summarize-form' ] ); |
868 | $this->output->addHTML( $html ); |
869 | |
870 | $this->output->addWikiMsg( $summarizeMsg ); |
871 | |
872 | $talkpage = $thread->article(); |
873 | |
874 | Hooks::$editTalkpage = $talkpage; |
875 | Hooks::$editArticle = $article; |
876 | Hooks::$editThread = $thread; |
877 | Hooks::$editType = 'summarize'; |
878 | Hooks::$editAppliesTo = $thread; |
879 | |
880 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
881 | $e = new EditPage( $article ); |
882 | $e->setContextTitle( $article->getTitle() ); |
883 | |
884 | // Add an offset so it works if it's on the wrong page. |
885 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
886 | $offset = wfTimestamp( TS_UNIX, $thread->topmostThread()->sortkey() ); |
887 | $offset++; |
888 | $offset = $dbr->timestamp( $offset ); |
889 | |
890 | $e->suppressIntro = true; |
891 | $e->editFormTextBeforeContent .= |
892 | $this->perpetuate( 'lqt_method', 'hidden' ) . |
893 | $this->perpetuate( 'lqt_operand', 'hidden' ) . |
894 | Html::hidden( 'lqt_nonce', MWCryptRand::generateHex( 32 ) ) . |
895 | Html::hidden( 'offset', $offset ); |
896 | |
897 | $e->edit(); |
898 | |
899 | if ( $e->didSave ) { |
900 | $bump = !$this->request->getCheck( 'wpBumpThread' ) || |
901 | $this->request->getBool( 'wpBumpThread' ); |
902 | |
903 | self::summarizeMetadataUpdates( |
904 | [ |
905 | 'thread' => $thread, |
906 | 'article' => $article, |
907 | 'summary' => $e->summary, |
908 | 'bump' => $bump, |
909 | ] |
910 | ); |
911 | self::consumeNonce( $submitted_nonce ); |
912 | } |
913 | |
914 | if ( $this->output->getRedirect() != '' ) { |
915 | $redirectTitle = clone $talkpage->getTitle(); |
916 | $redirectTitle->setFragment( '#' . $this->anchorName( $thread ) ); |
917 | $this->output->redirect( $this->title->getLocalURL() ); |
918 | } |
919 | |
920 | $this->output->addHTML( '</div>' ); |
921 | } |
922 | |
923 | /** |
924 | * Check a nonce token on HTTP POST during a thread submission |
925 | * |
926 | * @param string $token |
927 | * @return bool Whether the user nonce token is unused or no token was provided |
928 | */ |
929 | public function checkNonce( $token ) { |
930 | if ( !$token ) { |
931 | return true; |
932 | } |
933 | |
934 | // Primary data-center cluster cache |
935 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance(); |
936 | $nonce_key = $cache->makeKey( 'lqt-nonce', $token, $this->user->getName() ); |
937 | |
938 | if ( $cache->get( $nonce_key ) ) { |
939 | $this->output->redirect( $this->article->getTitle()->getLocalURL() ); |
940 | return false; |
941 | } |
942 | |
943 | return true; |
944 | } |
945 | |
946 | /** |
947 | * Consume a nonce token on HTTP POST during a thread submission |
948 | * |
949 | * @param string $token |
950 | * @return bool Whether the user nonce token was acquired or no token was provided |
951 | */ |
952 | public function consumeNonce( $token ) { |
953 | if ( !$token ) { |
954 | return true; |
955 | } |
956 | |
957 | // Primary data-center cluster cache |
958 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance(); |
959 | $nonce_key = $cache->makeKey( 'lqt-nonce', $token, $this->user->getName() ); |
960 | |
961 | return $cache->add( $nonce_key, 1, $cache::TTL_HOUR ); |
962 | } |
963 | |
964 | /** |
965 | * @param string $db_subject |
966 | * @param string|false $subject |
967 | * @return string HTML |
968 | */ |
969 | public function getSubjectEditor( $db_subject, $subject ) { |
970 | if ( $subject === false ) { |
971 | $subject = $db_subject; |
972 | } |
973 | |
974 | $subject_label = wfMessage( 'lqt_subject' )->text(); |
975 | |
976 | $attr = [ 'tabindex' => 1 ]; |
977 | |
978 | return Xml::inputLabel( $subject_label, 'lqt_subject_field', |
979 | 'lqt_subject_field', 60, $subject, $attr ) . |
980 | Xml::element( 'br' ); |
981 | } |
982 | |
983 | public function getSignatureEditor( $from ) { |
984 | $signatureText = $this->request->getVal( 'wpLqtSignature', null ); |
985 | |
986 | if ( $signatureText === null ) { |
987 | if ( $from instanceof User ) { |
988 | $signatureText = self::getUserSignature( $from ); |
989 | } elseif ( $from instanceof Thread ) { |
990 | $signatureText = $from->signature() ?? ''; |
991 | } else { |
992 | $signatureText = ''; |
993 | } |
994 | } |
995 | |
996 | $signatureHTML = self::parseSignature( $signatureText ); |
997 | |
998 | // Signature edit box |
999 | $signaturePreview = Xml::tags( |
1000 | 'span', |
1001 | [ |
1002 | 'class' => 'lqt-signature-preview', |
1003 | 'style' => 'display: none;' |
1004 | ], |
1005 | $signatureHTML |
1006 | ); |
1007 | $signatureEditBox = Xml::input( |
1008 | 'wpLqtSignature', 45, $signatureText, |
1009 | [ 'class' => 'lqt-signature-edit' ] |
1010 | ); |
1011 | |
1012 | $signatureEditor = $signaturePreview . $signatureEditBox; |
1013 | |
1014 | return [ $signatureEditor, $signatureHTML ]; |
1015 | } |
1016 | |
1017 | public static function replyMetadataUpdates( User $user, $data = [] ) { |
1018 | $requiredFields = [ 'replyTo', 'root', 'text' ]; |
1019 | |
1020 | foreach ( $requiredFields as $f ) { |
1021 | if ( !isset( $data[$f] ) ) { |
1022 | throw new RuntimeException( "Missing required field $f" ); |
1023 | } |
1024 | } |
1025 | |
1026 | $signature = $data['signature'] ?? self::getUserSignature( $user ); |
1027 | |
1028 | $summary = $data['summary'] ?? ''; |
1029 | |
1030 | $replyTo = $data['replyTo']; |
1031 | $root = $data['root']; |
1032 | $bump = !empty( $data['bump'] ); |
1033 | |
1034 | $subject = $replyTo->subject(); |
1035 | $talkpage = $replyTo->article(); |
1036 | |
1037 | $thread = Thread::create( |
1038 | $root, $talkpage, $user, $replyTo, Threads::TYPE_NORMAL, $subject, |
1039 | $summary, $bump, $signature |
1040 | ); |
1041 | |
1042 | MediaWikiServices::getInstance()->getHookContainer() |
1043 | ->run( 'LiquidThreadsAfterReplyMetadataUpdates', [ &$thread ] ); |
1044 | |
1045 | return $thread; |
1046 | } |
1047 | |
1048 | public static function summarizeMetadataUpdates( $data = [] ) { |
1049 | $requiredFields = [ 'thread', 'article', 'summary' ]; |
1050 | |
1051 | foreach ( $requiredFields as $f ) { |
1052 | if ( !isset( $data[$f] ) ) { |
1053 | throw new RuntimeException( "Missing required field $f" ); |
1054 | } |
1055 | } |
1056 | |
1057 | $thread = $data["thread"]; |
1058 | $article = $data["article"]; |
1059 | $summary = $data["summary"]; |
1060 | |
1061 | $bump = $data["bump"] ?? null; |
1062 | |
1063 | $user = RequestContext::getMain()->getUser(); // Need to inject |
1064 | $thread->setSummary( $article ); |
1065 | $thread->commitRevision( |
1066 | Threads::CHANGE_EDITED_SUMMARY, $user, $thread, $summary, $bump ); |
1067 | |
1068 | return $thread; |
1069 | } |
1070 | |
1071 | public static function editMetadataUpdates( $data = [] ) { |
1072 | $requiredFields = [ 'thread', 'text', 'summary' ]; |
1073 | |
1074 | foreach ( $requiredFields as $f ) { |
1075 | if ( !isset( $data[$f] ) ) { |
1076 | throw new RuntimeException( "Missing required field $f" ); |
1077 | } |
1078 | } |
1079 | |
1080 | $thread = $data['thread']; |
1081 | |
1082 | // Use a separate type if the content is blanked. |
1083 | $type = strlen( trim( $data['text'] ) ) |
1084 | ? Threads::CHANGE_EDITED_ROOT |
1085 | : Threads::CHANGE_ROOT_BLANKED; |
1086 | |
1087 | if ( isset( $data['signature'] ) ) { |
1088 | $thread->setSignature( $data['signature'] ); |
1089 | } |
1090 | |
1091 | $bump = !empty( $data['bump'] ); |
1092 | |
1093 | $user = RequestContext::getMain()->getUser(); // Need to inject |
1094 | // Add the history entry. |
1095 | $thread->commitRevision( $type, $user, $thread, $data['summary'], $bump ); |
1096 | |
1097 | // Update subject if applicable. |
1098 | if ( $thread->isTopmostThread() && !empty( $data['subject'] ) && |
1099 | $data['subject'] != $thread->subject() ) { |
1100 | $thread->setSubject( $data['subject'] ); |
1101 | $thread->commitRevision( Threads::CHANGE_EDITED_SUBJECT, |
1102 | $user, $thread, $data['summary'] ); |
1103 | } |
1104 | |
1105 | return $thread; |
1106 | } |
1107 | |
1108 | public static function newPostMetadataUpdates( User $user, $data ) { |
1109 | $requiredFields = [ 'talkpage', 'root', 'text', 'subject' ]; |
1110 | |
1111 | foreach ( $requiredFields as $f ) { |
1112 | if ( !isset( $data[$f] ) ) { |
1113 | throw new RuntimeException( "Missing required field $f" ); |
1114 | } |
1115 | } |
1116 | |
1117 | $signature = $data['signature'] ?? self::getUserSignature( $user ); |
1118 | |
1119 | $summary = $data['summary'] ?? ''; |
1120 | |
1121 | $talkpage = $data['talkpage']; |
1122 | '@phan-var Article $talkpage'; |
1123 | $root = $data['root']; |
1124 | $subject = $data['subject']; |
1125 | |
1126 | $thread = Thread::create( |
1127 | $root, $talkpage, $user, null, |
1128 | Threads::TYPE_NORMAL, $subject, |
1129 | $summary, null, $signature |
1130 | ); |
1131 | |
1132 | MediaWikiServices::getInstance()->getHookContainer() |
1133 | ->run( 'LiquidThreadsAfterNewPostMetadataUpdates', [ &$thread ] ); |
1134 | |
1135 | return $thread; |
1136 | } |
1137 | |
1138 | /** |
1139 | * @param string $subject |
1140 | * @return Title |
1141 | */ |
1142 | public function newThreadTitle( $subject ) { |
1143 | return Threads::newThreadTitle( $subject, $this->article ); |
1144 | } |
1145 | |
1146 | /** |
1147 | * @param Thread $thread |
1148 | * @return Title |
1149 | */ |
1150 | public function newSummaryTitle( Thread $thread ) { |
1151 | return Threads::newSummaryTitle( $thread ); |
1152 | } |
1153 | |
1154 | /** |
1155 | * @param mixed $unused |
1156 | * @param Thread $thread |
1157 | * @return Title |
1158 | */ |
1159 | public function newReplyTitle( $unused, Thread $thread ) { |
1160 | return Threads::newReplyTitle( $thread, $this->user ); |
1161 | } |
1162 | |
1163 | /** |
1164 | * @return Title |
1165 | */ |
1166 | public function scratchTitle() { |
1167 | return Title::makeTitle( NS_LQT_THREAD, MWCryptRand::generateHex( 32 ) ); |
1168 | } |
1169 | |
1170 | /** |
1171 | * @param Thread $thread |
1172 | * @return array Example return value: |
1173 | * array ( |
1174 | * edit => array( 'label' => 'Edit', |
1175 | * 'href' => 'http...', |
1176 | * 'enabled' => false ), |
1177 | * reply => array( 'label' => 'Reply', |
1178 | * 'href' => 'http...', |
1179 | * 'enabled' => true ) |
1180 | * ) |
1181 | */ |
1182 | public function threadCommands( Thread $thread ) { |
1183 | $commands = []; |
1184 | $isLqtPage = LqtDispatch::isLqtPage( $thread->getTitle() ); |
1185 | |
1186 | $history_url = self::permalinkUrlWithQuery( $thread, [ 'action' => 'history' ] ); |
1187 | $commands['history'] = [ |
1188 | 'label' => wfMessage( 'history_short' )->parse(), |
1189 | 'href' => $history_url, |
1190 | 'enabled' => true |
1191 | ]; |
1192 | |
1193 | if ( $thread->isHistorical() ) { |
1194 | return []; |
1195 | } |
1196 | $services = MediaWikiServices::getInstance(); |
1197 | $user_can_edit = $services->getPermissionManager() |
1198 | ->quickUserCan( 'edit', $this->user, $thread->root()->getTitle() ); |
1199 | $editMsg = $user_can_edit ? 'edit' : 'viewsource'; |
1200 | |
1201 | if ( $isLqtPage ) { |
1202 | $commands['edit'] = [ |
1203 | 'label' => wfMessage( $editMsg )->parse(), |
1204 | 'href' => $this->talkpageUrl( |
1205 | $this->title, |
1206 | 'edit', $thread, |
1207 | true, /* include fragment */ |
1208 | $this->request |
1209 | ), |
1210 | 'enabled' => true |
1211 | ]; |
1212 | } |
1213 | |
1214 | if ( $this->user->isAllowed( 'delete' ) ) { |
1215 | $delete_url = $thread->title()->getLocalURL( 'action=delete' ); |
1216 | $deleteMsg = $thread->type() == Threads::TYPE_DELETED ? 'lqt_undelete' : 'delete'; |
1217 | |
1218 | $commands['delete'] = [ |
1219 | 'label' => wfMessage( $deleteMsg )->parse(), |
1220 | 'href' => $delete_url, |
1221 | 'enabled' => true |
1222 | ]; |
1223 | } |
1224 | |
1225 | if ( $isLqtPage ) { |
1226 | if ( !$thread->isTopmostThread() && $this->user->isAllowed( 'lqt-split' ) ) { |
1227 | $splitUrl = SpecialPage::getTitleFor( 'SplitThread', |
1228 | $thread->title()->getPrefixedText() )->getLocalURL(); |
1229 | $commands['split'] = [ |
1230 | 'label' => wfMessage( 'lqt-thread-split' )->parse(), |
1231 | 'href' => $splitUrl, |
1232 | 'enabled' => true |
1233 | ]; |
1234 | } |
1235 | |
1236 | if ( $this->user->isAllowed( 'lqt-merge' ) ) { |
1237 | $mergeParams = $_GET; |
1238 | $mergeParams['lqt_merge_from'] = $thread->id(); |
1239 | |
1240 | unset( $mergeParams['title'] ); |
1241 | |
1242 | $mergeUrl = $this->title->getLocalURL( wfArrayToCgi( $mergeParams ) ); |
1243 | $label = wfMessage( 'lqt-thread-merge' )->parse(); |
1244 | |
1245 | $commands['merge'] = [ |
1246 | 'label' => $label, |
1247 | 'href' => $mergeUrl, |
1248 | 'enabled' => true |
1249 | ]; |
1250 | } |
1251 | } |
1252 | |
1253 | $commands['link'] = [ |
1254 | 'label' => wfMessage( 'lqt_permalink' )->parse(), |
1255 | 'href' => $thread->title()->getLocalURL(), |
1256 | 'enabled' => true, |
1257 | 'showlabel' => true, |
1258 | 'tooltip' => wfMessage( 'lqt_permalink' )->parse() |
1259 | ]; |
1260 | |
1261 | $services->getHookContainer()->run( 'LiquidThreadsThreadCommands', [ $thread, &$commands ] ); |
1262 | |
1263 | return $commands; |
1264 | } |
1265 | |
1266 | /** |
1267 | * Commands for the bottom. |
1268 | * @param Thread $thread |
1269 | * @return array[] |
1270 | */ |
1271 | public function threadMajorCommands( Thread $thread ) { |
1272 | $isLqtPage = LqtDispatch::isLqtPage( $thread->getTitle() ); |
1273 | |
1274 | if ( $thread->isHistorical() ) { |
1275 | // No links for historical threads. |
1276 | $history_url = self::permalinkUrlWithQuery( $thread, |
1277 | [ 'action' => 'history' ] ); |
1278 | $commands = []; |
1279 | |
1280 | $commands['history'] = [ |
1281 | 'label' => wfMessage( 'history_short' )->parse(), |
1282 | 'href' => $history_url, |
1283 | 'enabled' => true |
1284 | ]; |
1285 | |
1286 | return $commands; |
1287 | } |
1288 | |
1289 | $commands = []; |
1290 | |
1291 | if ( $isLqtPage ) { |
1292 | if ( $this->user->isAllowed( 'lqt-merge' ) && |
1293 | $this->request->getCheck( 'lqt_merge_from' ) |
1294 | ) { |
1295 | $srcThread = Threads::withId( $this->request->getInt( 'lqt_merge_from' ) ); |
1296 | $par = $srcThread->title()->getPrefixedText(); |
1297 | $mergeTitle = SpecialPage::getTitleFor( 'MergeThread', $par ); |
1298 | $mergeUrl = $mergeTitle->getLocalURL( 'dest=' . $thread->id() ); |
1299 | $label = wfMessage( 'lqt-thread-merge-to' )->parse(); |
1300 | |
1301 | $commands['merge-to'] = [ |
1302 | 'label' => $label, |
1303 | 'href' => $mergeUrl, |
1304 | 'enabled' => true, |
1305 | 'tooltip' => $label |
1306 | ]; |
1307 | } |
1308 | |
1309 | if ( $thread->canUserReply( $this->user, 'quick' ) === true ) { |
1310 | $commands['reply'] = [ |
1311 | 'label' => wfMessage( 'lqt_reply' )->parse(), |
1312 | 'href' => $this->talkpageUrl( $this->title, 'reply', $thread, |
1313 | true /* include fragment */, $this->request ), |
1314 | 'enabled' => true, |
1315 | 'showlabel' => 1, |
1316 | 'tooltip' => wfMessage( 'lqt_reply' )->parse(), |
1317 | 'icon' => 'reply.png', |
1318 | ]; |
1319 | } |
1320 | } |
1321 | |
1322 | // Parent post link |
1323 | if ( !$thread->isTopmostThread() ) { |
1324 | $parent = $thread->superthread(); |
1325 | $anchor = $parent->getAnchorName(); |
1326 | |
1327 | $commands['parent'] = [ |
1328 | 'label' => wfMessage( 'lqt-parent' )->parse(), |
1329 | 'href' => '#' . $anchor, |
1330 | 'enabled' => true, |
1331 | 'showlabel' => 1, |
1332 | ]; |
1333 | } |
1334 | |
1335 | MediaWikiServices::getInstance()->getHookContainer() |
1336 | ->run( 'LiquidThreadsThreadMajorCommands', [ $thread, &$commands ] ); |
1337 | |
1338 | return $commands; |
1339 | } |
1340 | |
1341 | /** |
1342 | * @param Thread $thread |
1343 | * @return array |
1344 | */ |
1345 | public function topLevelThreadCommands( Thread $thread ) { |
1346 | $commands = []; |
1347 | |
1348 | $commands['history'] = [ |
1349 | 'label' => wfMessage( 'history_short' )->parse(), |
1350 | 'href' => self::permalinkUrl( $thread, 'thread_history' ), |
1351 | 'enabled' => true |
1352 | ]; |
1353 | |
1354 | if ( $this->user->isAllowed( 'move' ) ) { |
1355 | $move_href = SpecialPage::getTitleFor( |
1356 | 'MoveThread', $thread->title()->getPrefixedText() )->getLocalURL(); |
1357 | $commands['move'] = [ |
1358 | 'label' => wfMessage( 'lqt-movethread' )->parse(), |
1359 | 'href' => $move_href, |
1360 | 'enabled' => true |
1361 | ]; |
1362 | } |
1363 | |
1364 | $services = MediaWikiServices::getInstance(); |
1365 | if ( $this->user->isAllowed( 'protect' ) ) { |
1366 | $protect_href = $thread->title()->getLocalURL( 'action=protect' ); |
1367 | |
1368 | // Check if it's already protected |
1369 | if ( !$services->getRestrictionStore()->isProtected( $thread->title() ) ) { |
1370 | $label = wfMessage( 'protect' )->parse(); |
1371 | } else { |
1372 | $label = wfMessage( 'unprotect' )->parse(); |
1373 | } |
1374 | |
1375 | $commands['protect'] = [ |
1376 | 'label' => $label, |
1377 | 'href' => $protect_href, |
1378 | 'enabled' => true |
1379 | ]; |
1380 | } |
1381 | |
1382 | if ( !$this->user->isAnon() && !$services->getWatchlistManager() |
1383 | ->isWatched( $this->user, $thread->title() ) ) { |
1384 | $commands['watch'] = [ |
1385 | 'label' => wfMessage( 'watch' )->parse(), |
1386 | 'href' => self::permalinkUrlWithQuery( |
1387 | $thread, |
1388 | [ |
1389 | 'action' => 'watch', |
1390 | 'token' => $this->article->getContext()->getCsrfTokenSet()->getToken( 'watch' )->toString() |
1391 | ] |
1392 | ), |
1393 | 'enabled' => true |
1394 | ]; |
1395 | } elseif ( !$this->user->isAnon() ) { |
1396 | $commands['unwatch'] = [ |
1397 | 'label' => wfMessage( 'unwatch' )->parse(), |
1398 | 'href' => self::permalinkUrlWithQuery( |
1399 | $thread, |
1400 | [ |
1401 | 'action' => 'unwatch', |
1402 | 'token' => $this->article->getContext()->getCsrfTokenSet()->getToken( 'unwatch' )->toString() |
1403 | ] |
1404 | ), |
1405 | 'enabled' => true |
1406 | ]; |
1407 | } |
1408 | |
1409 | if ( LqtDispatch::isLqtPage( $thread->getTitle() ) ) { |
1410 | $summarizeUrl = self::permalinkUrl( $thread, 'summarize', $thread->id() ); |
1411 | $commands['summarize'] = [ |
1412 | 'label' => wfMessage( 'lqt_summarize_link' )->parse(), |
1413 | 'href' => $summarizeUrl, |
1414 | 'enabled' => true, |
1415 | ]; |
1416 | } |
1417 | |
1418 | $services->getHookContainer()->run( 'LiquidThreadsTopLevelCommands', [ $thread, &$commands ] ); |
1419 | |
1420 | return $commands; |
1421 | } |
1422 | |
1423 | /** |
1424 | * @param Article $post |
1425 | * @param int|null $oldid |
1426 | * @return string|false false if the article and revision do not exist. The HTML of the page to |
1427 | * display if it exists. Note that this impacts the state out OutputPage by adding |
1428 | * all the other relevant parts of the parser output. If you don't want this, call |
1429 | * $post->getParserOutput. |
1430 | */ |
1431 | public function showPostBody( $post, $oldid = null ) { |
1432 | $parserOutput = $post->getParserOutput( $oldid ); |
1433 | |
1434 | if ( $parserOutput === false ) { |
1435 | return false; |
1436 | } |
1437 | |
1438 | // Remove title, so that it stays set correctly. |
1439 | $parserOutput->setTitleText( '' ); |
1440 | |
1441 | $out = RequestContext::getMain()->getOutput(); |
1442 | $out->addParserOutputMetadata( $parserOutput ); |
1443 | |
1444 | // LanguageConverter for language conversion |
1445 | $services = MediaWikiServices::getInstance(); |
1446 | $langConv = $services |
1447 | ->getLanguageConverterFactory() |
1448 | ->getLanguageConverter( $services->getContentLanguage() ); |
1449 | |
1450 | return $langConv->convert( $parserOutput->getText() ); |
1451 | } |
1452 | |
1453 | /** |
1454 | * @param Thread $thread |
1455 | * @return string |
1456 | * @suppress SecurityCheck-DoubleEscaped |
1457 | */ |
1458 | public function showThreadToolbar( Thread $thread ) { |
1459 | $html = ''; |
1460 | |
1461 | $headerParts = []; |
1462 | |
1463 | foreach ( $this->threadMajorCommands( $thread ) as $key => $cmd ) { |
1464 | $content = $this->contentForCommand( $cmd, false /* No icon divs */ ); |
1465 | $headerParts[] = Xml::tags( 'li', |
1466 | [ 'class' => "lqt-command lqt-command-$key" ], |
1467 | $content ); |
1468 | } |
1469 | |
1470 | // Drop-down menu |
1471 | $commands = $this->threadCommands( $thread ); |
1472 | $menuHTML = Xml::tags( 'ul', [ 'class' => 'lqt-thread-toolbar-command-list' ], |
1473 | $this->listItemsForCommands( $commands ) ); |
1474 | |
1475 | $triggerText = Xml::tags( 'a', [ |
1476 | 'class' => 'lqt-thread-actions-icon', |
1477 | 'href' => '#' |
1478 | ], |
1479 | wfMessage( 'lqt-menu-trigger' )->escaped() ); |
1480 | $dropdownTrigger = Xml::tags( 'div', |
1481 | [ 'class' => 'lqt-thread-actions-trigger ' . |
1482 | 'lqt-command-icon', 'style' => 'display: none;' ], |
1483 | $triggerText ); |
1484 | |
1485 | if ( count( $commands ) ) { |
1486 | $headerParts[] = Xml::tags( 'li', |
1487 | [ 'class' => 'lqt-thread-toolbar-menu' ], |
1488 | $dropdownTrigger ); |
1489 | } |
1490 | |
1491 | $html .= implode( ' ', $headerParts ); |
1492 | |
1493 | $html = Xml::tags( 'ul', [ 'class' => 'lqt-thread-toolbar-commands' ], $html ); |
1494 | |
1495 | $html = Xml::tags( 'div', [ 'class' => 'lqt-thread-toolbar' ], $html ) . |
1496 | $menuHTML; |
1497 | |
1498 | return $html; |
1499 | } |
1500 | |
1501 | public function listItemsForCommands( $commands ) { |
1502 | $result = []; |
1503 | foreach ( $commands as $key => $command ) { |
1504 | $thisCommand = $this->contentForCommand( $command ); |
1505 | |
1506 | $thisCommand = Xml::tags( |
1507 | 'li', |
1508 | [ 'class' => 'lqt-command lqt-command-' . $key ], |
1509 | $thisCommand |
1510 | ); |
1511 | |
1512 | $result[] = $thisCommand; |
1513 | } |
1514 | return implode( ' ', $result ); |
1515 | } |
1516 | |
1517 | /** |
1518 | * @param array $command |
1519 | * @param bool $icon_divs |
1520 | * @return string HTML |
1521 | */ |
1522 | public function contentForCommand( $command, $icon_divs = true ) { |
1523 | $label = $command['label']; |
1524 | $href = $command['href']; |
1525 | $enabled = $command['enabled']; |
1526 | $tooltip = $command['tooltip'] ?? ''; |
1527 | |
1528 | if ( isset( $command['icon'] ) ) { |
1529 | $icon = Xml::tags( 'div', [ 'title' => $label, |
1530 | 'class' => 'lqt-command-icon' ], ' ' ); |
1531 | if ( $icon_divs ) { |
1532 | if ( !empty( $command['showlabel'] ) ) { |
1533 | $fullLabel = $icon . ' ' . $label; |
1534 | } else { |
1535 | $fullLabel = $icon; |
1536 | } |
1537 | } elseif ( empty( $command['showlabel'] ) ) { |
1538 | $fullLabel = ''; |
1539 | } else { |
1540 | $fullLabel = $label; |
1541 | } |
1542 | } else { |
1543 | $fullLabel = $label; |
1544 | } |
1545 | |
1546 | if ( $enabled ) { |
1547 | $thisCommand = Xml::tags( 'a', [ 'href' => $href, 'title' => $tooltip ], |
1548 | $fullLabel ); |
1549 | } else { |
1550 | $thisCommand = Xml::tags( 'span', [ 'class' => 'lqt_command_disabled', |
1551 | 'title' => $tooltip ], $fullLabel ); |
1552 | } |
1553 | |
1554 | return $thisCommand; |
1555 | } |
1556 | |
1557 | /** |
1558 | * Shows a normal (i.e. not deleted or moved) thread body |
1559 | * @param Thread $thread |
1560 | */ |
1561 | public function showThreadBody( Thread $thread ) { |
1562 | $post = $thread->root(); |
1563 | |
1564 | $divClass = $this->postDivClass( $thread ); |
1565 | $html = ''; |
1566 | |
1567 | // This is a bit of a hack to have individual histories work. |
1568 | // We can grab oldid either from lqt_oldid (which is a thread rev), |
1569 | // or from oldid (which is a page rev). But oldid only applies to the |
1570 | // thread being requested, not any replies. |
1571 | $page_rev = $this->request->getVal( 'oldid', null ); |
1572 | if ( $page_rev !== null && $this->title->equals( $thread->root()->getTitle() ) ) { |
1573 | $oldid = $page_rev; |
1574 | } else { |
1575 | $oldid = $thread->isHistorical() ? $thread->rootRevision() : null; |
1576 | } |
1577 | |
1578 | $hookContainer = MediaWikiServices::getInstance()->getHookContainer(); |
1579 | // If we're editing the thread, show the editing form. |
1580 | $showAnything = $hookContainer->run( 'LiquidThreadsShowThreadBody', |
1581 | [ $thread ] ); |
1582 | if ( $this->methodAppliesToThread( 'edit', $thread ) && $showAnything ) { |
1583 | $html = Xml::openElement( 'div', [ 'class' => $divClass ] ); |
1584 | $this->output->addHTML( $html ); |
1585 | $html = ''; |
1586 | |
1587 | // No way am I refactoring EditForm to return its output as HTML. |
1588 | // so I'm just flushing the HTML and displaying it as-is. |
1589 | $this->showPostEditingForm( $thread ); |
1590 | $html .= Xml::closeElement( 'div' ); |
1591 | } elseif ( $showAnything ) { |
1592 | $html .= Xml::openElement( 'div', [ 'class' => $divClass ] ); |
1593 | |
1594 | $show = $hookContainer->run( 'LiquidThreadsShowPostContent', |
1595 | [ $thread, &$post ] ); |
1596 | if ( $show ) { |
1597 | $html .= $this->showPostBody( $post, $oldid ); |
1598 | } |
1599 | $html .= Xml::closeElement( 'div' ); |
1600 | $hookContainer->run( 'LiquidThreadsShowPostThreadBody', |
1601 | [ $thread, $this->request, &$html ] ); |
1602 | |
1603 | $html .= $this->showThreadToolbar( $thread ); |
1604 | $html .= $this->threadSignature( $thread ); |
1605 | } |
1606 | |
1607 | $this->output->addHTML( $html ); |
1608 | } |
1609 | |
1610 | /** |
1611 | * @param Thread $thread |
1612 | * @return string |
1613 | */ |
1614 | public function threadSignature( Thread $thread ) { |
1615 | global $wgLang; |
1616 | |
1617 | $signature = $thread->signature() ?? ''; |
1618 | $signature = self::parseSignature( $signature ); |
1619 | |
1620 | $signature = Xml::tags( 'span', [ 'class' => 'lqt-thread-user-signature' ], |
1621 | $signature ); |
1622 | |
1623 | $signature .= $wgLang->getDirMark(); |
1624 | |
1625 | $timestamp = $wgLang->timeanddate( $thread->created(), true ); |
1626 | $signature .= Xml::element( 'span', |
1627 | [ 'class' => 'lqt-thread-toolbar-timestamp' ], |
1628 | $timestamp ); |
1629 | |
1630 | MediaWikiServices::getInstance()->getHookContainer() |
1631 | ->run( 'LiquidThreadsThreadSignature', [ $thread, &$signature ] ); |
1632 | |
1633 | $signature = Xml::tags( 'div', [ 'class' => 'lqt-thread-signature' ], |
1634 | $signature ); |
1635 | |
1636 | return $signature; |
1637 | } |
1638 | |
1639 | /** |
1640 | * @param Thread $thread |
1641 | * @return string |
1642 | */ |
1643 | private function threadInfoPanel( Thread $thread ) { |
1644 | global $wgLang; |
1645 | |
1646 | $infoElements = []; |
1647 | |
1648 | // Check for edited flag. |
1649 | $editedFlag = $thread->editedness(); |
1650 | $ebLookup = [ Threads::EDITED_BY_AUTHOR => 'author', |
1651 | Threads::EDITED_BY_OTHERS => 'others' ]; |
1652 | $lastEdit = $thread->root()->getPage()->getTimestamp(); |
1653 | $lastEditTime = $wgLang->time( $lastEdit, false, true ); |
1654 | $lastEditDate = $wgLang->date( $lastEdit, false, true ); |
1655 | $lastEdit = $wgLang->timeanddate( $lastEdit, true ); |
1656 | $editors = ''; |
1657 | $editorCount = 0; |
1658 | |
1659 | if ( $editedFlag > Threads::EDITED_BY_AUTHOR ) { |
1660 | $editors = $thread->editors(); |
1661 | $editorCount = count( $editors ); |
1662 | $formattedEditors = []; |
1663 | |
1664 | foreach ( $editors as $ed ) { |
1665 | $id = IPUtils::isIPAddress( $ed ) ? 0 : 1; |
1666 | $fEditor = Linker::userLink( $id, $ed ) . |
1667 | Linker::userToolLinks( $id, $ed ); |
1668 | $formattedEditors[] = $fEditor; |
1669 | } |
1670 | |
1671 | $editors = $wgLang->commaList( $formattedEditors ); |
1672 | } |
1673 | |
1674 | if ( isset( $ebLookup[$editedFlag] ) ) { |
1675 | $editedBy = $ebLookup[$editedFlag]; |
1676 | $author = $thread->author(); |
1677 | // Used messages: lqt-thread-edited-author, lqt-thread-edited-others |
1678 | $editedNotice = wfMessage( "lqt-thread-edited-$editedBy" ) |
1679 | ->params( $lastEdit )->numParams( $editorCount ) |
1680 | ->params( $lastEditTime, $lastEditDate ) |
1681 | ->params( $author->getName() )->parse(); |
1682 | $editedNotice = str_replace( '$3', $editors, $editedNotice ); |
1683 | $infoElements[] = Xml::tags( 'div', [ 'class' => |
1684 | "lqt-thread-toolbar-edited-$editedBy" ], |
1685 | $editedNotice ); |
1686 | } |
1687 | |
1688 | MediaWikiServices::getInstance()->getHookContainer() |
1689 | ->run( 'LiquidThreadsThreadInfoPanel', [ $thread, &$infoElements ] ); |
1690 | |
1691 | if ( !count( $infoElements ) ) { |
1692 | return ''; |
1693 | } |
1694 | |
1695 | return Xml::tags( 'div', [ 'class' => 'lqt-thread-info-panel' ], |
1696 | implode( "\n", $infoElements ) ); |
1697 | } |
1698 | |
1699 | /** |
1700 | * Shows the headING for a thread (as opposed to the headeER for a post within |
1701 | * a thread). |
1702 | * @param Thread $thread |
1703 | * @return string |
1704 | */ |
1705 | public function showThreadHeading( Thread $thread ) { |
1706 | if ( $thread->hasDistinctSubject() ) { |
1707 | if ( $thread->hasSuperthread() ) { |
1708 | $commands_html = ""; |
1709 | } else { |
1710 | $commands = $this->topLevelThreadCommands( $thread ); |
1711 | $lis = $this->listItemsForCommands( $commands ); |
1712 | $id = 'lqt-threadlevel-commands-' . $thread->id(); |
1713 | $commands_html = Xml::tags( 'ul', |
1714 | [ 'class' => 'lqt_threadlevel_commands', |
1715 | 'id' => $id ], |
1716 | $lis ); |
1717 | } |
1718 | |
1719 | $id = 'lqt-header-' . $thread->id(); |
1720 | |
1721 | $services = MediaWikiServices::getInstance(); |
1722 | $langConv = $services |
1723 | ->getLanguageConverterFactory() |
1724 | ->getLanguageConverter( $services->getContentLanguage() ); |
1725 | $html = $langConv->convert( $thread->formattedSubject() ); |
1726 | |
1727 | $show = $services->getHookContainer()->run( 'LiquidThreadsShowThreadHeading', [ $thread, &$html ] ); |
1728 | |
1729 | if ( $show ) { |
1730 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
1731 | $html = Xml::tags( 'span', [ 'class' => 'mw-headline' ], $html ); |
1732 | $html .= Html::hidden( 'raw-header', $thread->subject() ); |
1733 | $html = Xml::tags( 'h' . $this->headerLevel, |
1734 | [ 'class' => 'lqt_header', 'id' => $id, 'dir' => $contLang->getDir() ], |
1735 | $html ) . $commands_html; |
1736 | } |
1737 | |
1738 | // wrap it all in a container |
1739 | $html = Xml::tags( 'div', |
1740 | [ 'class' => 'lqt_thread_heading' ], |
1741 | $html ); |
1742 | return $html; |
1743 | } |
1744 | |
1745 | return ''; |
1746 | } |
1747 | |
1748 | public function postDivClass( Thread $thread ) { |
1749 | $levelClass = 'lqt-thread-nest-' . $this->threadNestingLevel; |
1750 | $alternatingType = ( $this->threadNestingLevel % 2 ) ? 'odd' : 'even'; |
1751 | $alternatingClass = "lqt-thread-$alternatingType"; |
1752 | $dir = MediaWikiServices::getInstance()->getContentLanguage()->getDir(); |
1753 | |
1754 | return "lqt_post $levelClass $alternatingClass mw-content-$dir"; |
1755 | } |
1756 | |
1757 | /** |
1758 | * @param Thread $thread |
1759 | * @return string |
1760 | */ |
1761 | public static function anchorName( Thread $thread ) { |
1762 | return $thread->getAnchorName(); |
1763 | } |
1764 | |
1765 | /** |
1766 | * Display a moved thread |
1767 | * |
1768 | * @param Thread $thread |
1769 | * @throws Exception |
1770 | */ |
1771 | public function showMovedThread( Thread $thread ) { |
1772 | global $wgLang; |
1773 | |
1774 | // Grab target thread |
1775 | if ( !$thread->title() ) { |
1776 | return; // Odd case: moved thread with no title? |
1777 | } |
1778 | |
1779 | $article = new Article( $thread->title(), 0 ); |
1780 | $target = $article->getPage()->getRedirectTarget(); |
1781 | |
1782 | if ( !$target ) { |
1783 | $content = $article->getPage()->getContent(); |
1784 | $contentText = ( $content instanceof TextContent ) ? $content->getText() : ''; |
1785 | throw new LogicException( "Thread " . $thread->id() . ' purports to be moved, ' . |
1786 | 'but no redirect found in text (' . $contentText . ') of ' . |
1787 | $thread->root()->getTitle()->getPrefixedText() . '. Dying.' |
1788 | ); |
1789 | } |
1790 | |
1791 | $t_thread = Threads::withRoot( |
1792 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $target ) |
1793 | ); |
1794 | |
1795 | // Grab data about the new post. |
1796 | $author = $thread->author(); |
1797 | $sig = Linker::userLink( $author->getId(), $author->getName() ) . |
1798 | Linker::userToolLinks( $author->getId(), $author->getName() ); |
1799 | $newTalkpage = is_object( $t_thread ) ? $t_thread->getTitle() : ''; |
1800 | |
1801 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
1802 | $html = wfMessage( 'lqt_move_placeholder' ) |
1803 | ->rawParams( $linkRenderer->makeLink( $target ), $sig ) |
1804 | ->params( $wgLang->date( $thread->modified() ), $wgLang->time( $thread->modified() ) ) |
1805 | ->rawParams( $newTalkpage ? $linkRenderer->makeLink( $newTalkpage ) : '' )->parse(); |
1806 | |
1807 | $this->output->addHTML( $html ); |
1808 | } |
1809 | |
1810 | /** |
1811 | * Shows a deleted thread. Returns true to show the thread body |
1812 | * @param Thread $thread |
1813 | * @return bool |
1814 | */ |
1815 | public function showDeletedThread( $thread ) { |
1816 | if ( $this->user->isAllowed( 'deletedhistory' ) ) { |
1817 | $this->output->addWikiMsg( 'lqt_thread_deleted_for_sysops' ); |
1818 | return true; |
1819 | } else { |
1820 | $msg = wfMessage( 'lqt_thread_deleted' )->parse(); |
1821 | $msg = Xml::tags( 'em', null, $msg ); |
1822 | $msg = Xml::tags( 'p', null, $msg ); |
1823 | |
1824 | $this->output->addHTML( $msg ); |
1825 | return false; |
1826 | } |
1827 | } |
1828 | |
1829 | /** |
1830 | * Shows a single thread, rather than a thread tree. |
1831 | * |
1832 | * @param Thread $thread |
1833 | */ |
1834 | public function showSingleThread( Thread $thread ) { |
1835 | $html = ''; |
1836 | |
1837 | // If it's a 'moved' thread, show the placeholder |
1838 | if ( $thread->type() == Threads::TYPE_MOVED ) { |
1839 | $this->showMovedThread( $thread ); |
1840 | return; |
1841 | } elseif ( $thread->type() == Threads::TYPE_DELETED ) { |
1842 | $res = $this->showDeletedThread( $thread ); |
1843 | |
1844 | if ( !$res ) { |
1845 | return; |
1846 | } |
1847 | } |
1848 | |
1849 | $this->output->addHTML( $this->threadInfoPanel( $thread ) ); |
1850 | |
1851 | if ( $thread->summary() ) { |
1852 | $html .= $this->getSummary( $thread ); |
1853 | } |
1854 | |
1855 | // Unfortunately, I can't rewrite showRootPost() to pass back HTML |
1856 | // as it would involve rewriting EditPage, which I do NOT intend to do. |
1857 | |
1858 | $this->output->addHTML( $html ); |
1859 | |
1860 | $this->showThreadBody( $thread ); |
1861 | } |
1862 | |
1863 | /** |
1864 | * @param (Thread|int)[] $threads |
1865 | * @return Thread[] |
1866 | */ |
1867 | public function getMustShowThreads( array $threads = [] ) { |
1868 | if ( $this->request->getCheck( 'lqt_operand' ) ) { |
1869 | $operands = explode( ',', $this->request->getVal( 'lqt_operand' ) ); |
1870 | $operands = array_filter( $operands, 'ctype_digit' ); |
1871 | $threads = array_merge( $threads, $operands ); |
1872 | } |
1873 | |
1874 | if ( $this->request->getCheck( 'lqt_mustshow' ) ) { |
1875 | // Check for must-show in the request |
1876 | $specifiedMustShow = $this->request->getVal( 'lqt_mustshow' ); |
1877 | $specifiedMustShow = explode( ',', $specifiedMustShow ); |
1878 | $specifiedMustShow = array_filter( $specifiedMustShow, 'ctype_digit' ); |
1879 | |
1880 | $threads = array_merge( $threads, $specifiedMustShow ); |
1881 | } |
1882 | |
1883 | foreach ( $threads as $walk_thread ) { |
1884 | do { |
1885 | if ( !is_object( $walk_thread ) ) { |
1886 | $walk_thread = Threads::withId( $walk_thread ); |
1887 | } |
1888 | |
1889 | if ( !is_object( $walk_thread ) ) { |
1890 | continue; |
1891 | } |
1892 | |
1893 | $threads[$walk_thread->id()] = $walk_thread; |
1894 | $walk_thread = $walk_thread->superthread(); |
1895 | } while ( $walk_thread ); |
1896 | } |
1897 | |
1898 | return $threads; |
1899 | } |
1900 | |
1901 | /** |
1902 | * @param Thread $thread |
1903 | * @param Thread $st |
1904 | * @param int $i |
1905 | * @return string |
1906 | */ |
1907 | public function getShowMore( Thread $thread, $st, $i ) { |
1908 | $linkText = new HtmlArmor( wfMessage( 'lqt-thread-show-more' )->parse() ); |
1909 | $linkTitle = clone $thread->topmostThread()->title(); |
1910 | $linkTitle->setFragment( '#' . $st->getAnchorName() ); |
1911 | |
1912 | $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
1913 | $linkTitle, |
1914 | $linkText, |
1915 | [ |
1916 | 'class' => 'lqt-show-more-posts', |
1917 | ] |
1918 | ); |
1919 | $link .= Html::hidden( 'lqt-thread-start-at', (string)$i, |
1920 | [ 'class' => 'lqt-thread-start-at' ] ); |
1921 | |
1922 | return $link; |
1923 | } |
1924 | |
1925 | /** |
1926 | * When some replies are hidden because the are nested too deep, |
1927 | * this method creates a link that can be used to show the hidden |
1928 | * threats. |
1929 | * @param Thread $thread |
1930 | * @return string Html |
1931 | */ |
1932 | public function getShowReplies( Thread $thread ) { |
1933 | $linkText = new HtmlArmor( wfMessage( 'lqt-thread-show-replies' ) |
1934 | ->numParams( $thread->replyCount() ) |
1935 | ->parse() ); |
1936 | $linkTitle = clone $thread->topmostThread()->title(); |
1937 | $linkTitle->setFragment( '#' . $thread->getAnchorName() ); |
1938 | |
1939 | $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
1940 | $linkTitle, |
1941 | $linkText, |
1942 | [ |
1943 | 'class' => 'lqt-show-replies', |
1944 | ] |
1945 | ); |
1946 | $link = Xml::tags( 'div', [ 'class' => 'lqt-thread-replies' ], $link ); |
1947 | |
1948 | return $link; |
1949 | } |
1950 | |
1951 | /** |
1952 | * @param Thread $thread |
1953 | * @return bool |
1954 | */ |
1955 | public static function threadContainsRepliesWithContent( Thread $thread ) { |
1956 | $replies = $thread->replies(); |
1957 | |
1958 | foreach ( $replies as $reply ) { |
1959 | $content = ''; |
1960 | if ( $reply->root() ) { |
1961 | $pageContent = $reply->root()->getPage()->getContent(); |
1962 | $content = ( $pageContent instanceof TextContent ) ? $pageContent->getText() : ''; |
1963 | } |
1964 | |
1965 | if ( $content !== null && trim( $content ) != '' ) { |
1966 | return true; |
1967 | } |
1968 | |
1969 | if ( self::threadContainsRepliesWithContent( $reply ) ) { |
1970 | return true; |
1971 | } |
1972 | |
1973 | if ( $reply->type() == Threads::TYPE_MOVED ) { |
1974 | return true; |
1975 | } |
1976 | } |
1977 | |
1978 | return false; |
1979 | } |
1980 | |
1981 | /** |
1982 | * @param Thread $thread |
1983 | * @param int $startAt |
1984 | * @param int $maxCount |
1985 | * @param bool $showThreads |
1986 | * @param array $cascadeOptions |
1987 | * @param bool $interruption |
1988 | */ |
1989 | public function showThreadReplies( Thread $thread, $startAt, $maxCount, $showThreads, |
1990 | $cascadeOptions, $interruption = false ) { |
1991 | $repliesClass = 'lqt-thread-replies lqt-thread-replies-' . |
1992 | $this->threadNestingLevel; |
1993 | |
1994 | if ( $interruption ) { |
1995 | $repliesClass .= ' lqt-thread-replies-interruption'; |
1996 | } |
1997 | |
1998 | $div = Xml::openElement( 'div', [ 'class' => $repliesClass ] ); |
1999 | |
2000 | $subthreadCount = count( $thread->subthreads() ); |
2001 | $i = 0; |
2002 | $showCount = 0; |
2003 | $showThreads = true; |
2004 | |
2005 | $mustShowThreads = $cascadeOptions['mustShowThreads']; |
2006 | |
2007 | $replies = $thread->subthreads(); |
2008 | usort( $replies, [ 'Thread', 'createdSortCallback' ] ); |
2009 | |
2010 | foreach ( $replies as $st ) { |
2011 | ++$i; |
2012 | |
2013 | // Only show undeleted threads that are above our 'startAt' index. |
2014 | $shown = false; |
2015 | if ( $st->type() != Threads::TYPE_DELETED && |
2016 | $i >= $startAt && |
2017 | $showThreads ) { |
2018 | if ( $showCount > $maxCount && $maxCount > 0 ) { |
2019 | // We've shown too many threads. |
2020 | $link = $this->getShowMore( $thread, $st, $i ); |
2021 | |
2022 | $this->output->addHTML( $div . $link . '</div>' ); |
2023 | $showThreads = false; |
2024 | continue; |
2025 | } |
2026 | |
2027 | ++$showCount; |
2028 | if ( $showCount == 1 ) { |
2029 | // There's a post sep before each reply group to |
2030 | // separate from the parent thread. |
2031 | $this->output->addHTML( $div ); |
2032 | } |
2033 | |
2034 | $this->showThread( $st, $i, $subthreadCount, $cascadeOptions ); |
2035 | $shown = true; |
2036 | } |
2037 | |
2038 | // Handle must-show threads. |
2039 | // FIXME this thread will be duplicated if somebody clicks the |
2040 | // "show more" link (probably needs fixing in the JS) |
2041 | if ( $st->type() != Threads::TYPE_DELETED && !$shown && |
2042 | array_key_exists( $st->id(), $mustShowThreads ) ) { |
2043 | $this->showThread( $st, $i, $subthreadCount, $cascadeOptions ); |
2044 | } |
2045 | } |
2046 | |
2047 | // Show reply stuff |
2048 | $this->showReplyBox( $thread ); |
2049 | |
2050 | $finishDiv = ''; |
2051 | $finishDiv .= Xml::tags( |
2052 | 'div', |
2053 | [ 'class' => 'lqt-replies-finish' ], |
2054 | ' ' |
2055 | ); |
2056 | |
2057 | $this->output->addHTML( $finishDiv . Xml::closeElement( 'div' ) ); |
2058 | } |
2059 | |
2060 | /** |
2061 | * @param Thread $thread |
2062 | * @param int $levelNum |
2063 | * @param int $totalInLevel |
2064 | * @param array $options |
2065 | * @throws Exception |
2066 | */ |
2067 | public function showThread( Thread $thread, $levelNum = 1, $totalInLevel = 1, |
2068 | $options = [] |
2069 | ) { |
2070 | // Safeguard |
2071 | if ( $thread->type() & Threads::TYPE_DELETED || |
2072 | !$thread->root() ) { |
2073 | return; |
2074 | } |
2075 | |
2076 | $this->threadNestingLevel++; |
2077 | |
2078 | // Figure out which threads *need* to be shown because they're involved in an |
2079 | // operation |
2080 | $mustShowOption = $options['mustShowThreads'] ?? []; |
2081 | $mustShowThreads = $this->getMustShowThreads( $mustShowOption ); |
2082 | |
2083 | // For cascading. |
2084 | $options['mustShowThreads'] = $mustShowThreads; |
2085 | |
2086 | // Don't show blank posts unless we have to |
2087 | $content = ''; |
2088 | if ( $thread->root() ) { |
2089 | $pageContent = $thread->root()->getPage()->getContent(); |
2090 | $content = ( $pageContent instanceof TextContent ) ? $pageContent->getText() : ''; |
2091 | } |
2092 | |
2093 | if ( |
2094 | $content !== null && trim( $content ) == '' && |
2095 | $thread->type() != Threads::TYPE_MOVED && |
2096 | !self::threadContainsRepliesWithContent( $thread ) && |
2097 | !array_key_exists( $thread->id(), $mustShowThreads ) |
2098 | ) { |
2099 | $this->threadNestingLevel--; |
2100 | return; |
2101 | } |
2102 | |
2103 | // Grab options |
2104 | $services = MediaWikiServices::getInstance(); |
2105 | $userOptionsLookup = $services->getUserOptionsLookup(); |
2106 | $maxDepth = $options['maxDepth'] ?? $userOptionsLookup->getOption( $this->user, 'lqtdisplaydepth' ); |
2107 | $maxCount = $options['maxCount'] ?? $userOptionsLookup->getOption( $this->user, 'lqtdisplaycount' ); |
2108 | $startAt = $options['startAt'] ?? 0; |
2109 | |
2110 | // Figure out if we have replies to show or not. |
2111 | $showThreads = ( $maxDepth == -1 ) || |
2112 | ( $this->threadNestingLevel <= $maxDepth ); |
2113 | $mustShowThreadIds = array_keys( $mustShowThreads ); |
2114 | $subthreadIds = array_keys( $thread->replies() ); |
2115 | $mustShowSubthreadIds = array_intersect( $mustShowThreadIds, $subthreadIds ); |
2116 | |
2117 | $hasSubthreads = self::threadContainsRepliesWithContent( $thread ); |
2118 | $hasSubthreads = $hasSubthreads || count( $mustShowSubthreadIds ); |
2119 | // Show subthreads if one of the subthreads is on the must-show list |
2120 | $showThreads = $showThreads || |
2121 | count( array_intersect( |
2122 | array_keys( $mustShowThreads ), array_keys( $thread->replies() ) |
2123 | ) ); |
2124 | $replyTo = $this->methodAppliesToThread( 'reply', $thread ); |
2125 | |
2126 | $this->output->addModules( 'ext.liquidThreads' ); |
2127 | |
2128 | $html = ''; |
2129 | $services->getHookContainer()->run( 'EditPageBeforeEditToolbar', [ &$html ] ); |
2130 | |
2131 | $class = $this->threadDivClass( $thread ); |
2132 | if ( $levelNum == 1 ) { |
2133 | $class .= ' lqt-thread-first'; |
2134 | } elseif ( $levelNum == $totalInLevel ) { |
2135 | $class .= ' lqt-thread-last'; |
2136 | } |
2137 | |
2138 | if ( $hasSubthreads && $showThreads ) { |
2139 | $class .= ' lqt-thread-with-subthreads'; |
2140 | } else { |
2141 | $class .= ' lqt-thread-no-subthreads'; |
2142 | } |
2143 | |
2144 | if ( !$services->getPermissionManager() |
2145 | ->quickUserCan( 'edit', $this->user, $thread->title() ) |
2146 | || !LqtDispatch::isLqtPage( $thread->getTitle() ) |
2147 | ) { |
2148 | $class .= ' lqt-thread-uneditable'; |
2149 | } |
2150 | |
2151 | $class .= ' lqt-thread-wrapper'; |
2152 | |
2153 | $html .= Xml::openElement( |
2154 | 'div', |
2155 | [ |
2156 | 'class' => $class, |
2157 | 'id' => 'lqt_thread_id_' . $thread->id() |
2158 | ] |
2159 | ); |
2160 | |
2161 | $html .= Xml::element( 'a', [ 'name' => $this->anchorName( $thread ) ], ' ' ); |
2162 | $html .= $this->showThreadHeading( $thread ); |
2163 | |
2164 | // Metadata stuck in the top of the lqt_thread div. |
2165 | // Modified time for topmost threads... |
2166 | if ( $thread->isTopmostThread() ) { |
2167 | $html .= Html::hidden( |
2168 | 'lqt-thread-modified-' . $thread->id(), |
2169 | wfTimestamp( TS_MW, $thread->modified() ), |
2170 | [ |
2171 | 'id' => 'lqt-thread-modified-' . $thread->id(), |
2172 | 'class' => 'lqt-thread-modified' |
2173 | ] |
2174 | ); |
2175 | $html .= Html::hidden( |
2176 | 'lqt-thread-sortkey', |
2177 | $thread->sortkey(), |
2178 | [ 'id' => 'lqt-thread-sortkey-' . $thread->id() ] |
2179 | ); |
2180 | |
2181 | $html .= Html::hidden( |
2182 | 'lqt-thread-talkpage-' . $thread->id(), |
2183 | $thread->article()->getTitle()->getPrefixedText(), |
2184 | [ |
2185 | 'class' => 'lqt-thread-talkpage-metadata', |
2186 | ] |
2187 | ); |
2188 | } |
2189 | |
2190 | if ( !$thread->title() ) { |
2191 | throw new LogicException( "Thread " . $thread->id() . " has null title" ); |
2192 | } |
2193 | |
2194 | // Add the thread's title |
2195 | $html .= Html::hidden( |
2196 | 'lqt-thread-title-' . $thread->id(), |
2197 | $thread->title()->getPrefixedText(), |
2198 | [ |
2199 | 'id' => 'lqt-thread-title-' . $thread->id(), |
2200 | 'class' => 'lqt-thread-title-metadata' |
2201 | ] |
2202 | ); |
2203 | |
2204 | // Flush output to display thread |
2205 | $this->output->addHTML( $html ); |
2206 | $this->output->addHTML( Xml::openElement( 'div', |
2207 | [ 'class' => 'lqt-post-wrapper' ] ) ); |
2208 | $this->showSingleThread( $thread ); |
2209 | $this->output->addHTML( Xml::closeElement( 'div' ) ); |
2210 | |
2211 | $cascadeOptions = $options; |
2212 | unset( $cascadeOptions['startAt'] ); |
2213 | |
2214 | $replyInterruption = $levelNum < $totalInLevel; |
2215 | |
2216 | if ( ( $hasSubthreads && $showThreads ) ) { |
2217 | // If the thread has subthreads, and we want to show them, we should do so. |
2218 | $this->showThreadReplies( $thread, $startAt, $maxCount, $showThreads, |
2219 | $cascadeOptions, $replyInterruption ); |
2220 | } elseif ( $hasSubthreads && !$showThreads ) { |
2221 | // If the thread has subthreads, but we don't want to show them, then |
2222 | // show the reply form if necessary, and add the "Show X replies" link. |
2223 | if ( $replyTo ) { |
2224 | $this->showReplyForm( $thread ); |
2225 | } |
2226 | |
2227 | // Add a "show subthreads" link. |
2228 | $link = $this->getShowReplies( $thread ); |
2229 | |
2230 | $this->output->addHTML( $link ); |
2231 | |
2232 | if ( $levelNum < $totalInLevel ) { |
2233 | $this->output->addHTML( |
2234 | Xml::tags( 'div', [ 'class' => 'lqt-post-sep' ], ' ' ) ); |
2235 | } |
2236 | } elseif ( $levelNum < $totalInLevel ) { |
2237 | // If we have no replies, and we're not at the end of this level, add the post separator |
2238 | // and a reply box if necessary. |
2239 | $this->output->addHTML( |
2240 | Xml::tags( 'div', [ 'class' => 'lqt-post-sep' ], ' ' ) ); |
2241 | |
2242 | if ( $replyTo ) { |
2243 | $class = 'lqt-thread-replies lqt-thread-replies-' . |
2244 | $this->threadNestingLevel; |
2245 | $html = Xml::openElement( 'div', [ 'class' => $class ] ); |
2246 | $this->output->addHTML( $html ); |
2247 | |
2248 | $this->showReplyForm( $thread ); |
2249 | |
2250 | $finishDiv = Xml::tags( 'div', |
2251 | [ 'class' => 'lqt-replies-finish' ], |
2252 | ' ' ); |
2253 | // Layout plus close div.lqt-thread-replies |
2254 | |
2255 | $finishHTML = Xml::closeElement( 'div' ); // lqt-reply-form |
2256 | $finishHTML .= $finishDiv; // Layout |
2257 | $finishHTML .= Xml::closeElement( 'div' ); // lqt-thread-replies |
2258 | $this->output->addHTML( $finishHTML ); |
2259 | } |
2260 | } elseif ( !$hasSubthreads && $replyTo ) { |
2261 | // If we have no replies, we're at the end of this level, and we want to reply, |
2262 | // show the reply box. |
2263 | $class = 'lqt-thread-replies lqt-thread-replies-' . |
2264 | $this->threadNestingLevel; |
2265 | $html = Xml::openElement( 'div', [ 'class' => $class ] ); |
2266 | $this->output->addHTML( $html ); |
2267 | |
2268 | $this->showReplyForm( $thread ); |
2269 | |
2270 | $html = Xml::tags( 'div', |
2271 | [ 'class' => 'lqt-replies-finish' ], |
2272 | Xml::tags( 'div', |
2273 | [ 'class' => |
2274 | 'lqt-replies-finish-corner' |
2275 | ], ' ' ) ); |
2276 | $html .= Xml::closeElement( 'div' ); |
2277 | $this->output->addHTML( $html ); |
2278 | } |
2279 | |
2280 | $this->output->addHTML( Xml::closeElement( 'div' ) ); |
2281 | |
2282 | $this->threadNestingLevel--; |
2283 | } |
2284 | |
2285 | /** |
2286 | * @param Thread $thread |
2287 | */ |
2288 | public function showReplyBox( Thread $thread ) { |
2289 | // Check if we're actually replying to this thread. |
2290 | if ( $this->methodAppliesToThread( 'reply', $thread ) ) { |
2291 | $this->showReplyForm( $thread ); |
2292 | return; |
2293 | } elseif ( !$thread->canUserReply( $this->user, 'quick' ) ) { |
2294 | return; |
2295 | } |
2296 | |
2297 | $html = ''; |
2298 | $html .= Xml::tags( 'div', |
2299 | [ 'class' => 'lqt-reply-form lqt-edit-form', |
2300 | 'style' => 'display: none;' ], |
2301 | '' |
2302 | ); |
2303 | |
2304 | $this->output->addHTML( $html ); |
2305 | } |
2306 | |
2307 | /** |
2308 | * @param Thread $thread |
2309 | * @return string |
2310 | */ |
2311 | public function threadDivClass( Thread $thread ) { |
2312 | $levelClass = 'lqt-thread-nest-' . $this->threadNestingLevel; |
2313 | $alternatingType = ( $this->threadNestingLevel % 2 ) ? 'odd' : 'even'; |
2314 | $alternatingClass = "lqt-thread-$alternatingType"; |
2315 | $topmostClass = $thread->isTopmostThread() ? ' lqt-thread-topmost' : ''; |
2316 | |
2317 | return "lqt_thread $levelClass $alternatingClass$topmostClass"; |
2318 | } |
2319 | |
2320 | /** |
2321 | * @param Thread $t |
2322 | * @return string |
2323 | */ |
2324 | public function getSummary( $t ) { |
2325 | if ( !$t->summary() ) { |
2326 | return ''; |
2327 | } |
2328 | if ( !( $t->summary()->getPage()->getContent() instanceof TextContent ) ) { |
2329 | return ''; // Blank summary |
2330 | } |
2331 | |
2332 | $label = wfMessage( 'lqt_summary_label' )->parse(); |
2333 | $edit_text = wfMessage( 'edit' )->text(); |
2334 | $link_text = new HtmlArmor( wfMessage( 'lqt_permalink' )->parse() ); |
2335 | |
2336 | $html = ''; |
2337 | |
2338 | $html .= Xml::tags( |
2339 | 'span', |
2340 | [ 'class' => 'lqt_thread_permalink_summary_title' ], |
2341 | $label |
2342 | ); |
2343 | |
2344 | $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); |
2345 | $link = $linkRenderer->makeLink( |
2346 | $t->summary()->getTitle(), |
2347 | $link_text, |
2348 | [ |
2349 | 'class' => 'lqt-summary-link', |
2350 | ] |
2351 | ); |
2352 | $link .= Html::hidden( 'summary-title', $t->summary()->getTitle()->getPrefixedText() ); |
2353 | $edit_link = self::permalink( $t, $edit_text, 'summarize', $t->id() ); |
2354 | $links = "[$link]\n[$edit_link]"; |
2355 | $html .= Xml::tags( |
2356 | 'span', |
2357 | [ 'class' => 'lqt_thread_permalink_summary_actions' ], |
2358 | $links |
2359 | ); |
2360 | |
2361 | $summary_body = $this->showPostBody( $t->summary() ); |
2362 | $html .= Xml::tags( |
2363 | 'div', |
2364 | [ 'class' => 'lqt_thread_permalink_summary_body' ], |
2365 | $summary_body |
2366 | ); |
2367 | |
2368 | $html = Xml::tags( |
2369 | 'div', |
2370 | [ 'class' => 'lqt_thread_permalink_summary' ], |
2371 | $html |
2372 | ); |
2373 | |
2374 | return $html; |
2375 | } |
2376 | |
2377 | /** |
2378 | * @param User|int|string $user |
2379 | * @return string |
2380 | */ |
2381 | public function getSignature( $user ) { |
2382 | if ( is_object( $user ) ) { |
2383 | $uid = $user->getId(); |
2384 | } elseif ( is_int( $user ) ) { |
2385 | $uid = $user; |
2386 | $user = User::newFromId( $uid ); |
2387 | } else { |
2388 | $user = User::newFromName( $user ); |
2389 | $uid = $user->getId(); |
2390 | } |
2391 | |
2392 | return $this->getUserSignature( $user, $uid ); |
2393 | } |
2394 | |
2395 | /** |
2396 | * @param User|null $user |
2397 | * @param int|null $uid |
2398 | * @return string |
2399 | */ |
2400 | public static function getUserSignature( $user, $uid = null ) { |
2401 | $parser = MediaWikiServices::getInstance()->getParser(); |
2402 | |
2403 | if ( !$user ) { |
2404 | $user = User::newFromId( $uid ); |
2405 | } |
2406 | |
2407 | $parser->setOptions( new ParserOptions( $user ) ); |
2408 | |
2409 | $sig = $parser->getUserSig( $user ); |
2410 | |
2411 | return $sig; |
2412 | } |
2413 | |
2414 | /** |
2415 | * @param string $sig |
2416 | * @return string |
2417 | */ |
2418 | public static function parseSignature( $sig ) { |
2419 | static $parseCache = []; |
2420 | $sigKey = md5( $sig ); |
2421 | |
2422 | if ( isset( $parseCache[$sigKey] ) ) { |
2423 | return $parseCache[$sigKey]; |
2424 | } |
2425 | |
2426 | $out = RequestContext::getMain()->getOutput(); |
2427 | $sig = Parser::stripOuterParagraph( |
2428 | $out->parseAsContent( $sig ) |
2429 | ); |
2430 | |
2431 | $parseCache[$sigKey] = $sig; |
2432 | |
2433 | return $sig; |
2434 | } |
2435 | |
2436 | /** |
2437 | * @param string $sig |
2438 | * @param User $user |
2439 | * @return string |
2440 | */ |
2441 | public static function signaturePST( $sig, $user ) { |
2442 | global $wgTitle; |
2443 | |
2444 | $title = $wgTitle ?: $user->getUserPage(); |
2445 | |
2446 | $sig = MediaWikiServices::getInstance()->getParser()->preSaveTransform( |
2447 | $sig, |
2448 | $title, |
2449 | $user, |
2450 | new ParserOptions( $user ), |
2451 | true |
2452 | ); |
2453 | |
2454 | return $sig; |
2455 | } |
2456 | |
2457 | public function customizeNavigation( $skin, &$links ) { |
2458 | // No-op |
2459 | } |
2460 | |
2461 | public function show() { |
2462 | return true; // No-op |
2463 | } |
2464 | |
2465 | /** |
2466 | * Copy-and-modify of MediaWiki\CommentFormatter\CommentFormatter::format |
2467 | * |
2468 | * @param string $s |
2469 | * @return string |
2470 | */ |
2471 | public static function formatSubject( $s ) { |
2472 | # Sanitize text a bit: |
2473 | $s = str_replace( "\n", " ", $s ); |
2474 | # Allow HTML entities |
2475 | $s = Sanitizer::escapeHtmlAllowEntities( $s ); |
2476 | |
2477 | # Render links: |
2478 | return MediaWikiServices::getInstance()->getCommentFormatter() |
2479 | ->formatLinks( $s, null, false ); |
2480 | } |
2481 | } |