Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 549
0.00% covered (danger)
0.00%
0 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 549
0.00% covered (danger)
0.00%
0 / 33
19740
0.00% covered (danger)
0.00%
0 / 1
 customizeOldChangesList
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 setNewtalkHTML
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 beforeWatchlist
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 getPreferences
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 updateNewtalkOnEdit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 dumpThreadData
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 modifyExportQuery
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 customiseSearchResultTitle
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onUserRename
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onUserMergeAccountFields
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 editCheckboxes
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 customiseSearchProfiles
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 onLoadExtensionSchemaUpdates
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 1
12
 onPageMoveComplete
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 onMovePageIsValidMove
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 userIsBlockedFrom
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
272
 onSkinTemplateNavigation
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 getProtectionTypes
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 handlePageXMLTag
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 afterImportPage
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
182
 applyPendingThreadRelationship
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 applyPendingArticleRelationship
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 loadPendingRelationships
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 addPendingRelationship
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 onGetUserPermissionsErrors
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 onCanonicalNamespaces
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 onAPIQueryAfterExecute
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 onInfoAction
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialPage_initList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onRegistration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onPreferencesGetIcon
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\LiquidThreads;
4
5use ApiQueryInfo;
6use Article;
7use ChangesList;
8use DatabaseUpdater;
9use HtmlArmor;
10use LqtDispatch;
11use LqtParserFunctions;
12use LqtView;
13use MediaWiki\EditPage\EditPage;
14use MediaWiki\Linker\LinkTarget;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Output\OutputPage;
17use MediaWiki\RenameUser\RenameuserSQL;
18use MediaWiki\Revision\RevisionRecord;
19use MediaWiki\SpecialPage\SpecialPage;
20use MediaWiki\Storage\EditResult;
21use MediaWiki\Title\Title;
22use MediaWiki\User\User;
23use MediaWiki\User\UserIdentity;
24use MessageSpecifier;
25use NewMessages;
26use Parser;
27use RecentChange;
28use RequestContext;
29use TextContent;
30use Thread;
31use Threads;
32use UtfNormal\Validator;
33use WikiImporter;
34use WikiPage;
35use Xml;
36use XMLReader;
37
38class 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}