Index: trunk/extensions/LiquidThreads/LiquidThreads.php |
— | — | @@ -66,6 +66,10 @@ |
67 | 67 | $wgHooks['OAIFetchRowsQuery'][] = 'LqtHooks::modifyOAIQuery'; |
68 | 68 | $wgHooks['OAIFetchRecordQuery'][] = 'LqtHooks::modifyOAIQuery'; |
69 | 69 | |
| 70 | +// Import-related |
| 71 | +$wgHooks['ImportHandlePageXMLTag'][] = 'LqtHooks::handlePageXMLTag'; |
| 72 | +$wgHooks['AfterImportPage'][] = 'LqtHooks::afterImportPage'; |
| 73 | + |
70 | 74 | // Deletion |
71 | 75 | $wgHooks['ArticleDeleteComplete'][] = 'LqtDeletionController::onArticleDeleteComplete'; |
72 | 76 | $wgHooks['ArticleRevisionUndeleted'][] = 'LqtDeletionController::onArticleRevisionUndeleted'; |
Index: trunk/extensions/LiquidThreads/classes/Thread.php |
— | — | @@ -41,7 +41,7 @@ |
42 | 42 | protected $rootRevision; |
43 | 43 | |
44 | 44 | /* Flag about who has edited or replied to this thread. */ |
45 | | - protected $editedness; |
| 45 | + public $editedness; |
46 | 46 | protected $editors = null; |
47 | 47 | |
48 | 48 | protected $replies; |
— | — | @@ -122,6 +122,10 @@ |
123 | 123 | |
124 | 124 | function insert() { |
125 | 125 | $this->dieIfHistorical(); |
| 126 | + |
| 127 | + if ( $this->id() ) { |
| 128 | + throw new MWException( "Attempt to insert a thread that already exists." ); |
| 129 | + } |
126 | 130 | |
127 | 131 | $dbw = wfGetDB( DB_MASTER ); |
128 | 132 | |
Index: trunk/extensions/LiquidThreads/classes/Hooks.php |
— | — | @@ -7,6 +7,18 @@ |
8 | 8 | public static $editArticle = null; |
9 | 9 | public static $editTalkpage = null; |
10 | 10 | public static $scriptVariables = array(); |
| 11 | + |
| 12 | + public static $editedStati = array( |
| 13 | + Threads::EDITED_NEVER => 'never', |
| 14 | + Threads::EDITED_HAS_REPLY => 'has-reply', |
| 15 | + Threads::EDITED_BY_AUTHOR => 'by-author', |
| 16 | + Threads::EDITED_BY_OTHERS => 'by-others' |
| 17 | + ); |
| 18 | + public static $threadTypes = array( |
| 19 | + Threads::TYPE_NORMAL => 'normal', |
| 20 | + Threads::TYPE_MOVED => 'moved', |
| 21 | + Threads::TYPE_DELETED => 'deleted' |
| 22 | + ); |
11 | 23 | |
12 | 24 | static function customizeOldChangesList( &$changeslist, &$s, $rc ) { |
13 | 25 | if ( $rc->getTitle()->getNamespace() != NS_LQT_THREAD ) |
— | — | @@ -157,44 +169,35 @@ |
158 | 170 | } |
159 | 171 | |
160 | 172 | static function dumpThreadData( $writer, &$out, $row, $title ) { |
161 | | - $editedStati = array( |
162 | | - Threads::EDITED_NEVER => 'never', |
163 | | - Threads::EDITED_HAS_REPLY => 'has-reply', |
164 | | - Threads::EDITED_BY_AUTHOR => 'by-author', |
165 | | - Threads::EDITED_BY_OTHERS => 'by-others' |
166 | | - ); |
167 | | - $threadTypes = array( |
168 | | - Threads::TYPE_NORMAL => 'normal', |
169 | | - Threads::TYPE_MOVED => 'moved', |
170 | | - Threads::TYPE_DELETED => 'deleted' |
171 | | - ); |
172 | | - |
173 | 173 | // Is it a thread |
174 | | - if ( !empty( $row->thread_id ) ) { |
175 | | - $thread = Thread::newFromRow( $row ); |
176 | | - $threadInfo = "\n"; |
177 | | - $attribs = array(); |
178 | | - $attribs['ThreadSubject'] = $thread->subject(); |
179 | | - if ( $thread->hasSuperThread() ) { |
180 | | - $attribs['ThreadParent'] = $thread->superThread()->id(); |
181 | | - } |
182 | | - $attribs['ThreadAncestor'] = $thread->topmostThread()->id(); |
183 | | - $attribs['ThreadPage'] = $thread->getTitle()->getPrefixedText(); |
184 | | - $attribs['ThreadID'] = $thread->id(); |
185 | | - if ( $thread->hasSummary() && $thread->summary() ) { |
186 | | - $attribs['ThreadSummaryPage'] = $thread->summary()->getId(); |
187 | | - } |
188 | | - $attribs['ThreadAuthor'] = $thread->author()->getName(); |
189 | | - $attribs['ThreadEditStatus'] = $editedStati[$thread->editedness()]; |
190 | | - $attribs['ThreadType'] = $threadTypes[$thread->type()]; |
| 174 | + if ( empty( $row->thread_id ) ) { |
| 175 | + return true; |
| 176 | + } |
| 177 | + |
| 178 | + $thread = Thread::newFromRow( $row ); |
| 179 | + $threadInfo = "\n"; |
| 180 | + $attribs = array(); |
| 181 | + $attribs['ThreadSubject'] = $thread->subject(); |
| 182 | + if ( $thread->hasSuperThread() ) { |
| 183 | + $attribs['ThreadParent'] = $thread->superThread()->title()->getPrefixedText(); |
| 184 | + $attribs['ThreadAncestor'] = $thread->topmostThread()->title()->getPrefixedText(); |
| 185 | + } |
| 186 | + $attribs['ThreadPage'] = $thread->getTitle()->getPrefixedText(); |
| 187 | + $attribs['ThreadID'] = $thread->id(); |
| 188 | + if ( $thread->hasSummary() && $thread->summary() ) { |
| 189 | + $attribs['ThreadSummaryPage'] = $thread->summary()->getTitle()->getPrefixedText(); |
| 190 | + } |
| 191 | + $attribs['ThreadAuthor'] = $thread->author()->getName(); |
| 192 | + $attribs['ThreadEditStatus'] = self::$editedStati[$thread->editedness()]; |
| 193 | + $attribs['ThreadType'] = self::$threadTypes[$thread->type()]; |
| 194 | + $attribs['ThreadSignature'] = $thread->signature(); |
191 | 195 | |
192 | | - foreach ( $attribs as $key => $value ) { |
193 | | - $threadInfo .= "\t" . Xml::element( $key, null, $value ) . "\n"; |
194 | | - } |
195 | | - |
196 | | - $out .= Xml::tags( 'DiscussionThreading', null, $threadInfo ) . "\n"; |
| 196 | + foreach ( $attribs as $key => $value ) { |
| 197 | + $threadInfo .= "\t" . Xml::element( $key, null, $value ) . "\n"; |
197 | 198 | } |
198 | 199 | |
| 200 | + $out .= Xml::tags( 'DiscussionThreading', null, $threadInfo ) . "\n"; |
| 201 | + |
199 | 202 | return true; |
200 | 203 | } |
201 | 204 | |
— | — | @@ -573,4 +576,197 @@ |
574 | 577 | return ''; |
575 | 578 | } |
576 | 579 | } |
| 580 | + |
| 581 | + /** |
| 582 | + * Handles tags in Page sections of XML dumps |
| 583 | + */ |
| 584 | + |
| 585 | + public static function handlePageXMLTag( $reader, &$pageInfo ) { |
| 586 | + if ( !( $reader->nodeType == XmlReader::ELEMENT && |
| 587 | + $reader->name == 'DiscussionThreading' ) ) { |
| 588 | + return true; |
| 589 | + } |
| 590 | + |
| 591 | + $pageInfo['DiscussionThreading'] = array(); |
| 592 | + $fields = array( |
| 593 | + 'ThreadSubject', |
| 594 | + 'ThreadParent', |
| 595 | + 'ThreadAncestor', |
| 596 | + 'ThreadPage', |
| 597 | + 'ThreadID', |
| 598 | + 'ThreadSummaryPage', |
| 599 | + 'ThreadAuthor', |
| 600 | + 'ThreadEditStatus', |
| 601 | + 'ThreadType', |
| 602 | + 'ThreadSignature', |
| 603 | + ); |
| 604 | + |
| 605 | + $skip = false; |
| 606 | + |
| 607 | + while ( $skip ? $reader->next() : $reader->read() ) { |
| 608 | + if ( $reader->nodeType == XmlReader::END_ELEMENT && |
| 609 | + $reader->name == 'DiscussionThreading') { |
| 610 | + break; |
| 611 | + } |
| 612 | + |
| 613 | + $tag = $reader->name; |
| 614 | + |
| 615 | + if ( in_array( $tag, $fields ) ) { |
| 616 | + $pageInfo['DiscussionThreading'][$tag] = $reader->nodeContents(); |
| 617 | + } |
| 618 | + } |
| 619 | + |
| 620 | + return false; |
| 621 | + } |
| 622 | + |
| 623 | + // Processes discussion threading data in XML dumps (extracted in handlePageXMLTag). |
| 624 | + public static function afterImportPage( $title, $origTitle, $revCount, $sRevCount, $pageInfo ) { |
| 625 | + // in-process cache of pending thread relationships |
| 626 | + static $pendingRelationships = null; |
| 627 | + |
| 628 | + if ( $pendingRelationships === null ) { |
| 629 | + $pendingRelationships = self::loadPendingRelationships(); |
| 630 | + } |
| 631 | + |
| 632 | + $titlePendingRelationships = array(); |
| 633 | + if ( isset($pendingRelationships[$title->getPrefixedText()]) ) { |
| 634 | + $titlePendingRelationships = $pendingRelationships[$title->getPrefixedText()]; |
| 635 | + |
| 636 | + foreach( $titlePendingRelationships as $k => $v ) { |
| 637 | + if ( $v['type'] == 'article' ) { |
| 638 | + self::applyPendingArticleRelationship( $v, $title ); |
| 639 | + unset( $titlePendingRelationships[$k] ); |
| 640 | + } |
| 641 | + } |
| 642 | + } |
| 643 | + |
| 644 | + if ( ! isset( $pageInfo['DiscussionThreading'] ) ) { |
| 645 | + return true; |
| 646 | + } |
| 647 | + |
| 648 | + $statusValues = array_flip( self::$editedStati ); |
| 649 | + $typeValues = array_flip( self::$threadTypes ); |
| 650 | + |
| 651 | + $info = $pageInfo['DiscussionThreading']; |
| 652 | + |
| 653 | + $root = new Article( $title ); |
| 654 | + $article = new Article( Title::newFromText( $info['ThreadPage'] ) ); |
| 655 | + $type = $typeValues[$info['ThreadType']]; |
| 656 | + $editedness = $statusValues[$info['ThreadEditStatus']]; |
| 657 | + $subject = $info['ThreadSubject']; |
| 658 | + $summary = wfMsgForContent( 'lqt-imported' ); |
| 659 | + |
| 660 | + $signature = null; |
| 661 | + if ( isset( $info['ThreadSignature'] ) ) { |
| 662 | + $signature = $info['ThreadSignature']; |
| 663 | + } |
| 664 | + |
| 665 | + $thread = Thread::create( $root, $article, null, $type, |
| 666 | + $subject, $summary, null, $signature ); |
| 667 | + |
| 668 | + if ( isset( $info['ThreadSummaryPage'] ) ) { |
| 669 | + $summaryPageName = $info['ThreadSummaryPage']; |
| 670 | + $summaryPage = new Article( Title::newFromText( $summaryPageName ) ); |
| 671 | + if ( $summaryPage->exists() ) { |
| 672 | + $thread->setSummaryPage( $summaryPage ); |
| 673 | + } else { |
| 674 | + self::addPendingRelationship( $thread->id(), 'thread_summary_page', |
| 675 | + $summaryPageName, 'article', $pendingRelationships ); |
| 676 | + } |
| 677 | + } |
| 678 | + |
| 679 | + if ( isset( $info['ThreadParent'] ) ) { |
| 680 | + $threadPageName = $info['ThreadParent']; |
| 681 | + $parentArticle = new Article( Title::newFromText( $threadPageName ) ); |
| 682 | + $superthread = Threads::withRoot( $parentArticle ); |
| 683 | + |
| 684 | + if ( $superthread ) { |
| 685 | + $thread->setSuperthread( $superthread ); |
| 686 | + } else { |
| 687 | + self::addPendingRelationship( $thread->id(), 'thread_parent', |
| 688 | + $threadPageName, 'thread', $pendingRelationships ); |
| 689 | + } |
| 690 | + } |
| 691 | + |
| 692 | + $thread->save(); |
| 693 | + |
| 694 | + foreach( $titlePendingRelationships as $k => $v ) { |
| 695 | + if ( $v['type'] == 'thread' ) { |
| 696 | + self::applyPendingThreadRelationship( $pendingRelationship, $thread ); |
| 697 | + unset( $titlePendingRelationships[$k] ); |
| 698 | + } |
| 699 | + } |
| 700 | + |
| 701 | + return true; |
| 702 | + } |
| 703 | + |
| 704 | + public static function applyPendingThreadRelationship( $pendingRelationship, $thread ) { |
| 705 | + if ( $pendingRelationship['relationship'] == 'thread_parent' ) { |
| 706 | + $childThread = Threads::withID( $pendingRelationship['thread'] ); |
| 707 | + |
| 708 | + $childThread->setSuperthread( $thread ); |
| 709 | + $childThread->save(); |
| 710 | + $thread->save(); |
| 711 | + } |
| 712 | + } |
| 713 | + |
| 714 | + public static function applyPendingArticleRelationship( $pendingRelationship, $title ) { |
| 715 | + $articleID = $title->getArticleId(); |
| 716 | + |
| 717 | + $dbw = wfGetDB( DB_MASTER ); |
| 718 | + |
| 719 | + $dbw->update( 'thread', array( $pendingRelationship['relationship'] => $articleID ), |
| 720 | + array( 'thread_id' => $pendingRelationship['thread'] ), |
| 721 | + __METHOD__ ); |
| 722 | + |
| 723 | + $dbw->delete( 'thread_pending_relationship', |
| 724 | + array( 'tpr_title' => $pendingRelationship['title'] ), __METHOD__ ); |
| 725 | + } |
| 726 | + |
| 727 | + public static function loadPendingRelationships() { |
| 728 | + $dbr = wfGetDB( DB_MASTER ); |
| 729 | + $arr = array(); |
| 730 | + |
| 731 | + $res = $dbr->select( 'thread_pending_relationship', '*', array(1), __METHOD__ ); |
| 732 | + |
| 733 | + foreach( $res as $row ) { |
| 734 | + $entry = array( |
| 735 | + 'thread' => $row->tpr_thread, |
| 736 | + 'relationship' => $row->tpr_relationship, |
| 737 | + 'title' => $row->tpr_title, |
| 738 | + 'type' => $row->tpr_type, |
| 739 | + ); |
| 740 | + |
| 741 | + if ( !isset($arr[$title]) ) { |
| 742 | + $arr[$title] = array(); |
| 743 | + } |
| 744 | + |
| 745 | + $arr[$title][] = $entry; |
| 746 | + } |
| 747 | + |
| 748 | + return $arr; |
| 749 | + } |
| 750 | + |
| 751 | + public static function addPendingRelationship( $thread, $relationship, $title, $type, &$array ) { |
| 752 | + $entry = array( |
| 753 | + 'thread' => $thread, |
| 754 | + 'relationship' => $relationship, |
| 755 | + 'title' => $title, |
| 756 | + 'type' => $type, |
| 757 | + ); |
| 758 | + |
| 759 | + $row = array(); |
| 760 | + foreach( $entry as $k => $v ) { |
| 761 | + $row['tpr_'.$k] = $v; |
| 762 | + } |
| 763 | + |
| 764 | + $dbw = wfGetDB( DB_MASTER ); |
| 765 | + $dbw->insert( 'thread_pending_relationship', $row, __METHOD__ ); |
| 766 | + |
| 767 | + if ( !isset( $array[$title] ) ) { |
| 768 | + $array[$title] = array(); |
| 769 | + } |
| 770 | + |
| 771 | + $array[$title][] = $entry; |
| 772 | + } |
577 | 773 | } |
Index: trunk/extensions/LiquidThreads/lqt.sql |
— | — | @@ -86,3 +86,12 @@ |
87 | 87 | KEY (th_timestamp,th_thread), |
88 | 88 | KEY (th_user,th_user_text) |
89 | 89 | ) /*$wgDBTableOptions*/; |
| 90 | + |
| 91 | +-- Storage for "pending" relationships from import |
| 92 | +CREATE TABLE /*_*/thread_pending_relationship ( |
| 93 | + tpr_thread int unsigned NOT NULL, |
| 94 | + tpr_relationship varbinary(64) NOT NULL, |
| 95 | + tpr_title varbinary(1024) NOT NULL, |
| 96 | + tpr_type varbinary(32) NOT NULL, |
| 97 | + PRIMARY KEY (tpr_thread,tpr_relationship) |
| 98 | +) /*$wgDBTableOptions*/; |