| 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*/; |