Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 549 |
|
0.00% |
0 / 33 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 549 |
|
0.00% |
0 / 33 |
19740 | |
0.00% |
0 / 1 |
customizeOldChangesList | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
setNewtalkHTML | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
beforeWatchlist | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
getPreferences | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
20 | |||
updateNewtalkOnEdit | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
dumpThreadData | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
90 | |||
modifyExportQuery | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
customiseSearchResultTitle | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onUserRename | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onUserMergeAccountFields | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
editCheckboxes | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
56 | |||
customiseSearchProfiles | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
onLoadExtensionSchemaUpdates | |
0.00% |
0 / 87 |
|
0.00% |
0 / 1 |
12 | |||
onPageMoveComplete | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
onMovePageIsValidMove | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
userIsBlockedFrom | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
272 | |||
onSkinTemplateNavigation | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
onPageSaveComplete | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
getProtectionTypes | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
handlePageXMLTag | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
72 | |||
afterImportPage | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
182 | |||
applyPendingThreadRelationship | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
applyPendingArticleRelationship | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
loadPendingRelationships | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
addPendingRelationship | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
onGetUserPermissionsErrors | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
onParserFirstCallInit | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
onCanonicalNamespaces | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onAPIQueryAfterExecute | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
onInfoAction | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialPage_initList | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onRegistration | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onPreferencesGetIcon | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\LiquidThreads; |
4 | |
5 | use ApiQueryInfo; |
6 | use Article; |
7 | use ChangesList; |
8 | use DatabaseUpdater; |
9 | use HtmlArmor; |
10 | use LqtDispatch; |
11 | use LqtParserFunctions; |
12 | use LqtView; |
13 | use MediaWiki\EditPage\EditPage; |
14 | use MediaWiki\Linker\LinkTarget; |
15 | use MediaWiki\MediaWikiServices; |
16 | use MediaWiki\Output\OutputPage; |
17 | use MediaWiki\RenameUser\RenameuserSQL; |
18 | use MediaWiki\Revision\RevisionRecord; |
19 | use MediaWiki\SpecialPage\SpecialPage; |
20 | use MediaWiki\Storage\EditResult; |
21 | use MediaWiki\Title\Title; |
22 | use MediaWiki\User\User; |
23 | use MediaWiki\User\UserIdentity; |
24 | use MessageSpecifier; |
25 | use NewMessages; |
26 | use Parser; |
27 | use RecentChange; |
28 | use RequestContext; |
29 | use TextContent; |
30 | use Thread; |
31 | use Threads; |
32 | use UtfNormal\Validator; |
33 | use WikiImporter; |
34 | use WikiPage; |
35 | use Xml; |
36 | use XMLReader; |
37 | |
38 | class Hooks { |
39 | /** @var string|null Used to inform hooks about edits that are taking place. */ |
40 | public static $editType = null; |
41 | /** @var Thread|null Used to inform hooks about edits that are taking place. */ |
42 | public static $editThread = null; |
43 | /** @var Thread|null Used to inform hooks about edits that are taking place. */ |
44 | public static $editAppliesTo = null; |
45 | |
46 | /** |
47 | * @var Article|null |
48 | */ |
49 | public static $editArticle = null; |
50 | /** |
51 | * @var Article|null |
52 | */ |
53 | public static $editTalkpage = null; |
54 | |
55 | /** @var string[] */ |
56 | public static $editedStati = [ |
57 | Threads::EDITED_NEVER => 'never', |
58 | Threads::EDITED_HAS_REPLY => 'has-reply', |
59 | Threads::EDITED_BY_AUTHOR => 'by-author', |
60 | Threads::EDITED_BY_OTHERS => 'by-others' |
61 | ]; |
62 | /** @var string[] */ |
63 | public static $threadTypes = [ |
64 | Threads::TYPE_NORMAL => 'normal', |
65 | Threads::TYPE_MOVED => 'moved', |
66 | Threads::TYPE_DELETED => 'deleted' |
67 | ]; |
68 | |
69 | /** |
70 | * @param ChangesList $changeslist |
71 | * @param string &$s |
72 | * @param RecentChange $rc |
73 | * @param array &$classes |
74 | * @return bool |
75 | */ |
76 | public static function customizeOldChangesList( ChangesList $changeslist, &$s, $rc, &$classes ) { |
77 | $rcTitle = $rc->getTitle(); |
78 | if ( $rcTitle->getNamespace() != NS_LQT_THREAD ) { |
79 | return true; |
80 | } |
81 | |
82 | $thread = Threads::withRoot( MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $rcTitle ) ); |
83 | if ( !$thread ) { |
84 | return true; |
85 | } |
86 | |
87 | $changeslist->getOutput()->addModules( 'ext.liquidThreads' ); |
88 | $lang = $changeslist->getLanguage(); |
89 | |
90 | // Custom display for new posts. |
91 | if ( $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW ) { |
92 | // Article link, timestamp, user |
93 | $s = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
94 | $thread->getTitle() |
95 | ); |
96 | $changeslist->insertTimestamp( $s, $rc ); |
97 | $changeslist->insertUserRelatedLinks( $s, $rc ); |
98 | |
99 | // Action text |
100 | $msg = $thread->isTopmostThread() |
101 | ? 'lqt_rc_new_discussion' : 'lqt_rc_new_reply'; |
102 | $link = LqtView::linkInContext( $thread ); |
103 | $s .= ' ' . $changeslist->msg( $msg )->rawParams( $link )->parse(); |
104 | |
105 | $s .= $lang->getDirMark(); |
106 | |
107 | // add the truncated post content |
108 | $content = $thread->root()->getPage()->getContent(); |
109 | $quote = ( $content instanceof TextContent ) ? $content->getText() : ''; |
110 | $quote = $lang->truncateForVisual( $quote, 200 ); |
111 | $s .= ' ' . MediaWikiServices::getInstance()->getCommentFormatter()->formatBlock( $quote ); |
112 | |
113 | $classes = []; |
114 | $changeslist->insertTags( $s, $rc, $classes ); |
115 | $changeslist->insertExtra( $s, $rc, $classes ); |
116 | } |
117 | |
118 | return true; |
119 | } |
120 | |
121 | /** |
122 | * @param string &$newMessagesAlert |
123 | * @param array $newtalks |
124 | * @param User $user |
125 | * @param OutputPage $out |
126 | * @return bool |
127 | */ |
128 | public static function setNewtalkHTML( &$newMessagesAlert, $newtalks, $user, $out ) { |
129 | $usertalk_t = $user->getTalkPage(); |
130 | |
131 | // If the user isn't using LQT on their talk page, bail out |
132 | if ( !LqtDispatch::isLqtPage( $usertalk_t ) ) { |
133 | return true; |
134 | } |
135 | |
136 | $pageTitle = $out->getTitle(); |
137 | $newmsg_t = SpecialPage::getTitleFor( 'NewMessages' ); |
138 | $watchlist_t = SpecialPage::getTitleFor( 'Watchlist' ); |
139 | |
140 | if ( $newtalks |
141 | && !$newmsg_t->equals( $pageTitle ) |
142 | && !$watchlist_t->equals( $pageTitle ) |
143 | && !$usertalk_t->equals( $pageTitle ) |
144 | ) { |
145 | $newMessagesAlert = $out->msg( 'lqt_youhavenewmessages', $newmsg_t->getPrefixedText() )->parse(); |
146 | $out->setCdnMaxage( 0 ); |
147 | return true; |
148 | } |
149 | |
150 | return false; |
151 | } |
152 | |
153 | public static function beforeWatchlist( |
154 | $name, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts |
155 | ) { |
156 | global $wgLiquidThreadsEnableNewMessages; |
157 | |
158 | if ( !$wgLiquidThreadsEnableNewMessages ) { |
159 | return true; |
160 | } |
161 | |
162 | if ( $name !== 'Watchlist' ) { |
163 | return true; |
164 | } |
165 | |
166 | // Only reading from this DB, but presumably getting primary for latest content. |
167 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
168 | $context = RequestContext::getMain(); |
169 | $user = $context->getUser(); |
170 | |
171 | if ( !in_array( 'page', $tables ) ) { |
172 | $tables[] = 'page'; |
173 | // Yes, this is the correct field to join to. Weird naming. |
174 | $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ]; |
175 | } |
176 | $conds[] = "page_namespace IS NULL OR page_namespace != " . $dbw->addQuotes( NS_LQT_THREAD ); |
177 | |
178 | $talkpage_messages = NewMessages::newUserMessages( $user ); |
179 | $tn = count( $talkpage_messages ); |
180 | |
181 | $watch_messages = NewMessages::watchedThreadsForUser( $user ); |
182 | $wn = count( $watch_messages ); |
183 | |
184 | if ( $tn == 0 && $wn == 0 ) { |
185 | return true; |
186 | } |
187 | |
188 | $out = $context->getOutput(); |
189 | $out->addModules( 'ext.liquidThreads' ); |
190 | $messages_title = SpecialPage::getTitleFor( 'NewMessages' ); |
191 | $new_messages = wfMessage( 'lqt-new-messages' )->parse(); |
192 | |
193 | $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( |
194 | $messages_title, |
195 | new HtmlArmor( $new_messages ), |
196 | [ 'class' => 'lqt_watchlist_messages_notice' ] ); |
197 | $out->addHTML( $link ); |
198 | |
199 | return true; |
200 | } |
201 | |
202 | public static function getPreferences( $user, &$preferences ) { |
203 | global $wgEnableEmail, $wgLqtTalkPages, $wgLiquidThreadsEnableNewMessages, $wgHiddenPrefs; |
204 | |
205 | if ( $wgEnableEmail ) { |
206 | $preferences['lqtnotifytalk'] = |
207 | [ |
208 | 'type' => 'toggle', |
209 | 'label-message' => 'lqt-preference-notify-talk', |
210 | 'section' => 'personal/email' |
211 | ]; |
212 | } |
213 | |
214 | $preferences['lqt-watch-threads'] = [ |
215 | 'type' => 'toggle', |
216 | 'label-message' => 'lqt-preference-watch-threads', |
217 | 'section' => 'watchlist/advancedwatchlist', |
218 | ]; |
219 | |
220 | // Display depth and count |
221 | $preferences['lqtdisplaydepth'] = [ |
222 | 'type' => 'int', |
223 | 'label-message' => 'lqt-preference-display-depth', |
224 | 'section' => 'lqt', |
225 | 'min' => 1, |
226 | ]; |
227 | |
228 | $preferences['lqtdisplaycount'] = [ |
229 | 'type' => 'int', |
230 | 'label-message' => 'lqt-preference-display-count', |
231 | 'section' => 'lqt', |
232 | 'min' => 1, |
233 | ]; |
234 | |
235 | // Don't show any preferences if the wiki's LQT status is frozen |
236 | if ( !( $wgLqtTalkPages || $wgLiquidThreadsEnableNewMessages ) ) { |
237 | $wgHiddenPrefs[] = 'lqtnotifytalk'; |
238 | $wgHiddenPrefs[] = 'lqt-watch-threads'; |
239 | $wgHiddenPrefs[] = 'lqtdisplaydepth'; |
240 | $wgHiddenPrefs[] = 'lqtdisplaycount'; |
241 | } |
242 | |
243 | return true; |
244 | } |
245 | |
246 | /** |
247 | * @param WikiPage $wikiPage |
248 | * |
249 | * @return bool |
250 | */ |
251 | public static function updateNewtalkOnEdit( WikiPage $wikiPage ) { |
252 | $title = $wikiPage->getTitle(); |
253 | |
254 | // They're only editing the header, don't update newtalk. |
255 | return !LqtDispatch::isLqtPage( $title ); |
256 | } |
257 | |
258 | public static function dumpThreadData( $writer, &$out, $row, $title ) { |
259 | // Is it a thread |
260 | if ( empty( $row->thread_id ) || $row->thread_type >= 2 ) { |
261 | return true; |
262 | } |
263 | |
264 | $thread = Thread::newFromRow( $row ); |
265 | $threadInfo = "\n"; |
266 | $attribs = []; |
267 | $attribs['ThreadSubject'] = $thread->subject(); |
268 | |
269 | if ( $thread->hasSuperthread() ) { |
270 | if ( $thread->superthread()->title() ) { |
271 | $attribs['ThreadParent'] = $thread->superthread()->title()->getPrefixedText(); |
272 | } |
273 | |
274 | if ( $thread->topmostThread()->title() ) { |
275 | $attribs['ThreadAncestor'] = $thread->topmostThread()->title()->getPrefixedText(); |
276 | } |
277 | } |
278 | |
279 | $attribs['ThreadPage'] = $thread->getTitle()->getPrefixedText(); |
280 | $attribs['ThreadID'] = $thread->id(); |
281 | |
282 | if ( $thread->hasSummary() && $thread->summary() ) { |
283 | $attribs['ThreadSummaryPage'] = $thread->summary()->getTitle()->getPrefixedText(); |
284 | } |
285 | |
286 | $attribs['ThreadAuthor'] = $thread->author()->getName(); |
287 | $attribs['ThreadEditStatus'] = self::$editedStati[$thread->editedness()]; |
288 | $attribs['ThreadType'] = self::$threadTypes[$thread->type()]; |
289 | $attribs['ThreadSignature'] = $thread->signature(); |
290 | |
291 | foreach ( $attribs as $key => $value ) { |
292 | $threadInfo .= "\t" . Xml::element( $key, null, $value ) . "\n"; |
293 | } |
294 | |
295 | $out .= Validator::cleanUp( Xml::tags( 'DiscussionThreading', null, $threadInfo ) . "\n" ); |
296 | |
297 | return true; |
298 | } |
299 | |
300 | public static function modifyExportQuery( $db, &$tables, &$cond, &$opts, &$join ) { |
301 | $tables[] = 'thread'; |
302 | |
303 | $join['thread'] = [ 'left join', [ 'thread_root=page_id' ] ]; |
304 | |
305 | return true; |
306 | } |
307 | |
308 | public static function customiseSearchResultTitle( &$title, &$text, $result, $terms, $page ) { |
309 | if ( $title->getNamespace() != NS_LQT_THREAD ) { |
310 | return true; |
311 | } |
312 | |
313 | $thread = Threads::withRoot( MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ) ); |
314 | |
315 | if ( $thread ) { |
316 | $text = $thread->subject(); |
317 | |
318 | $title = clone $thread->topmostThread()->title(); |
319 | $title->setFragment( '#' . $thread->getAnchorName() ); |
320 | } |
321 | |
322 | return true; |
323 | } |
324 | |
325 | /** |
326 | * For integration with user renames. |
327 | * |
328 | * @param RenameuserSQL $renameUserSQL |
329 | * @return bool |
330 | */ |
331 | public static function onUserRename( $renameUserSQL ) { |
332 | // Always use the job queue, talk page edits will take forever |
333 | foreach ( self::$userTables as $table => $fields ) { |
334 | $renameUserSQL->tablesJob[$table] = $fields; |
335 | } |
336 | return true; |
337 | } |
338 | |
339 | /** @var string[][] */ |
340 | private static $userTables = [ |
341 | 'thread' => [ 'thread_author_name', 'thread_author_id', 'thread_modified' ], |
342 | 'thread_history' => [ 'th_user_text', 'th_user', 'th_timestamp' ] |
343 | ]; |
344 | |
345 | /** |
346 | * For integration with the UserMerge extension. |
347 | * |
348 | * @param array &$updateFields |
349 | * @return bool |
350 | */ |
351 | public static function onUserMergeAccountFields( &$updateFields ) { |
352 | // array( tableName, idField, textField ) |
353 | foreach ( self::$userTables as $table => $fields ) { |
354 | $updateFields[] = [ $table, $fields[1], $fields[0] ]; |
355 | } |
356 | return true; |
357 | } |
358 | |
359 | /** |
360 | * Handle EditPageGetCheckboxesDefinition hook |
361 | * |
362 | * @param EditPage $editPage |
363 | * @param array &$checkboxes |
364 | * @return bool |
365 | */ |
366 | public static function editCheckboxes( $editPage, &$checkboxes ) { |
367 | global $wgRequest, $wgLiquidThreadsShowBumpCheckbox; |
368 | |
369 | $article = $editPage->getArticle(); |
370 | $title = $article->getTitle(); |
371 | |
372 | if ( |
373 | !$article->getPage()->exists() |
374 | && $title->getNamespace() == NS_LQT_THREAD |
375 | ) { |
376 | unset( $checkboxes['minor'] ); |
377 | unset( $checkboxes['watch'] ); |
378 | } |
379 | |
380 | if ( $title->getNamespace() == NS_LQT_THREAD && self::$editType != 'new' && |
381 | $wgLiquidThreadsShowBumpCheckbox |
382 | ) { |
383 | $checkboxes['wpBumpThread'] = [ |
384 | 'id' => 'wpBumpThread', |
385 | 'label-message' => 'lqt-edit-bump', |
386 | 'title-message' => 'lqt-edit-bump-tooltip', |
387 | 'legacy-name' => 'bump', |
388 | 'default' => !$wgRequest->wasPosted() || $wgRequest->getBool( 'wpBumpThread' ), |
389 | ]; |
390 | } |
391 | |
392 | return true; |
393 | } |
394 | |
395 | public static function customiseSearchProfiles( &$profiles ) { |
396 | $namespaces = [ NS_LQT_THREAD, NS_LQT_SUMMARY ]; |
397 | |
398 | // Add odd namespaces |
399 | $searchableNS = MediaWikiServices::getInstance()->getSearchEngineConfig() |
400 | ->searchableNamespaces(); |
401 | foreach ( $searchableNS as $ns => $nsName ) { |
402 | if ( $ns % 2 == 1 ) { |
403 | $namespaces[] = $ns; |
404 | } |
405 | } |
406 | |
407 | $insert = [ |
408 | 'threads' => [ |
409 | 'message' => 'searchprofile-threads', |
410 | 'tooltip' => 'searchprofile-threads-tooltip', |
411 | 'namespaces' => $namespaces, |
412 | 'namespace-messages' => MediaWikiServices::getInstance()->getSearchEngineConfig() |
413 | ->namespacesAsText( $namespaces ), |
414 | ], |
415 | ]; |
416 | |
417 | // Insert translations before 'all' |
418 | $index = array_search( 'all', array_keys( $profiles ) ); |
419 | |
420 | // Or just at the end if all is not found |
421 | if ( $index === false ) { |
422 | wfWarn( '"all" not found in search profiles' ); |
423 | $index = count( $profiles ); |
424 | } |
425 | |
426 | $profiles = array_merge( |
427 | array_slice( $profiles, 0, $index ), |
428 | $insert, |
429 | array_slice( $profiles, $index ) |
430 | ); |
431 | |
432 | return true; |
433 | } |
434 | |
435 | /** |
436 | * @param DatabaseUpdater|null $updater |
437 | * @return bool |
438 | */ |
439 | public static function onLoadExtensionSchemaUpdates( DatabaseUpdater $updater = null ) { |
440 | $dir = realpath( __DIR__ . '/../sql' ); |
441 | $dbType = $updater->getDB()->getType(); |
442 | $updater->addExtensionTable( 'thread', "$dir/$dbType/tables-generated.sql" ); |
443 | |
444 | // 1.31 |
445 | $updater->dropExtensionIndex( |
446 | 'thread', |
447 | 'thread_root_2', |
448 | "$dir/patches/thread-drop-thread_root_2.sql" |
449 | ); |
450 | |
451 | if ( $dbType === 'mysql' ) { |
452 | // 1.39 |
453 | $updater->modifyExtensionField( |
454 | 'thread', |
455 | 'thread_created', |
456 | "$dir/$dbType/patch-thread-timestamps.sql" |
457 | ); |
458 | $updater->modifyExtensionField( |
459 | 'user_message_state', |
460 | 'ums_read_timestamp', |
461 | "$dir/$dbType/patch-user_message_state-ums_read_timestamp.sql" |
462 | ); |
463 | $updater->modifyExtensionField( |
464 | 'thread_history', |
465 | 'th_timestamp', |
466 | "$dir/$dbType/patch-thread_history-th_timestamp.sql" |
467 | ); |
468 | $updater->dropExtensionIndex( |
469 | 'thread', |
470 | 'thread_root_2', |
471 | "$dir/$dbType/patch-thread-drop-index.sql" |
472 | ); |
473 | } elseif ( $dbType === 'postgres' ) { |
474 | // 1.39 |
475 | $updater->addExtensionUpdate( [ |
476 | 'dropDefault', 'thread', 'thread_modified' |
477 | ] ); |
478 | $updater->addExtensionUpdate( [ |
479 | 'dropDefault', 'thread', 'thread_created' |
480 | ] ); |
481 | $updater->addExtensionUpdate( [ |
482 | 'changeField', 'thread_history', 'th_timestamp', 'TIMESTAMPTZ', 'th_timestamp::timestamp with time zone' |
483 | ] ); |
484 | $updater->addExtensionUpdate( [ |
485 | 'dropPgIndex', 'thread', 'thread_thread_root_key' |
486 | ] ); |
487 | $updater->addExtensionUpdate( [ |
488 | 'renameIndex', 'thread', 'thread_root_page', 'thread_root' |
489 | ] ); |
490 | $updater->addExtensionUpdate( [ |
491 | 'renameIndex', 'thread', 'thread_author', 'thread_author_name' |
492 | ] ); |
493 | $updater->addExtensionUpdate( [ |
494 | 'addPgIndex', 'thread', 'thread_parent', '(thread_parent)' |
495 | ] ); |
496 | $updater->addExtensionUpdate( [ |
497 | 'changeField', 'thread', 'thread_editedness', 'INT', 'thread_editedness::INT DEFAULT 0' |
498 | ] ); |
499 | $updater->addExtensionUpdate( [ |
500 | 'changeField', 'thread', 'thread_article_namespace', 'INT', '' |
501 | ] ); |
502 | $updater->addExtensionUpdate( [ |
503 | 'changeField', 'thread', 'thread_type', 'INT', 'thread_type::INT DEFAULT 0' |
504 | ] ); |
505 | $updater->addExtensionUpdate( [ |
506 | 'changeNullableField', 'thread', 'thread_replies', 'NULL', true |
507 | ] ); |
508 | $updater->addExtensionIndex( |
509 | 'historical_thread', 'historical_thread_pkey', "$dir/$dbType/patch-historical_thread-pk.sql" |
510 | ); |
511 | $updater->addExtensionIndex( |
512 | 'user_message_state', 'user_message_state_pkey', "$dir/$dbType/patch-user_message_state-pk.sql" |
513 | ); |
514 | $updater->addExtensionUpdate( [ |
515 | 'renameIndex', 'thread_history', 'thread_history_thread', 'th_thread_timestamp' |
516 | ] ); |
517 | $updater->addExtensionUpdate( [ |
518 | 'renameIndex', 'thread_history', 'thread_history_user', 'th_user_text' |
519 | ] ); |
520 | $updater->addExtensionUpdate( [ |
521 | 'addPgIndex', 'thread_history', 'th_timestamp_thread', '(th_timestamp,th_thread)' |
522 | ] ); |
523 | $updater->addExtensionUpdate( [ |
524 | 'renameIndex', 'thread_reaction', 'thread_reaction_user_text_value', 'tr_user_text_value' |
525 | ] ); |
526 | } |
527 | |
528 | $updater->dropExtensionIndex( |
529 | 'thread', |
530 | 'thread_root_page', |
531 | "$dir/patches/thread-drop-thread_root_page.sql" |
532 | ); |
533 | |
534 | return true; |
535 | } |
536 | |
537 | public static function onPageMoveComplete( |
538 | LinkTarget $oldLinkTarget, |
539 | LinkTarget $newLinkTarget, |
540 | UserIdentity $user, |
541 | int $pageId, |
542 | int $redirId, |
543 | string $reason, |
544 | RevisionRecord $revisionRecord |
545 | ) { |
546 | $oldTitle = Title::newFromLinkTarget( $oldLinkTarget ); |
547 | $newTitle = Title::newFromLinkTarget( $newLinkTarget ); |
548 | // Check if it's a talk page. |
549 | if ( !LqtDispatch::isLqtPage( $oldTitle ) && |
550 | !LqtDispatch::isLqtPage( $newTitle ) |
551 | ) { |
552 | return true; |
553 | } |
554 | |
555 | // Synchronise the first 500 threads, in reverse order by thread id. If |
556 | // there are more threads to synchronise, the job queue will take over. |
557 | Threads::synchroniseArticleData( |
558 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $newTitle ), |
559 | 500, |
560 | 'cascade' |
561 | ); |
562 | } |
563 | |
564 | public static function onMovePageIsValidMove( Title $oldTitle ) { |
565 | // Synchronise article data so that moving the article doesn't break any |
566 | // article association. |
567 | Threads::synchroniseArticleData( |
568 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $oldTitle ) |
569 | ); |
570 | |
571 | return true; |
572 | } |
573 | |
574 | /** |
575 | * @param User $user |
576 | * @param Title $title |
577 | * @param bool &$isBlocked |
578 | * @param bool &$allowUserTalk |
579 | * @return bool |
580 | */ |
581 | public static function userIsBlockedFrom( $user, $title, &$isBlocked, &$allowUserTalk ) { |
582 | // Limit applicability |
583 | if ( !( $isBlocked && $allowUserTalk && $title->getNamespace() == NS_LQT_THREAD ) ) { |
584 | return true; |
585 | } |
586 | |
587 | // Now we're dealing with blocked users with user talk editing allowed editing pages |
588 | // in the thread namespace. |
589 | |
590 | if ( $title->exists() ) { |
591 | // If the page actually exists, allow the user to edit posts on their own talk page. |
592 | $thread = Threads::withRoot( |
593 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ) |
594 | ); |
595 | |
596 | if ( !$thread ) { |
597 | return true; |
598 | } |
599 | |
600 | $articleTitle = $thread->getTitle(); |
601 | |
602 | if ( $articleTitle->getNamespace() == NS_USER_TALK && |
603 | $user->getName() == $title->getText() ) { |
604 | $isBlocked = false; |
605 | return true; |
606 | } |
607 | } else { |
608 | // Otherwise, it's a bit trickier. Allow creation of thread titles prefixed by the |
609 | // user's talk page. |
610 | |
611 | // Figure out if it's on the talk page |
612 | $talkPage = $user->getTalkPage(); |
613 | $isOnTalkPage = ( self::$editThread && |
614 | self::$editThread->getTitle()->equals( $talkPage ) ); |
615 | $isOnTalkPage = $isOnTalkPage || ( self::$editAppliesTo && |
616 | self::$editAppliesTo->getTitle()->equals( $talkPage ) ); |
617 | |
618 | # FIXME: self::$editArticle is sometimes not set; |
619 | # is that ok and if not why is it happening? |
620 | if ( self::$editArticle instanceof Article ) { |
621 | $isOnTalkPage = $isOnTalkPage || |
622 | ( self::$editArticle->getTitle()->equals( $talkPage ) ); |
623 | } |
624 | |
625 | if ( self::$editArticle instanceof Article |
626 | && self::$editArticle->getTitle()->equals( $title ) |
627 | && $isOnTalkPage |
628 | ) { |
629 | $isBlocked = false; |
630 | return true; |
631 | } |
632 | } |
633 | |
634 | return true; |
635 | } |
636 | |
637 | public static function onSkinTemplateNavigation( $skinTemplate, &$links ) { |
638 | $user = $skinTemplate->getUser(); |
639 | |
640 | if ( $user->isAnon() ) { |
641 | return true; |
642 | } |
643 | |
644 | global $wgLiquidThreadsEnableNewMessages; |
645 | |
646 | if ( $wgLiquidThreadsEnableNewMessages ) { |
647 | $newMessagesCount = NewMessages::newMessageCount( $user ); |
648 | |
649 | // Add new messages link. |
650 | $url = SpecialPage::getTitleFor( 'NewMessages' )->getLocalURL(); |
651 | $msg = 'lqt-newmessages-n'; |
652 | $newMessagesLink = [ |
653 | 'href' => $url, |
654 | 'text' => wfMessage( $msg )->numParams( $newMessagesCount )->text(), |
655 | 'active' => $newMessagesCount > 0, |
656 | ]; |
657 | |
658 | $insertUrls = [ 'newmessages' => $newMessagesLink ]; |
659 | $personal_urls = $links['user-menu'] ?? []; |
660 | // User has viewmywatchlist permission |
661 | if ( isset( $personal_urls['watchlist'] ) ) { |
662 | $personal_urls = wfArrayInsertAfter( $personal_urls, $insertUrls, 'watchlist' ); |
663 | } else { |
664 | $personal_urls = wfArrayInsertAfter( $personal_urls, $insertUrls, 'preferences' ); |
665 | } |
666 | $links['user-menu'] = $personal_urls; |
667 | } |
668 | |
669 | return true; |
670 | } |
671 | |
672 | /** |
673 | * @param WikiPage $wikiPage |
674 | * @param UserIdentity $user |
675 | * @param string $summary |
676 | * @param int $flags |
677 | * @param RevisionRecord $revisionRecord |
678 | * @param EditResult $editResult |
679 | * @return bool |
680 | */ |
681 | public static function onPageSaveComplete( |
682 | WikiPage $wikiPage, |
683 | UserIdentity $user, |
684 | string $summary, |
685 | int $flags, |
686 | RevisionRecord $revisionRecord, |
687 | EditResult $editResult |
688 | ) { |
689 | $title = $wikiPage->getTitle(); |
690 | if ( $title->getNamespace() != NS_LQT_THREAD ) { |
691 | // Not a thread |
692 | return true; |
693 | } |
694 | |
695 | if ( $flags & EDIT_NEW ) { |
696 | // New page |
697 | return true; |
698 | } |
699 | |
700 | $thread = Threads::withRoot( $wikiPage ); |
701 | |
702 | if ( !$thread ) { |
703 | // No matching thread. |
704 | return true; |
705 | } |
706 | |
707 | $content = $wikiPage->getContent(); |
708 | LqtView::editMetadataUpdates( |
709 | [ |
710 | 'root' => $wikiPage, |
711 | 'thread' => $thread, |
712 | 'summary' => $summary, |
713 | 'text' => ( $content instanceof TextContent ) ? $content->getText() : '', |
714 | ] ); |
715 | |
716 | return true; |
717 | } |
718 | |
719 | /** |
720 | * @param Title $title |
721 | * @param array &$types |
722 | * @return bool |
723 | */ |
724 | public static function getProtectionTypes( $title, &$types ) { |
725 | $isLqtPage = LqtDispatch::isLqtPage( $title ); |
726 | $isThread = $title->getNamespace() == NS_LQT_THREAD; |
727 | |
728 | if ( !$isLqtPage && !$isThread ) { |
729 | return true; |
730 | } |
731 | |
732 | if ( $isLqtPage ) { |
733 | $types[] = 'newthread'; |
734 | $types[] = 'reply'; |
735 | } |
736 | |
737 | if ( $isThread ) { |
738 | $types[] = 'reply'; |
739 | } |
740 | |
741 | return true; |
742 | } |
743 | |
744 | /** |
745 | * Handles tags in Page sections of XML dumps |
746 | * @param WikiImporter $importer |
747 | * @param array &$pageInfo |
748 | * @return bool |
749 | */ |
750 | public static function handlePageXMLTag( $importer, &$pageInfo ) { |
751 | $reader = $importer->getReader(); |
752 | if ( !( $reader->nodeType == XMLReader::ELEMENT && |
753 | $reader->name == 'DiscussionThreading' ) ) { |
754 | return true; |
755 | } |
756 | |
757 | $pageInfo['DiscussionThreading'] = []; |
758 | $fields = [ |
759 | 'ThreadSubject', |
760 | 'ThreadParent', |
761 | 'ThreadAncestor', |
762 | 'ThreadPage', |
763 | 'ThreadID', |
764 | 'ThreadSummaryPage', |
765 | 'ThreadAuthor', |
766 | 'ThreadEditStatus', |
767 | 'ThreadType', |
768 | 'ThreadSignature', |
769 | ]; |
770 | |
771 | $skip = false; |
772 | |
773 | while ( $skip ? $reader->next() : $reader->read() ) { |
774 | if ( $reader->nodeType == XMLReader::END_ELEMENT && |
775 | $reader->name == 'DiscussionThreading' |
776 | ) { |
777 | break; |
778 | } |
779 | |
780 | $tag = $reader->name; |
781 | |
782 | if ( in_array( $tag, $fields ) ) { |
783 | $pageInfo['DiscussionThreading'][$tag] = $importer->nodeContents(); |
784 | } |
785 | } |
786 | |
787 | return false; |
788 | } |
789 | |
790 | /** |
791 | * Processes discussion threading data in XML dumps (extracted in handlePageXMLTag). |
792 | * |
793 | * @param Title $title |
794 | * @param Title $origTitle |
795 | * @param int $revCount |
796 | * @param int $sRevCount |
797 | * @param array $pageInfo |
798 | * @return bool |
799 | */ |
800 | public static function afterImportPage( $title, $origTitle, $revCount, $sRevCount, $pageInfo ) { |
801 | // in-process cache of pending thread relationships |
802 | static $pendingRelationships = null; |
803 | |
804 | if ( $pendingRelationships === null ) { |
805 | $pendingRelationships = self::loadPendingRelationships(); |
806 | } |
807 | |
808 | $titlePendingRelationships = []; |
809 | if ( isset( $pendingRelationships[$title->getPrefixedText()] ) ) { |
810 | $titlePendingRelationships = $pendingRelationships[$title->getPrefixedText()]; |
811 | |
812 | foreach ( $titlePendingRelationships as $k => $v ) { |
813 | if ( $v['type'] == 'article' ) { |
814 | self::applyPendingArticleRelationship( $v, $title ); |
815 | unset( $titlePendingRelationships[$k] ); |
816 | } |
817 | } |
818 | } |
819 | |
820 | if ( !isset( $pageInfo['DiscussionThreading'] ) ) { |
821 | return true; |
822 | } |
823 | |
824 | $typeValues = array_flip( self::$threadTypes ); |
825 | |
826 | $info = $pageInfo['DiscussionThreading']; |
827 | |
828 | $root = new Article( $title, 0 ); |
829 | $article = new Article( Title::newFromText( $info['ThreadPage'] ), 0 ); |
830 | $type = $typeValues[$info['ThreadType']]; |
831 | $subject = $info['ThreadSubject']; |
832 | $summary = wfMessage( 'lqt-imported' )->inContentLanguage()->text(); |
833 | |
834 | $signature = null; |
835 | if ( isset( $info['ThreadSignature'] ) ) { |
836 | $signature = $info['ThreadSignature']; |
837 | } |
838 | |
839 | $user = RequestContext::getMain()->getUser(); |
840 | $thread = Thread::create( $root, $article, $user, null, $type, |
841 | $subject, $summary, null, $signature ); |
842 | |
843 | if ( isset( $info['ThreadSummaryPage'] ) ) { |
844 | $summaryPageName = $info['ThreadSummaryPage']; |
845 | $summaryPage = new Article( Title::newFromText( $summaryPageName ), 0 ); |
846 | |
847 | if ( $summaryPage->getPage()->exists() ) { |
848 | $thread->setSummary( $summaryPage ); |
849 | } else { |
850 | self::addPendingRelationship( $thread->id(), 'thread_summary_page', |
851 | $summaryPageName, 'article', $pendingRelationships ); |
852 | } |
853 | } |
854 | |
855 | if ( isset( $info['ThreadParent'] ) ) { |
856 | $threadPageName = $info['ThreadParent']; |
857 | $superthread = Threads::withRoot( |
858 | MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( |
859 | Title::newFromText( $threadPageName ) |
860 | ) |
861 | ); |
862 | |
863 | if ( $superthread ) { |
864 | $thread->setSuperthread( $superthread ); |
865 | } else { |
866 | self::addPendingRelationship( $thread->id(), 'thread_parent', |
867 | $threadPageName, 'thread', $pendingRelationships ); |
868 | } |
869 | } |
870 | |
871 | $thread->save(); |
872 | |
873 | foreach ( $titlePendingRelationships as $k => $v ) { |
874 | if ( $v['type'] == 'thread' ) { |
875 | self::applyPendingThreadRelationship( $v, $thread ); |
876 | unset( $titlePendingRelationships[$k] ); |
877 | } |
878 | } |
879 | |
880 | return true; |
881 | } |
882 | |
883 | public static function applyPendingThreadRelationship( $pendingRelationship, Thread $thread ) { |
884 | if ( $pendingRelationship['relationship'] == 'thread_parent' ) { |
885 | $childThread = Threads::withId( $pendingRelationship['thread'] ); |
886 | |
887 | $childThread->setSuperthread( $thread ); |
888 | $childThread->save(); |
889 | $thread->save(); |
890 | } |
891 | } |
892 | |
893 | /** |
894 | * @param array $pendingRelationship |
895 | * @param Title $title |
896 | */ |
897 | public static function applyPendingArticleRelationship( $pendingRelationship, $title ) { |
898 | $articleID = $title->getArticleID(); |
899 | |
900 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
901 | |
902 | $dbw->newUpdateQueryBuilder() |
903 | ->update( 'thread' ) |
904 | ->set( [ $pendingRelationship['relationship'] => $articleID ] ) |
905 | ->where( [ 'thread_id' => $pendingRelationship['thread'] ] ) |
906 | ->caller( __METHOD__ ) |
907 | ->execute(); |
908 | |
909 | $dbw->newDeleteQueryBuilder() |
910 | ->deleteFrom( 'thread_pending_relationship' ) |
911 | ->where( [ 'tpr_title' => $pendingRelationship['title'] ] ) |
912 | ->caller( __METHOD__ ) |
913 | ->execute(); |
914 | } |
915 | |
916 | /** |
917 | * @return array |
918 | */ |
919 | public static function loadPendingRelationships() { |
920 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
921 | $arr = []; |
922 | |
923 | $res = $dbr->newSelectQueryBuilder() |
924 | ->select( '*' ) |
925 | ->from( 'thread_pending_relationship' ) |
926 | ->caller( __METHOD__ ) |
927 | ->fetchResultSet(); |
928 | |
929 | foreach ( $res as $row ) { |
930 | $title = $row->tpr_title; |
931 | $entry = [ |
932 | 'thread' => $row->tpr_thread, |
933 | 'relationship' => $row->tpr_relationship, |
934 | 'title' => $title, |
935 | 'type' => $row->tpr_type, |
936 | ]; |
937 | |
938 | if ( !isset( $arr[$title] ) ) { |
939 | $arr[$title] = []; |
940 | } |
941 | |
942 | $arr[$title][] = $entry; |
943 | } |
944 | |
945 | return $arr; |
946 | } |
947 | |
948 | public static function addPendingRelationship( |
949 | $thread, $relationship, $title, $type, &$array |
950 | ) { |
951 | $entry = [ |
952 | 'thread' => $thread, |
953 | 'relationship' => $relationship, |
954 | 'title' => $title, |
955 | 'type' => $type, |
956 | ]; |
957 | |
958 | $row = []; |
959 | foreach ( $entry as $k => $v ) { |
960 | $row['tpr_' . $k] = $v; |
961 | } |
962 | |
963 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
964 | $dbw->newInsertQueryBuilder() |
965 | ->insertInto( 'thread_pending_relationship' ) |
966 | ->row( $row ) |
967 | ->caller( __METHOD__ ) |
968 | ->execute(); |
969 | |
970 | if ( !isset( $array[$title] ) ) { |
971 | $array[$title] = []; |
972 | } |
973 | |
974 | $array[$title][] = $entry; |
975 | } |
976 | |
977 | /** |
978 | * Do not allow users to read threads on talkpages that they cannot read. |
979 | * |
980 | * @param Title $title |
981 | * @param User $user |
982 | * @param string $action |
983 | * @param array|string|MessageSpecifier &$result |
984 | * @return bool |
985 | */ |
986 | public static function onGetUserPermissionsErrors( $title, $user, $action, &$result ) { |
987 | if ( $title->getNamespace() != NS_LQT_THREAD || $action != 'read' ) { |
988 | return true; |
989 | } |
990 | |
991 | $thread = Threads::withRoot( MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ) ); |
992 | |
993 | if ( !$thread ) { |
994 | return true; |
995 | } |
996 | |
997 | $talkpage = $thread->article(); |
998 | |
999 | $canRead = MediaWikiServices::getInstance()->getPermissionManager() |
1000 | ->quickUserCan( 'read', $user, $talkpage->getTitle() ); |
1001 | |
1002 | if ( $canRead ) { |
1003 | return true; |
1004 | } else { |
1005 | $result = 'liquidthreads-blocked-read'; |
1006 | return false; |
1007 | } |
1008 | } |
1009 | |
1010 | /** |
1011 | * @param Parser $parser |
1012 | * @return bool |
1013 | */ |
1014 | public static function onParserFirstCallInit( $parser ) { |
1015 | $parser->setFunctionHook( |
1016 | 'useliquidthreads', |
1017 | [ LqtParserFunctions::class, 'useLiquidThreads' ] |
1018 | ); |
1019 | |
1020 | $parser->setFunctionHook( |
1021 | 'lqtpagelimit', |
1022 | [ LqtParserFunctions::class, 'lqtPageLimit' ] |
1023 | ); |
1024 | |
1025 | global $wgLiquidThreadsAllowEmbedding; |
1026 | |
1027 | if ( $wgLiquidThreadsAllowEmbedding ) { |
1028 | $parser->setHook( 'talkpage', [ LqtParserFunctions::class, 'lqtTalkPage' ] ); |
1029 | $parser->setHook( 'thread', [ LqtParserFunctions::class, 'lqtThread' ] ); |
1030 | } |
1031 | |
1032 | return true; |
1033 | } |
1034 | |
1035 | /** |
1036 | * @param array &$list |
1037 | * @return bool |
1038 | */ |
1039 | public static function onCanonicalNamespaces( &$list ) { |
1040 | $list[NS_LQT_THREAD] = 'Thread'; |
1041 | $list[NS_LQT_THREAD_TALK] = 'Thread_talk'; |
1042 | $list[NS_LQT_SUMMARY] = 'Summary'; |
1043 | $list[NS_LQT_SUMMARY_TALK] = 'Summary_talk'; |
1044 | return true; |
1045 | } |
1046 | |
1047 | public static function onAPIQueryAfterExecute( $module ) { |
1048 | if ( $module instanceof ApiQueryInfo ) { |
1049 | $result = $module->getResult(); |
1050 | |
1051 | $data = (array)$result->getResultData( [ 'query', 'pages' ], [ |
1052 | 'Strip' => 'base' |
1053 | ] ); |
1054 | foreach ( $data as $pageid => $page ) { |
1055 | if ( $page == 'page' ) { |
1056 | continue; |
1057 | } |
1058 | |
1059 | if ( isset( $page['title'] ) |
1060 | && LqtDispatch::isLqtPage( Title::newFromText( $page['title'] ) ) |
1061 | ) { |
1062 | $result->addValue( |
1063 | [ 'query', 'pages' ], |
1064 | $pageid, |
1065 | [ 'islqttalkpage' => '' ] |
1066 | ); |
1067 | } |
1068 | } |
1069 | } |
1070 | |
1071 | return true; |
1072 | } |
1073 | |
1074 | public static function onInfoAction( $context, &$pageInfo ) { |
1075 | if ( LqtDispatch::isLqtPage( $context->getTitle() ) ) { |
1076 | $pageInfo['header-basic'][] = [ |
1077 | $context->msg( 'pageinfo-usinglqt' ), $context->msg( 'pageinfo-usinglqt-yes' ) |
1078 | ]; |
1079 | } |
1080 | |
1081 | return true; |
1082 | } |
1083 | |
1084 | public static function onSpecialPage_initList( &$aSpecialPages ) { |
1085 | global $wgLiquidThreadsEnableNewMessages; |
1086 | |
1087 | if ( !$wgLiquidThreadsEnableNewMessages ) { |
1088 | if ( isset( $aSpecialPages['NewMessages'] ) ) { |
1089 | unset( $aSpecialPages['NewMessages'] ); |
1090 | } |
1091 | } |
1092 | return true; |
1093 | } |
1094 | |
1095 | public static function onRegistration() { |
1096 | if ( !defined( 'NS_LQT_THREAD' ) ) { |
1097 | define( 'NS_LQT_THREAD', 90 ); |
1098 | define( 'NS_LQT_THREAD_TALK', 91 ); |
1099 | define( 'NS_LQT_SUMMARY', 92 ); |
1100 | define( 'NS_LQT_SUMMARY_TALK', 93 ); |
1101 | } |
1102 | } |
1103 | |
1104 | /** |
1105 | * Add icon for Special:Preferences mobile layout |
1106 | * |
1107 | * @param array &$iconNames Array of icon names for their respective sections. |
1108 | */ |
1109 | public static function onPreferencesGetIcon( &$iconNames ) { |
1110 | $iconNames[ 'lqt' ] = 'speechBubbles'; |
1111 | } |
1112 | } |