BIG ONE - Possible DB server killer! (deactivated by default:-)
authorMagnus Manske <magnusmanske@users.mediawiki.org>
Mon, 9 Jan 2006 14:20:26 +0000 (14:20 +0000)
committerMagnus Manske <magnusmanske@users.mediawiki.org>
Mon, 9 Jan 2006 14:20:26 +0000 (14:20 +0000)
Recent changes can now be filtered by categories (AND and OR)
To enable, set $wgAllowCategorizedRecentChanges = true ; in LocalSettings

includes/Categoryfinder.php [new file with mode: 0644]
includes/DefaultSettings.php
includes/SpecialRecentchanges.php
languages/Language.php

diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
new file mode 100644 (file)
index 0000000..f9c4aa3
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+/*
+The "Categoryfinder" class takes a list of articles, creates an internal representation of all their parent
+categories (as well as parents of parents etc.). From this representation, it determines which of these articles
+are in one or all of a given subset of categories.
+
+Example use :
+
+       # Determines wether the article with the page_id 12345 is in both
+       # "Category 1" and "Category 2" or their subcategories, respectively
+       
+       $cf = new Categoryfinder ;
+       $cf->seed (
+               array ( 12345 ) ,
+               array ( "Category 1","Category 2" ) ,
+               "AND"
+       ) ;
+       $a = $cf->run() ;
+       print implode ( "," , $a ) ;
+
+*/
+
+
+if( !defined( 'MEDIAWIKI' ) ) die();
+
+class Categoryfinder {
+
+       var $articles = array () ; # The original article IDs passed to the seed function
+       var $deadend = array () ; # Array of DBKEY category names for categories that don't have a page
+       var $parents = array () ; # Array of [ID => array()]
+       var $next = array () ; # Array of article/category IDs
+       var $targets = array () ; # Array of DBKEY category names
+       var $name2id = array () ;
+       var $mode ; # "AND" or "OR"
+       var $dbr ; # Read-DB slave
+
+       /**
+        * Constructor (currently empty).
+       */
+       function Categoryfinder () {
+       }
+
+       /**
+        * Initializes the instance. Do this prior to calling run().
+        @param $article_ids Array of article IDs
+       */
+       function seed ( $article_ids , $categories , $mode = "AND" ) {
+               $this->articles = $article_ids ;
+               $this->next = $article_ids ;
+               $this->mode = $mode ;
+
+               # Set the list of target categories; convert them to DBKEY form first
+               $this->targets = array () ;
+               foreach ( $categories AS $c ) {
+                       $ct = Title::newFromText ( $c , NS_CATEGORY ) ;
+                       $c = $ct->getDBkey () ;
+                       $this->targets[$c] = $c ;
+               }
+       }
+
+       /**
+        * Iterates through the parent tree starting with the seed values,
+        * then checks the articles if they match the conditions
+        @return array of page_ids (those given to seed() that match the conditions)
+       */
+       function run () {
+               $this->dbr =& wfGetDB( DB_SLAVE );
+               while ( count ( $this->next ) > 0 ) {
+                       $this->scan_next_layer () ;
+               }
+
+               # Now check if this applies to the individual articles
+               $ret = array () ;
+               foreach ( $this->articles AS $article ) {
+                       $conds = $this->targets ;
+                       if ( $this->check ( $article , $conds ) ) {
+                               # Matches the conditions
+                               $ret[] = $article ;
+                       }
+               }
+               return $ret ;
+       }
+
+       /**
+        * This functions recurses through the parent representation, trying to match the conditions
+        @param $id The article/category to check
+        @param $conds The array of categories to match
+        @return bool Does this match the conditions?
+       */
+       function check ( $id , &$conds ) {
+               # Shortcut (runtime paranoia): No contitions=all matched
+               if ( count ( $conds ) == 0 ) return true ;
+               
+               if ( !isset ( $this->parents[$id] ) ) return false ;
+
+               # iterate through the parents
+               foreach ( $this->parents[$id] AS $p ) {
+                       $pname = $p->cl_to ;
+                       
+                       # Is this a condition?
+                       if ( isset ( $conds[$pname] ) ) {
+                               # This key is in the category list!
+                               if ( $this->mode == "OR" ) {
+                                       # One found, that's enough!
+                                       $conds = array () ;
+                                       return true ;
+                               } else {
+                                       # Assuming "AND" as default
+                                       unset ( $conds[$pname] ) ;
+                                       if ( count ( $conds ) == 0 ) {
+                                               # All conditions met, done
+                                               return true ;
+                                       }
+                               }
+                       }
+                       
+                       # Not done yet, try sub-parents
+                       if ( !isset ( $this->name2id[$pname] ) ) {
+                               # No sub-parent
+                               continue ;
+                       }
+                       $done = $this->check ( $this->name2id[$pname] , $conds ) ;
+                       if ( $done OR count ( $conds ) == 0 ) {
+                               # Subparents have done it!
+                               return true ;
+                       }
+               }
+               return false ;
+       }
+
+       /**
+        * Scans a "parent layer" of the articles/categories in $this->next
+       */
+       function scan_next_layer () {
+               $fname = "Categoryfinder::scan_next_layer" ;
+       
+               # Find all parents of the article currently in $this->next
+               $layer = array () ;
+               $res = $this->dbr->select(
+                               /* FROM   */ 'categorylinks',
+                               /* SELECT */ '*',
+                               /* WHERE  */ array( 'cl_from' => $this->next ),
+                               $fname."-1"
+               );
+               while ( $o = $this->dbr->fetchObject( $res ) ) {
+                       $k = $o->cl_to ;
+
+                       # Update parent tree
+                       if ( !isset ( $this->parents[$o->cl_from] ) ) {
+                               $this->parents[$o->cl_from] = array () ;
+                       }
+                       $this->parents[$o->cl_from][$k] = $o ;
+
+                       # Ignore those we already have
+                       if ( in_array ( $k , $this->deadend ) ) continue ;
+                       if ( isset ( $this->name2id[$k] ) ) continue ;
+
+                       # Hey, new category!
+                       $layer[$k] = $k ;
+               }
+               $this->dbr->freeResult( $res ) ;
+
+               $this->next = array() ;
+               
+               # Find the IDs of all category pages in $layer, if they exist
+               if ( count ( $layer ) > 0 ) {
+                       $res = $this->dbr->select(
+                                       /* FROM   */ 'page',
+                                       /* SELECT */ 'page_id,page_title',
+                                       /* WHERE  */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ),
+                                       $fname."-2"
+                       );
+                       while ( $o = $this->dbr->fetchObject( $res ) ) {
+                               $id = $o->page_id ;
+                               $name = $o->page_title ;
+                               $this->name2id[$name] = $id ;
+                               $this->next[] = $id ;
+                               unset ( $layer[$name] ) ;
+                       }
+                       $this->dbr->freeResult( $res ) ;
+                       }
+
+               # Mark dead ends
+               foreach ( $layer AS $v ) {
+                       $this->deadend[$v] = $v ;
+               }
+       }
+
+} # END OF CLASS "Categoryfinder"
+
+?>
\ No newline at end of file
index 834bbee..feb430e 100644 (file)
@@ -1807,4 +1807,9 @@ $wgUseTrackbacks = false;
 
 $wgFilterRobotsWL = false;
 
+/**
+ * Enable filtering of categories in Recentchanges
+ */
+$wgAllowCategorizedRecentChanges = false ;
+
 ?>
index 3346b7a..f48a253 100644 (file)
@@ -18,6 +18,7 @@ require_once( 'Revision.php' );
 function wfSpecialRecentchanges( $par, $specialPage ) {
        global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
        global $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
+       global $wgAllowCategorizedRecentChanges ;
        $fname = 'wfSpecialRecentchanges';
 
        # Get query parameters
@@ -33,6 +34,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
        /* text */ 'from' => '',
        /* text */ 'namespace' => null,
        /* bool */ 'invert' => false,
+       /* bool */ 'categories_any' => true,
        );
 
        extract($defaults);
@@ -168,6 +170,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
 
                // Run existence checks
                $batch->execute();
+               $any = $wgRequest->getBool ( 'categories_any' , false ) ;
 
                // Output header
                if ( !$specialPage->including() ) {
@@ -185,6 +188,7 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
                        wfAppendToArrayIfNotDefault( 'from', $from, $defaults, $nondefaults);
                        wfAppendToArrayIfNotDefault( 'namespace', $namespace, $defaults, $nondefaults);
                        wfAppendToArrayIfNotDefault( 'invert', $invert, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'categories_any', $any, $defaults, $nondefaults);
 
                        // Add end of the texts
                        $wgOut->addHTML( '<div class="rcoptions">' . rcOptionsPanel( $defaults, $nondefaults ) . "\n" );
@@ -196,6 +200,13 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
                $wgOut->setSyndicated( true );
 
                $list = ChangesList::newFromUser( $wgUser );
+               
+               if ( $wgAllowCategorizedRecentChanges ) {
+                       $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+                       $categories = str_replace ( "|" , "\n" , $categories ) ;
+                       $categories = explode ( "\n" , $categories ) ;
+                       rcFilterByCategories ( $rows , $categories , $any ) ;
+               }
 
                $s = $list->beginRecentChangesList();
                $counter = 1;
@@ -234,6 +245,53 @@ function wfSpecialRecentchanges( $par, $specialPage ) {
        }
 }
 
+function rcFilterByCategories ( &$rows , $categories , $any ) {
+       require_once ( 'Categoryfinder.php' ) ;
+       
+       # Filter categories
+       $cats = array () ;
+       foreach ( $categories AS $cat ) {
+               $cat = trim ( $cat ) ;
+               if ( $cat == "" ) continue ;
+               $cats[] = $cat ;
+       }
+       
+       # Filter articles
+       $articles = array () ;
+       $a2r = array () ;
+       foreach ( $rows AS $k => $r ) {
+               $nt = Title::newFromText ( $r->rc_title , $r->rc_namespace ) ;
+               $id = $nt->getArticleID() ;
+               if ( $id == 0 ) continue ; # Page might have been deleted...
+               if ( !in_array ( $id , $articles ) ) {
+                       $articles[] = $id ;
+               }
+               if ( !isset ( $a2r[$id] ) ) {
+                       $a2r[$id] = array() ;
+               }
+               $a2r[$id][] = $k ;
+       }
+       
+       # Shortcut?
+       if ( count ( $articles ) == 0 OR count ( $cats ) == 0 )
+               return ;
+       
+       # Look up
+       $c = new Categoryfinder ;
+       $c->seed ( $articles , $cats , $any ? "OR" : "AND" ) ;
+       $match = $c->run () ;
+       
+       # Filter
+       $newrows = array () ;
+       foreach ( $match AS $id ) {
+               foreach ( $a2r[$id] AS $rev ) {
+                       $k = $rev ;
+                       $newrows[$k] = $rows[$k] ;
+               }
+       }
+       $rows = $newrows ;
+}
+
 function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
        global $messageMemc, $wgDBname, $wgFeedCacheTimeout;
        global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
@@ -460,13 +518,28 @@ function rcOptionsPanel( $defaults, $nondefaults ) {
  * @return string
  */
 function rcNamespaceForm ( $namespace, $invert, $nondefaults ) {
-       global $wgContLang, $wgScript;
+       global $wgContLang, $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest;
        $t = Title::makeTitle( NS_SPECIAL, 'Recentchanges' );
 
        $namespaceselect = HTMLnamespaceselector($namespace, '');
        $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . "\" />\n";
        $invertbox = "<input type='checkbox' name='invert' value='1' id='nsinvert'" . ( $invert ? ' checked="checked"' : '' ) . ' />';
-
+       
+       if ( $wgAllowCategorizedRecentChanges ) {
+               $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+               $any = $wgRequest->getBool ( 'categories_any' , true ) ;
+               $cb_arr = array( 'type' => 'checkbox', 'name' => 'categories_any', 'value' => "1" ) ;
+               if ( $any ) $cb_arr['checked'] = "checked" ;
+               $catbox = "<br/>" ;
+               $catbox .= wfMsg('rc_categories') . " ";
+               $catbox .= wfElement('input', array( 'type' => 'text', 'name' => 'categories', 'value' => $categories));
+               $catbox .= " &nbsp;" ;
+               $catbox .= wfElement('input', $cb_arr );
+               $catbox .= wfMsg('rc_categories_any');
+       } else {
+               $catbox = "" ;
+       }
+       
        $out = "<div class='namespacesettings'><form method='get' action='{$wgScript}'>\n";
 
        foreach ( $nondefaults as $key => $value ) {
@@ -478,7 +551,7 @@ function rcNamespaceForm ( $namespace, $invert, $nondefaults ) {
        $out .= "
 <div id='nsselect' class='recentchanges'>
        <label for='namespace'>" . wfMsgHtml('namespace') . "</label>
-       {$namespaceselect}{$submitbutton}{$invertbox} <label for='nsinvert'>" . wfMsgHtml('invert') . "</label>\n</div>";
+       {$namespaceselect}{$submitbutton}{$invertbox} <label for='nsinvert'>" . wfMsgHtml('invert') . "</label>{$catbox}\n</div>";
        $out .= '</form></div>';
        return $out;
 }
index 6956f13..765c6f3 100644 (file)
@@ -1005,6 +1005,8 @@ Unselected groups will not be changed. You can deselect a group with CTRL + Left
 'sectionlink' => '→',
 'number_of_watching_users_RCview'      => '[$1]',
 'number_of_watching_users_pageview'    => '[$1 watching user/s]',
+'rc_categories'        => 'Limit to categories (separate with "|")',
+'rc_categories_any'    => 'Any',
 
 # Upload
 #