Index: trunk/phase3/maintenance/tables.sql |
— | — | @@ -1237,4 +1237,28 @@ |
1238 | 1238 | PRIMARY KEY (ul_key) |
1239 | 1239 | ) /*$wgDBTableOptions*/; |
1240 | 1240 | |
| 1241 | +-- A table to track link changes |
| 1242 | +-- Experimental: enable using $wgTrackLinkChanges |
| 1243 | +CREATE TABLE /*$wgDBprefix*/recentlinkchanges ( |
| 1244 | + rlc_id int unsigned NOT NULL auto_increment, |
| 1245 | + |
| 1246 | + -- page, image, category, ... |
| 1247 | + rlc_type varchar(15) binary NOT NULL, |
| 1248 | + rlc_timestamp binary(14) NOT NULL default '', |
| 1249 | + -- 1: insert; 2: deletion; |
| 1250 | + -- should probably make this an enum... |
| 1251 | + rlc_action tinyint(1) NOT NULL default 0, |
| 1252 | + |
| 1253 | + -- page where the links are on |
| 1254 | + rlc_from int NOT NULL, |
| 1255 | + |
| 1256 | + rlc_to_namespace int, |
| 1257 | + rlc_to_title varchar(255) binary, |
| 1258 | + rlc_to_blob blob, |
| 1259 | + |
| 1260 | + PRIMARY KEY(rlc_id), |
| 1261 | + KEY from_timestamp (rlc_type, rlc_timestamp), |
| 1262 | + KEY to_timestamp (rlc_to_namespace, rlc_to_title, rlc_timestamp) |
| 1263 | +) /*$wgDBTableOptions*/; |
| 1264 | + |
1241 | 1265 | -- vim: sw=2 sts=2 et |
Index: trunk/phase3/includes/LinksUpdate.php |
— | — | @@ -12,12 +12,19 @@ |
13 | 13 | var $mId, //!< Page ID of the article linked from |
14 | 14 | $mTitle, //!< Title object of the article linked from |
15 | 15 | $mLinks, //!< Map of title strings to IDs for the links in the document |
| 16 | + $mExistingLinks, |
16 | 17 | $mImages, //!< DB keys of the images used, in the array key only |
| 18 | + $mExistingImages, |
17 | 19 | $mTemplates, //!< Map of title strings to IDs for the template references, including broken ones |
| 20 | + $mExistingTemplates, |
18 | 21 | $mExternals, //!< URLs of external links, array key only |
| 22 | + $mExistingExternals, |
19 | 23 | $mCategories, //!< Map of category names to sort keys |
| 24 | + $mExistingCategories, |
20 | 25 | $mInterlangs, //!< Map of language codes to titles |
| 26 | + $mExistingInterlangs, |
21 | 27 | $mProperties, //!< Map of arbitrary name to value |
| 28 | + $mExistingProperties, |
22 | 29 | $mDb, //!< Database connection reference |
23 | 30 | $mOptions, //!< SELECT options to be used (array) |
24 | 31 | $mRecursive; //!< Whether to queue jobs for recursive updates |
— | — | @@ -75,7 +82,7 @@ |
76 | 83 | * Update link tables with outgoing links from an updated article |
77 | 84 | */ |
78 | 85 | function doUpdate() { |
79 | | - global $wgUseDumbLinkUpdate; |
| 86 | + global $wgUseDumbLinkUpdate, $wgTrackLinkChanges; |
80 | 87 | |
81 | 88 | wfRunHooks( 'LinksUpdate', array( &$this ) ); |
82 | 89 | if ( $wgUseDumbLinkUpdate ) { |
— | — | @@ -83,6 +90,8 @@ |
84 | 91 | } else { |
85 | 92 | $this->doIncrementalUpdate(); |
86 | 93 | } |
| 94 | + if ( $wgTrackLinkChanges ) |
| 95 | + $this->makeRecentlinkchanges(); |
87 | 96 | wfRunHooks( 'LinksUpdateComplete', array( &$this ) ); |
88 | 97 | |
89 | 98 | } |
— | — | @@ -565,6 +574,9 @@ |
566 | 575 | * @private |
567 | 576 | */ |
568 | 577 | function getExistingLinks() { |
| 578 | + if ( is_array( $this->mExistingLinks ) ) |
| 579 | + return $this->mExistingLinks; |
| 580 | + |
569 | 581 | $res = $this->mDb->select( 'pagelinks', array( 'pl_namespace', 'pl_title' ), |
570 | 582 | array( 'pl_from' => $this->mId ), __METHOD__, $this->mOptions ); |
571 | 583 | $arr = array(); |
— | — | @@ -575,7 +587,7 @@ |
576 | 588 | $arr[$row->pl_namespace][$row->pl_title] = 1; |
577 | 589 | } |
578 | 590 | $this->mDb->freeResult( $res ); |
579 | | - return $arr; |
| 591 | + return $this->mExistingLinks = $arr; |
580 | 592 | } |
581 | 593 | |
582 | 594 | /** |
— | — | @@ -583,6 +595,9 @@ |
584 | 596 | * @private |
585 | 597 | */ |
586 | 598 | function getExistingTemplates() { |
| 599 | + if ( is_array( $this->mExistingTemplates ) ) |
| 600 | + return $this->mExistingTemplates; |
| 601 | + |
587 | 602 | $res = $this->mDb->select( 'templatelinks', array( 'tl_namespace', 'tl_title' ), |
588 | 603 | array( 'tl_from' => $this->mId ), __METHOD__, $this->mOptions ); |
589 | 604 | $arr = array(); |
— | — | @@ -593,7 +608,7 @@ |
594 | 609 | $arr[$row->tl_namespace][$row->tl_title] = 1; |
595 | 610 | } |
596 | 611 | $this->mDb->freeResult( $res ); |
597 | | - return $arr; |
| 612 | + return $this->mExistingTemplates = $arr; |
598 | 613 | } |
599 | 614 | |
600 | 615 | /** |
— | — | @@ -601,6 +616,9 @@ |
602 | 617 | * @private |
603 | 618 | */ |
604 | 619 | function getExistingImages() { |
| 620 | + if ( is_array( $this->mExistingImages ) ) |
| 621 | + return $this->mExistingImages; |
| 622 | + |
605 | 623 | $res = $this->mDb->select( 'imagelinks', array( 'il_to' ), |
606 | 624 | array( 'il_from' => $this->mId ), __METHOD__, $this->mOptions ); |
607 | 625 | $arr = array(); |
— | — | @@ -608,7 +626,7 @@ |
609 | 627 | $arr[$row->il_to] = 1; |
610 | 628 | } |
611 | 629 | $this->mDb->freeResult( $res ); |
612 | | - return $arr; |
| 630 | + return $this->mExistingImages = $arr; |
613 | 631 | } |
614 | 632 | |
615 | 633 | /** |
— | — | @@ -616,6 +634,9 @@ |
617 | 635 | * @private |
618 | 636 | */ |
619 | 637 | function getExistingExternals() { |
| 638 | + if ( is_array( $this->mExistingExternals ) ) |
| 639 | + return $this->mExistingExternals; |
| 640 | + |
620 | 641 | $res = $this->mDb->select( 'externallinks', array( 'el_to' ), |
621 | 642 | array( 'el_from' => $this->mId ), __METHOD__, $this->mOptions ); |
622 | 643 | $arr = array(); |
— | — | @@ -623,7 +644,7 @@ |
624 | 645 | $arr[$row->el_to] = 1; |
625 | 646 | } |
626 | 647 | $this->mDb->freeResult( $res ); |
627 | | - return $arr; |
| 648 | + return $this->mExistingExternals = $arr; |
628 | 649 | } |
629 | 650 | |
630 | 651 | /** |
— | — | @@ -631,6 +652,9 @@ |
632 | 653 | * @private |
633 | 654 | */ |
634 | 655 | function getExistingCategories() { |
| 656 | + if ( is_array( $this->mExistingCategories ) ) |
| 657 | + return $this->mExistingCategories; |
| 658 | + |
635 | 659 | $res = $this->mDb->select( 'categorylinks', array( 'cl_to', 'cl_sortkey' ), |
636 | 660 | array( 'cl_from' => $this->mId ), __METHOD__, $this->mOptions ); |
637 | 661 | $arr = array(); |
— | — | @@ -638,7 +662,7 @@ |
639 | 663 | $arr[$row->cl_to] = $row->cl_sortkey; |
640 | 664 | } |
641 | 665 | $this->mDb->freeResult( $res ); |
642 | | - return $arr; |
| 666 | + return $this->mExistingCategories = $arr; |
643 | 667 | } |
644 | 668 | |
645 | 669 | /** |
— | — | @@ -647,13 +671,16 @@ |
648 | 672 | * @private |
649 | 673 | */ |
650 | 674 | function getExistingInterlangs() { |
| 675 | + if ( is_array( $this->mExistingInterlangs ) ) |
| 676 | + return $this->mExistingInterlangs; |
| 677 | + |
651 | 678 | $res = $this->mDb->select( 'langlinks', array( 'll_lang', 'll_title' ), |
652 | 679 | array( 'll_from' => $this->mId ), __METHOD__, $this->mOptions ); |
653 | 680 | $arr = array(); |
654 | 681 | while ( $row = $this->mDb->fetchObject( $res ) ) { |
655 | 682 | $arr[$row->ll_lang] = $row->ll_title; |
656 | 683 | } |
657 | | - return $arr; |
| 684 | + return $this->mExistingInterlangs = $arr; |
658 | 685 | } |
659 | 686 | |
660 | 687 | /** |
— | — | @@ -661,6 +688,9 @@ |
662 | 689 | * @private |
663 | 690 | */ |
664 | 691 | function getExistingProperties() { |
| 692 | + if ( is_array( $this->mExistingProperties ) ) |
| 693 | + return $this->mExistingProperties; |
| 694 | + |
665 | 695 | $res = $this->mDb->select( 'page_props', array( 'pp_propname', 'pp_value' ), |
666 | 696 | array( 'pp_page' => $this->mId ), __METHOD__, $this->mOptions ); |
667 | 697 | $arr = array(); |
— | — | @@ -668,7 +698,7 @@ |
669 | 699 | $arr[$row->pp_propname] = $row->pp_value; |
670 | 700 | } |
671 | 701 | $this->mDb->freeResult( $res ); |
672 | | - return $arr; |
| 702 | + return $this->mExistingProperties = $arr; |
673 | 703 | } |
674 | 704 | |
675 | 705 | |
— | — | @@ -698,4 +728,124 @@ |
699 | 729 | } |
700 | 730 | } |
701 | 731 | } |
| 732 | + |
| 733 | + // Recentlinkchanges constants |
| 734 | + const INSERTION = 1; |
| 735 | + const DELETION = 2; |
| 736 | + private static $rlcFields = array( |
| 737 | + 'rlc_type', |
| 738 | + 'rlc_timestamp', |
| 739 | + 'rlc_action', |
| 740 | + 'rlc_from', |
| 741 | + 'rlc_to_namespace', |
| 742 | + 'rlc_to_title', |
| 743 | + 'rlc_to_blob' |
| 744 | + ); |
| 745 | + /* |
| 746 | + * Insert items to recentlinkchanges |
| 747 | + */ |
| 748 | + function makeRecentlinkchanges() { |
| 749 | + $insert = array(); |
| 750 | + $now = $this->mDb->timestamp(); |
| 751 | + |
| 752 | + // Category changes |
| 753 | + $existing = array_keys( $this->getExistingCategories() ); |
| 754 | + $current = array_keys( $this->mCategories ); |
| 755 | + $this->simpleAddToLinkchanges( $insert, 'category', $now, $existing, $current, NS_CATEGORY ); |
| 756 | + |
| 757 | + // External links |
| 758 | + $existing = array_keys( $this->getExistingExternals() ); |
| 759 | + $current = array_keys( $this->mExternals ); |
| 760 | + $insertions = array_diff( $current, $existing ); |
| 761 | + foreach ( $insertions as $item ) |
| 762 | + $insert[] = array( |
| 763 | + 'external', $now, self::INSERTION, |
| 764 | + $this->mId, null, null, $item ); |
| 765 | + $deletions = array_diff( $existing, $current ); |
| 766 | + foreach ( $deletions as $item ) |
| 767 | + $insert[] = array( |
| 768 | + 'external', $now, self::DELETION, |
| 769 | + $this->mId, null, null, $item ); |
| 770 | + |
| 771 | + // Image changes |
| 772 | + $existing = array_keys( $this->getExistingImages() ); |
| 773 | + $current = array_keys( $this->mImages ); |
| 774 | + $this->simpleAddToLinkchanges( $insert, 'image', $now, $existing, $current, NS_IMAGE ); |
| 775 | + |
| 776 | + // Interlangs |
| 777 | + $existing = $this->getExistingInterlangs(); |
| 778 | + $current = $this->mInterlangs; |
| 779 | + $this->assocAddToLinkchanges( $insert, 'interlang', $existing, $current ); |
| 780 | + |
| 781 | + // Page links |
| 782 | + $existing = $this->getExistingLinks(); |
| 783 | + $current = $this->mLinks; |
| 784 | + $this->addToLinkChangesByNamespace( $insert, 'page', $now, $existing, $current); |
| 785 | + |
| 786 | + // Properties |
| 787 | + $existing = $this->getExistingProperties(); |
| 788 | + $current = $this->mProperties; |
| 789 | + $this->assocAddToLinkchanges( $insert, 'property', $existing, $current ); |
| 790 | + |
| 791 | + // Templates |
| 792 | + $existing = $this->getExistingTemplates(); |
| 793 | + $current = $this->mTemplates; |
| 794 | + $this->addToLinkChangesByNamespace( $insert, 'template', $now, $existing, $current); |
| 795 | + |
| 796 | + $this->mDb->insert( 'recentlinkchanges', $insert, __METHOD__ ); |
| 797 | + |
| 798 | + } |
| 799 | + |
| 800 | + /* |
| 801 | + * Compute the difference for arrays of titles with namespace $ns and add |
| 802 | + * them to $insert. |
| 803 | + */ |
| 804 | + private function simpleAddToLinkchanges( &$insert, $type, $now, $existing, $current, $ns ) { |
| 805 | + |
| 806 | + $insertions = array_diff( $current, $existing ); |
| 807 | + foreach ( $insertions as $item ) |
| 808 | + $insert[] = array_combine(self::$rlcFields, array( |
| 809 | + $type, $now, self::INSERTION, |
| 810 | + $this->mId, $ns, $item, null |
| 811 | + ) ); |
| 812 | + $deletions = array_diff( $existing, $current ); |
| 813 | + foreach ( $deletions as $item ) |
| 814 | + $insert[] = array_combine(self::$rlcFields, array( |
| 815 | + $type, $now, self::DELETION, |
| 816 | + $this->mId, $ns, $item, null |
| 817 | + ) ); |
| 818 | + |
| 819 | + } |
| 820 | + |
| 821 | + /* |
| 822 | + * Compute the difference for associative arrays and insert them to |
| 823 | + * $insert as title => blob. |
| 824 | + */ |
| 825 | + function assocAddToLinkChanges( &$insert, $type, $now, $existing, $current ) { |
| 826 | + $insertions = array_diff_assoc( $current, $existing ); |
| 827 | + foreach ( $insertions as $key => $value ) |
| 828 | + $insert[] = array_combine(self::$rlcFields, array( |
| 829 | + $type, $now, self::INSERTION, |
| 830 | + $this->mId, null, $key, $value |
| 831 | + ) ); |
| 832 | + $deletions = array_diff_assoc( $existing, $current ); |
| 833 | + foreach ( $deletions as $key => $value ) |
| 834 | + $insert[] = array_combine(self::$rlcFields, array( |
| 835 | + $type, $now, self::DELETION, |
| 836 | + $this->mId, null, $key, $value |
| 837 | + ) ); |
| 838 | + } |
| 839 | + |
| 840 | + /* |
| 841 | + * Format arrays in the form $namespace => $titleArray for use in |
| 842 | + * simpleAddLinkLinkChanges |
| 843 | + */ |
| 844 | + function addToLinkChangesByNamespace( &$insert, $type, $now, $existing, $current ) { |
| 845 | + $namespaces = array_merge( array_keys( $existing ), array_keys( $current ) ); |
| 846 | + foreach ( $namespaces as $ns ) |
| 847 | + $this->simpleAddToLinkchanges( $insert, $type, $now, |
| 848 | + isset( $existing[$ns] ) ? array_keys( $existing[$ns] ) : array(), |
| 849 | + isset( $current[$ns] ) ? array_keys( $current[$ns] ) : array(), |
| 850 | + $ns ); |
| 851 | + } |
702 | 852 | } |
Index: trunk/phase3/includes/DefaultSettings.php |
— | — | @@ -3262,3 +3262,8 @@ |
3263 | 3263 | * ting this variable false. |
3264 | 3264 | */ |
3265 | 3265 | $wgUseAutomaticEditSummaries = true; |
| 3266 | + |
| 3267 | +/* |
| 3268 | + * Track link changes |
| 3269 | + */ |
| 3270 | +$wgTrackLinkChanges = false; |
Index: trunk/phase3/RELEASE-NOTES |
— | — | @@ -185,7 +185,9 @@ |
186 | 186 | * Add a new hook NormalizeMessageKey to allow extensions to replace messages before |
187 | 187 | the database is potentially queried |
188 | 188 | * (bug 9736) Redirects on Special:Fewestrevisions are now marked as such. |
189 | | - |
| 189 | +* (bug 13588) Experimentally track link changes if $wgTrackLinkChanges is set |
| 190 | + to true. Requires schema change. |
| 191 | + |
190 | 192 | === Bug fixes in 1.13 === |
191 | 193 | |
192 | 194 | * (bug 10677) Add link to the file description page on the shared repository |