Index: /extensions/GrumPluginClasses/classes/GPCRequestBuilder.class.inc.php
===================================================================
--- /extensions/GrumPluginClasses/classes/GPCRequestBuilder.class.inc.php	(revision 6893)
+++ /extensions/GrumPluginClasses/classes/GPCRequestBuilder.class.inc.php	(revision 6894)
@@ -2,7 +2,7 @@
 /* -----------------------------------------------------------------------------
   class name: GCPRequestBuilder
-  class version  : 1.0.0
+  class version  : 1.1.0
   plugin version : 3.1.0
-  date           : 2010-04-29
+  date           : 2010-09-08
 
   ------------------------------------------------------------------------------
@@ -14,10 +14,50 @@
     << May the Little SpaceFrog be with you ! >>
   ------------------------------------------------------------------------------
-
+  *
   * theses classes provides base functions to manage search pictures in the
   * database
   *
-
-
+  *
+  * HOW TO USE IT ?
+  * ===============
+  *
+  * when installing the plugin, you have to register the usage of the request
+  * builder
+  *
+  * 1/ Create a RBCallback class
+  *  - extends the GPCSearchCallback class ; the name of the extended class must
+  *    be "RBCallBack%" => replace the "%" by the plugin name
+  *     for example : 'ThePlugin' => 'RBCallBackThePlugin'
+  *
+  * 2/ In the plugin 'maintain.inc.php' file :
+  *  - function plugin_install, add :
+  *       GPCRequestBuilder::register('plugin name', 'path to the RBCallback classe');
+  *          for example : GPCRequestBuilder::register('ThePlugin', $piwigo_path.'plugins/ThePlugin/rbcallback_file_name.php');
+  *
+  *
+  *  - function plugin_uninstall, add :
+  *       GPCRequestBuilder::unregister('plugin name');
+  *          for example : GPCRequestBuilder::unregister('ThePlugin');
+  *
+  * 3/ In the plugin code, put somewhere
+  *     GPCRequestBuilder::loadJSandCSS();
+  *     => this will load specific JS and CSS in the page, by adding url in the
+  *        the header, so try to put this where you're used to prepare the header
+  *
+  * 4/ to display the request builder, just add the returned string in the html
+  *    page
+  *       $stringForTheTemplate=GPCRequestBuilder::displaySearchPage();
+  *
+  *
+  *
+  * HOW DOES THE REQUEST BUILDER WORKS ?
+  * ====================================
+  *
+  * the request builder works in 2 steps :
+  *  - first step  : build a cache, to associate all image id corresponding to
+  *                  the search criterion
+  *                  the cache is an association of request ID/image id
+  *  - second step : use the cache to retrieve images informations
+  *
   ------------------------------------------------------------------------------
   :: HISTORY
@@ -25,4 +65,6 @@
 | release | date       |
 | 1.0.0   | 2010/04/30 | * start coding
+|         |            |
+| 1.1.0   | 2010/09/08 | * add functionnalities to manage complex requests
 |         |            |
 |         |            |
@@ -34,6 +76,4 @@
 |         |            |
 |         |            |
-|         |            |
-|         |            |
 
   --------------------------------------------------------------------------- */
@@ -42,11 +82,83 @@
 include_once('GPCTables.class.inc.php');
 
-
+/**
+ *
+ * Preparing the cache
+ * -------------------
+ * To prepare the cache, the request builder use the following functions :
+ *  - getFrom
+ *  - getWhere
+ *  - getJoin
+ *  - getImageId (used for multirecord requests only)
+ *
+ * Retrieving the results
+ * ----------------------
+ * To retrieve the image informations, the request builder uses the following
+ * functions :
+ *  - getSelect
+ *  - getFrom
+ *  - getJoin
+ *  - getFilter (in fact, the result of this function is stored while the cache
+ *               is builded, but it is used only when retrieving the results for
+ *               multirecord tables)
+ *
+ *
+ * Example 1 : single record request
+ * ---------------------------------
+ * Consider the table "tableA" like this
+ *
+ *  - (*) imageId
+ *  -     att1
+ *  -     att2
+ *  The primary key is the 'imageId' attribute
+ *    => for one imageId, you can only have ZERO or ONE record
+ *
+ *  getSelect returns : "tableA.att1, tableA.att2"
+ *  getFrom returns   : "tableA"
+ *  getWhere returns  : "tableA.att1 = zzzz"
+ *  getJoin returns   : "tableA.imageId = pit.id"
+ *  getFilter returns : ""
+ *
+ *
+ * Example 2 : multi records request
+ * ---------------------------------
+ * Consider the table "tableA" like this
+ *
+ *  - (*) imageId
+ *  - (*) localId
+ *  -     att1
+ *  -     att2
+ *  The primary key is the 'imageId'+'localId' attributes
+ *    => for one imageId, you can have ZERO or more than ONE record
+ *       when you register the class, you have to set the $multiRecord parameter
+ *       to 'y'
+ *
+ *  gatImageId returns : "tableA.imageId"
+ *  getSelect returns  : "tableA.att1, tableA.att2"
+ *  getFrom returns    : "tableA"
+ *  getWhere returns   : "tableA.att1 = zzzz AND tableA.att1 = xxxx"
+ *  getJoin returns    : "tableA.id = pit.id"
+ *  getFilter returns  : ""
+ *
+ */
 class GPCSearchCallback {
 
   /**
+   * the getImageId returns the name of the image id attribute
+   * return String
+   */
+  static public function getImageId()
+  {
+    return("");
+  }
+
+  /**
    * the getSelect function must return an attribute list separated with a comma
    *
    * "att1, att2, att3, att4"
+   *
+   * you can specifie tables names and aliases
+   *
+   * "table1.att1 AS alias1, table1.att2 AS alias2, table2.att3 AS alias3"
    */
   static public function getSelect($param="")
@@ -76,6 +188,6 @@
 
   /**
-   * the getJoin function must return a ready to use where allowing to join the
-   * IMAGES table (key : pit.id) with given conditions
+   * the getJoin function must return a ready to use sql statement allowing to
+   * join the IMAGES table (key : pit.id) with given conditions
    *
    * "att3 = pit.id "
@@ -86,4 +198,18 @@
   }
 
+
+  /**
+   * the getFilter function must return a ready to use where clause
+   * this where clause is used to filter the cache when the used tables can
+   * return more than one result
+   *
+   * the filter can be empty, can be equal to the where clause, or can be equal
+   * to a sub part of the where clause
+   *
+   */
+  static public function getFilter($param="")
+  {
+    return(self::getWhere($param));
+  }
 
   /**
@@ -152,7 +278,8 @@
 
   static public $pluginName = 'GPCRequestBuilder';
-  static public $version = '1.0.0';
+  static public $version = '1.1.0';
 
   static private $tables = Array();
+  static protected $tGlobalId=0;
 
   /**
@@ -173,5 +300,5 @@
         'fileName' => $fileName,
         'date' => date("Y-m-d H:i:s"),
-        'version' => self::$version,
+        'version' => self::$version
       );
       return(GPCCore::saveConfig(self::$pluginName, $config));
@@ -232,5 +359,5 @@
   static public function init($prefixeTable, $pluginNameFile)
   {
-    $list=Array('request', 'result_cache');
+    $list=Array('request', 'result_cache', 'temp');
 
     for($i=0;$i<count($list);$i++)
@@ -253,4 +380,5 @@
   `execution_time` float unsigned NOT NULL default '0',
   `connected_plugin` char(255) NOT NULL,
+  `filter` text NOT NULL,
   PRIMARY KEY  (`id`)
 )
@@ -263,4 +391,11 @@
 )
 CHARACTER SET utf8 COLLATE utf8_general_ci",
+
+"CREATE TABLE `".self::$tables['temp']."` (
+  `requestId` char(30) NOT NULL,
+  `imageId` mediumint(8) unsigned NOT NULL,
+  PRIMARY KEY  (`request`,`id`)
+)
+CHARACTER SET utf8 COLLATE utf8_general_ci",
   );
 
@@ -277,5 +412,5 @@
   {
     $tablef= new GPCTables(self::$tables);
-    $tablef->drop($tables_def);
+    $tablef->drop();
     return(true);
   }
@@ -378,4 +513,43 @@
 
   /**
+   * prepare the temporary table used for multirecord requests
+   *
+   * @param Integer $requestNumber : id of request
+   * @return String : name of the request key temporary table
+   */
+  static private function prepareTempTable($requestNumber)
+  {
+    //$tableName=call_user_func(Array('RBCallBack'.$plugin, 'getFrom'));
+    //$imageIdName=call_user_func(Array('RBCallBack'.$plugin, 'getImageId'));
+
+    $tempWHERE=array();
+    foreach($_REQUEST['extraData'] as $key => $extraData)
+    {
+      $tmpWHERE[$key]=array(
+        'plugin' => $extraData['owner'],
+        'where' => call_user_func(Array('RBCallBack'.$extraData['owner'], 'getWhere'), $extraData['param'])
+      );
+    }
+
+    $sql="INSERT INTO ".self::$tables['temp']." ".self::buildGroupRequest($_REQUEST[$_REQUEST['requestName']], $tmpWHERE, $_REQUEST['operator'], ' AND ', $requestNumber);
+//echo $sql;
+    $result=pwg_query($sql);
+
+    return($requestNumber);
+  }
+
+  /**
+   * clear the temporary table used for multirecord requests
+   *
+   * @param Array $requestNumber : the requestNumber to delete
+   */
+  static private function clearTempTable($requestNumber)
+  {
+    $sql="DELETE FROM ".self::$tables['temp']." WHERE requestId = '$requestNumber';";
+    pwg_query($sql);
+  }
+
+
+  /**
    * execute a query, and place result in cache
    *
@@ -390,4 +564,5 @@
 
     $registeredPlugin=self::getRegistered();
+    $requestNumber=self::getNewRequest($user['id']);
 
     $build=Array(
@@ -396,4 +571,5 @@
       'WHERE' => '',
       'GROUPBY' => '',
+      'FILTER' => '',
     );
     $tmpBuild=Array(
@@ -407,5 +583,6 @@
       'GROUPBY' => Array(
         'pit.id'
-      )
+      ),
+      'FILTER' => Array(),
     );
 
@@ -429,4 +606,5 @@
     $pluginNeeded=Array();
     $pluginList=Array();
+    $tempName=Array();
     foreach($_REQUEST['extraData'] as $key => $val)
     {
@@ -435,24 +613,25 @@
     }
 
-    /* for each needed plugin :
-     *  - include the file
-     *  - call the static public function getWhere
-     */
+    /* for each plugin, include the rb callback class file */
+    foreach($pluginList as $val)
+    {
+      if(file_exists($registeredPlugin[$val]['fileName']))
+      {
+        include_once($registeredPlugin[$val]['fileName']);
+      }
+    }
+
+    /* prepare the temp table for the request */
+    self::prepareTempTable($requestNumber);
+    $tmpBuild['FROM'][]=self::$tables['temp'];
+    $tmpBuild['JOIN'][]=self::$tables['temp'].".requestId = '".$requestNumber."'
+                        AND ".self::$tables['temp'].".imageId = pit.id";
+
+    /* for each needed plugin, prepare the filter */
     foreach($pluginNeeded as $key => $val)
     {
-      if(array_key_exists($key, $registeredPlugin))
-      {
-        if(file_exists($registeredPlugin[$key]['fileName']))
-        {
-          include_once($registeredPlugin[$key]['fileName']);
-
-          $tmpBuild['FROM'][]=call_user_func(Array('RBCallBack'.$key, 'getFrom'));
-          $tmpBuild['JOIN'][]=call_user_func(Array('RBCallBack'.$key, 'getJoin'));
-
-          foreach($val as $itemNumber => $param)
-          {
-            $tmpBuild['WHERE'][$itemNumber]=call_user_func(Array('RBCallBack'.$key, 'getWhere'), $param);
-          }
-        }
+      foreach($val as $itemNumber => $param)
+      {
+        $tmpBuild['FILTER'][$key][]='('.call_user_func(Array('RBCallBack'.$key, 'getFilter'), $param).')';
       }
     }
@@ -475,4 +654,21 @@
 
 
+    /* build FILTER
+     */
+    self::cleanArray($tmpBuild['FILTER']);
+    if(count($tmpBuild['FILTER'])>0)
+    {
+      $tmp=array();
+      foreach($tmpBuild['FILTER'] as $key=>$val)
+      {
+        $tmp[$key]='('.implode(' OR ', $val).')';
+      }
+      $build['FILTER']=' ('.implode(' AND ', $tmp).') ';
+      // array_flip twice => simply remove identical values...
+      //$build['FILTER']=' ('.implode(' AND ', array_flip(array_flip($tmpBuild['FILTER']))).') ';
+    }
+    unset($tmpBuild['FILTER']);
+
+
     /* for each plugin, adds jointure with the IMAGE table
      */
@@ -480,5 +676,6 @@
     if(count($tmpBuild['JOIN'])>0)
     {
-      $build['WHERE'].=' AND ('.implode(' AND ', $tmpBuild['JOIN']).') ';
+      if($build['WHERE']!='') $build['WHERE'].=' AND ';
+      $build['WHERE'].=' ('.implode(' AND ', $tmpBuild['JOIN']).') ';
     }
     unset($tmpBuild['JOIN']);
@@ -505,7 +702,9 @@
     $sql.=" ORDER BY pit.id ";
 
-    $requestNumber=self::getNewRequest($user['id']);
-
     $sql="INSERT INTO ".self::$tables['result_cache']." (SELECT DISTINCT $requestNumber, ".$build['SELECT']." $sql)";
+
+//echo $sql;
+
+    $returned="0;0";
 
     $result=pwg_query($sql);
@@ -513,11 +712,12 @@
     {
       $numberItems=pwg_db_changes($result);
-      self::updateRequest($requestNumber, $numberItems, 0, implode(',', $pluginList));
-
-
-      return("$requestNumber;".$numberItems);
-    }
-
-    return("0;0");
+      self::updateRequest($requestNumber, $numberItems, 0, implode(',', $pluginList), $build['FILTER']);
+
+      $returned="$requestNumber;".$numberItems;
+    }
+
+    self::clearTempTable($requestNumber);
+
+    return($returned);
   }
 
@@ -554,8 +754,8 @@
       'SELECT' => Array(
         'RB_PIT' => "pit.id AS imageId, pit.name AS imageName, pit.path AS imagePath", // from the piwigo's image table
-        'RB_PIC' => "GROUP_CONCAT(pic.category_id SEPARATOR ',') AS imageCategoriesId",     // from the piwigo's image_category table
-        'RB_PCT' => "GROUP_CONCAT(CASE WHEN pct.name IS NULL THEN '' ELSE pct.name END SEPARATOR '#sep#') AS imageCategoriesNames,
-                     GROUP_CONCAT(CASE WHEN pct.permalink IS NULL THEN '' ELSE pct.permalink END SEPARATOR '#sep#') AS imageCategoriesPLink,
-                     GROUP_CONCAT(CASE WHEN pct.dir IS NULL THEN 'V' ELSE 'P' END) AS imageCategoriesDir",   //from the piwigo's categories table
+        'RB_PIC' => "GROUP_CONCAT(DISTINCT pic.category_id SEPARATOR ',') AS imageCategoriesId",     // from the piwigo's image_category table
+        'RB_PCT' => "GROUP_CONCAT(DISTINCT CASE WHEN pct.name IS NULL THEN '' ELSE pct.name END SEPARATOR '#sep#') AS imageCategoriesNames,
+                     GROUP_CONCAT(DISTINCT CASE WHEN pct.permalink IS NULL THEN '' ELSE pct.permalink END SEPARATOR '#sep#') AS imageCategoriesPLink,
+                     GROUP_CONCAT(DISTINCT CASE WHEN pct.dir IS NULL THEN 'V' ELSE 'P' END) AS imageCategoriesDir",   //from the piwigo's categories table
       ),
       'FROM' => Array(
@@ -591,5 +791,11 @@
         {
           include_once($registeredPlugin[$val]['fileName']);
-          $tmpBuild['SELECT'][$val]=call_user_func(Array('RBCallBack'.$val, 'getSelect'));
+
+          $tmp=explode(',', call_user_func(Array('RBCallBack'.$val, 'getSelect')));
+          foreach($tmp as $key2=>$val2)
+          {
+            $tmp[$key2]=self::groupConcatAlias($val2, '#sep#');
+          }
+          $tmpBuild['SELECT'][$val]=implode(',', $tmp);
           $tmpBuild['FROM'][$val]=call_user_func(Array('RBCallBack'.$val, 'getFrom'));
           $tmpBuild['JOIN'][$val]=call_user_func(Array('RBCallBack'.$val, 'getJoin'));
@@ -612,4 +818,5 @@
     /* build WHERE
      */
+    if($request['filter']!='') $tmpBuild['WHERE'][]=$request['filter'];
     $build['WHERE']=implode(' AND ', $tmpBuild['WHERE']);
     unset($tmpBuild['WHERE']);
@@ -641,5 +848,5 @@
         .' ORDER BY pit.id '
         .' LIMIT '.$limitFrom.', '.$numPerPage;
-
+//echo $sql;
     $result=pwg_query($sql);
     if($result)
@@ -737,5 +944,10 @@
     foreach($array as $key => $val)
     {
-      if(trim($val)=='') unset($array[$key]);
+      if(is_array($val))
+      {
+        self::cleanArray($val);
+        if(count($val)==0) unset($array[$key]);
+      }
+      elseif(trim($val)=='') unset($array[$key]);
     }
   }
@@ -744,15 +956,16 @@
    * returns the alias for an attribute
    *
-   *  item1                  => returns item1
-   *  table1.item1           => returns item1
-   *  table1.item1 AS alias1 => returns alias1
-   *  item1 AS alias1        => returns alias1
-   *
-   * @param String $var : value ti examine
+   *  item1                          => returns item1
+   *  table1.item1                   => returns item1
+   *  table1.item1 AS alias1         => returns alias1
+   *  item1 AS alias1                => returns alias1
+   *  GROUP_CONCAT( .... ) AS alias1 => returns alias1
+   *
+   * @param String $var : value to examine
    * @return String : the attribute name
    */
   static private function getAttribute($val)
   {
-    preg_match('/(?:(?:[A-Z0-9_]*)\.)?([A-Z0-9_]*)(?:\s+AS\s+([A-Z0-9_]*))?/i', trim($val), $result);
+    preg_match('/(?:GROUP_CONCAT\(.*\)|(?:[A-Z0-9_]*)\.)?([A-Z0-9_]*)(?:\s+AS\s+([A-Z0-9_]*))?/i', trim($val), $result);
     if(array_key_exists(2, $result))
     {
@@ -766,4 +979,41 @@
     {
       return($val);
+    }
+  }
+
+
+  /**
+   * returns a a sql statement GROUP_CONCAT for an alias
+   *
+   *  item1                  => returns GROUP_CONCAT(item1 SEPARATOR $sep) AS item1
+   *  table1.item1           => returns GROUP_CONCAT(table1.item1 SEPARATOR $sep) AS item1
+   *  table1.item1 AS alias1 => returns GROUP_CONCAT(table1.item1 SEPARATOR $sep) AS alias1
+   *  item1 AS alias1        => returns GROUP_CONCAT(item1 SEPARATOR $sep) AS alias1
+   *
+   * @param String $val : value to examine
+   * @param String $sep : the separator
+   * @return String : the attribute name
+   */
+  static private function groupConcatAlias($val, $sep=',')
+  {
+    /*
+     * table1.item1 AS alias1
+     *
+     * $result[3] = alias1
+     * $result[2] = item1
+     * $result[1] = table1.item1
+     */
+    preg_match('/((?:(?:[A-Z0-9_]*)\.)?([A-Z0-9_]*))(?:\s+AS\s+([A-Z0-9_]*))?/i', trim($val), $result);
+    if(array_key_exists(3, $result))
+    {
+      return("GROUP_CONCAT(".$result[1]." SEPARATOR '$sep') AS ".$result[3]);
+    }
+    elseif(array_key_exists(2, $result))
+    {
+      return("GROUP_CONCAT(".$result[1]." SEPARATOR '$sep') AS ".$result[2]);
+    }
+    else
+    {
+      return("GROUP_CONCAT($val SEPARATOR '$sep') AS ".$val);
     }
   }
@@ -778,5 +1028,5 @@
   static private function getNewRequest($userId)
   {
-    $sql="INSERT INTO ".self::$tables['request']." VALUES('', '$userId', '".date('Y-m-d H:i:s')."', 0, 0, '')";
+    $sql="INSERT INTO ".self::$tables['request']." VALUES('', '$userId', '".date('Y-m-d H:i:s')."', 0, 0, '', '')";
     $result=pwg_query($sql);
     if($result)
@@ -796,10 +1046,11 @@
    * @return Boolean : true if request was updated, otherwise false
    */
-  static private function updateRequest($requestId, $numItems, $executionTime, $pluginList)
+  static private function updateRequest($requestId, $numItems, $executionTime, $pluginList, $additionalFilter)
   {
     $sql="UPDATE ".self::$tables['request']."
             SET num_items = $numItems,
                 execution_time = $executionTime,
-                connected_plugin = '$pluginList'
+                connected_plugin = '$pluginList',
+                filter = '".mysql_escape_string($additionalFilter)."'
             WHERE id = $requestId";
     $result=pwg_query($sql);
@@ -820,5 +1071,5 @@
   {
     $returned=false;
-    $sql="SELECT user_id, date, num_items, execution_time, connected_plugin
+    $sql="SELECT user_id, date, num_items, execution_time, connected_plugin, filter
           FROM ".self::$tables['request']."
           WHERE id = $requestId";
@@ -836,5 +1087,6 @@
 
   /**
-   * internal function used by the executeRequest function
+   * internal function used by the executeRequest function for single record
+   * requests
    *
    * this function is called recursively
@@ -861,4 +1113,97 @@
     }
     return('('.implode($operator, $returned).')');
+  }
+
+
+  /**
+   * internal function used by the executeRequest function for multi records
+   * requests
+   *
+   * this function is called recursively
+   *
+   * @param Array $groupContent :
+   * @param Array $items :
+   * @return String : a SQL request
+   */
+  static private function buildGroupRequest($groupContent, $whereItems, $groups, $operator, $requestNumber)
+  {
+    $returnedS='';
+    $returned=Array();
+    foreach($groupContent as $key => $val)
+    {
+      if(strpos($val['id'], 'iCbGroup')!==false)
+      {
+        preg_match('/[0-9]*$/i', $val['id'], $groupNumber);
+
+        $groupValue=self::buildGroupRequest($val['children'], $whereItems, $groups, $groups[$groupNumber[0]], $requestNumber);
+
+        if($groupValue!='')
+          $returned[]=array(
+            'mode'  => 'group',
+            'value' => $groupValue
+          );
+      }
+      else
+      {
+        preg_match('/[0-9]*$/i', $val['id'], $itemNumber);
+
+        $returned[]=array(
+          'mode'  => 'item',
+          'plugin' => $whereItems[$itemNumber[0]]['plugin'],
+          'value' => " (".$whereItems[$itemNumber[0]]['where'].") "
+        );
+      }
+    }
+
+    if(count($returned)>0)
+    {
+      if(strtolower(trim($operator))=='and')
+      {
+        $tId=0;
+        foreach($returned as $key=>$val)
+        {
+          if($tId>0) $returnedS.=" JOIN ";
+
+          if($val['mode']=='item')
+          {
+            $returnedS.="(SELECT ".call_user_func(Array('RBCallBack'.$val['plugin'], 'getImageId'))." AS imageId
+                          FROM ".call_user_func(Array('RBCallBack'.$val['plugin'], 'getFrom'))."
+                          WHERE ".$val['value'].") t".self::$tGlobalId." ";
+          }
+          else
+          {
+            $returnedS.="(".$val['value'].") t".self::$tGlobalId." ";
+          }
+
+          if($tId>0) $returnedS.=" ON t".(self::$tGlobalId-1).".imageId = t".self::$tGlobalId.".imageId ";
+          $tId++;
+          self::$tGlobalId++;
+        }
+        $returnedS="SELECT '$requestNumber', t".(self::$tGlobalId-$tId).".imageId FROM ".$returnedS;
+      }
+      else
+      {
+        foreach($returned as $key=>$val)
+        {
+          if($returnedS!='') $returnedS.=" UNION DISTINCT ";
+
+          if($val['mode']=='item')
+          {
+            $returnedS.="SELECT '$requestNumber', t".self::$tGlobalId.".imageId
+                          FROM (SELECT ".call_user_func(Array('RBCallBack'.$val['plugin'], 'getImageId'))." AS imageId
+                                FROM ".call_user_func(Array('RBCallBack'.$val['plugin'], 'getFrom'))."
+                                WHERE ".$val['value'].") t".self::$tGlobalId." ";
+          }
+          else
+          {
+            $returnedS.="SELECT '$requestNumber', t".self::$tGlobalId.".imageId FROM (".$val['value'].") t".self::$tGlobalId;
+          }
+
+          self::$tGlobalId++;
+        }
+      }
+    }
+
+    return($returnedS);
   }
 
@@ -957,10 +1302,19 @@
   /**
    * display search page
-   */
-  static public function displaySearchPage()
+   *
+   * @param Array $filter : an array of string ; each item is the name of a
+   *                        registered plugin
+   *                        if no parameters are given, no filter is applied
+   *                        otherwise only plugin wich name is given are
+   *                        accessible
+   */
+  static public function displaySearchPage($filter=array())
   {
     global $template, $lang;
 
     load_language('rbuilder.lang', GPC_PATH);
+
+    if(is_string($filter)) $filter=array($filter);
+    $filter=array_flip($filter);
 
     $template->set_filename('gpc_search_page',
@@ -971,5 +1325,6 @@
     foreach($registeredPlugin as $key=>$val)
     {
-      if(array_key_exists($key, $registeredPlugin))
+      if(array_key_exists($key, $registeredPlugin) and
+         (count($filter)==0 or array_key_exists($key, $filter)))
       {
         if(file_exists($registeredPlugin[$key]['fileName']))
Index: /extensions/GrumPluginClasses/css/rbuilder.css
===================================================================
--- /extensions/GrumPluginClasses/css/rbuilder.css	(revision 6893)
+++ /extensions/GrumPluginClasses/css/rbuilder.css	(revision 6894)
@@ -112,2 +112,38 @@
 {
   margin-bottom:3px;
+  cursor:default;
+}
+
+div#iMenuCaddieBar {
+  margin-left:8px;
+  padding-left:8px;
+  border-left:1px solid;
+  cursor:default;
+}
+
+
+div#iMenuCaddieBar div#iMenuCaddieItems {
+  display:none;
+  list-style-type:none;
+  max-height:30px;
+  position:absolute;
+  padding:0px;
+}
+
+div#iMenuCaddieBar ul {
+  margin:4px 0 4px -8px;
+  padding-bottom:2px;
+  padding-left:16px;
+  padding-right:2px;
+}
+
+div#iMenuCaddieBar ul li {
+  list-style-type:none;
+  padding:4px;
+}
+
+div#iMenuCaddieBar:hover  div#iMenuCaddieItems
+{
+  cursor:pointer;
+  display:block;
+}
Index: /extensions/GrumPluginClasses/gpc_ajax.php
===================================================================
--- /extensions/GrumPluginClasses/gpc_ajax.php	(revision 6894)
+++ /extensions/GrumPluginClasses/gpc_ajax.php	(revision 6894)
@@ -0,0 +1,147 @@
+<?php
+/*
+ * -----------------------------------------------------------------------------
+ * Plugin Name: Grum Plugins Classes.3
+ * -----------------------------------------------------------------------------
+ * Author     : Grum
+ *   email    : grum@piwigo.org
+ *   website  : http://photos.grum.fr
+ *   PWG user : http://forum.piwigo.org/profile.php?id=3706
+ *
+ *   << May the Little SpaceFrog be with you ! >>
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * See main.inc.php for release information
+ *
+ * manage all the ajax requests
+ * -----------------------------------------------------------------------------
+ */
+
+  define('PHPWG_ROOT_PATH',dirname(dirname(dirname(__FILE__))).'/');
+
+  /*
+   * set ajax module in admin mode if request is used for admin interface
+   */
+  if(!isset($_REQUEST['ajaxfct'])) $_REQUEST['ajaxfct']='';
+  if(preg_match('/^admin\./i', $_REQUEST['ajaxfct']))
+  {
+    define('IN_ADMIN', true);
+  }
+
+  // the common.inc.php file loads all the main.inc.php plugins files
+  include_once(PHPWG_ROOT_PATH.'include/common.inc.php' );
+  include_once(PHPWG_PLUGINS_PATH.'GrumPluginClasses/classes/CommonPlugin.class.inc.php');
+  include_once(PHPWG_PLUGINS_PATH.'GrumPluginClasses/classes/GPCAjax.class.inc.php');
+
+  load_language('plugin.lang', GPC_PATH);
+
+  class GPC_Ajax extends CommonPlugin
+  {
+    /**
+     * constructor
+     */
+    public function __construct($prefixeTable, $filelocation)
+    {
+      $this->setPluginName("Grum Plugin Classes");
+      $this->setPluginNameFiles("gpc");
+      parent::__construct($prefixeTable, $filelocation);
+
+      $tableList=array('result_cache');
+      $this->setTablesList($tableList);
+
+      $this->loadConfig();
+      $this->checkRequest();
+      $this->returnAjaxContent();
+    }
+
+    /**
+     * check the $_REQUEST values and set default values
+     *
+     */
+    protected function checkRequest()
+    {
+      global $user;
+
+      if(!isset($_REQUEST['ajaxfct'])) $_REQUEST['ajaxfct']='';
+
+      // check if asked function is valid
+      if(!(
+           $_REQUEST['ajaxfct']=='admin.rbuilder.fillCaddie'
+          )
+        ) $_REQUEST['ajaxfct']='';
+
+      if(preg_match('/^admin\./i', $_REQUEST['ajaxfct']) and !is_admin()) $_REQUEST['ajaxfct']='';
+
+      if($_REQUEST['ajaxfct']!='')
+      {
+        /*
+         * check admin.makeStats.getList values
+         */
+        if($_REQUEST['ajaxfct']=="admin.rbuilder.fillCaddie")
+        {
+          if(!isset($_REQUEST['fillMode'])) $_REQUEST['fillMode']="add";
+
+          if(!($_REQUEST['fillMode']=="add" or
+               $_REQUEST['fillMode']=="replace")) $_REQUEST['fillMode']="add";
+
+          if(!isset($_REQUEST['requestNumber'])) $_REQUEST['ajaxfct']="";
+        }
+      }
+    }
+
+
+    /**
+     * return ajax content
+     */
+    protected function returnAjaxContent()
+    {
+      $result="<p class='errors'>An error has occured</p>";
+      switch($_REQUEST['ajaxfct'])
+      {
+        case 'admin.rbuilder.fillCaddie':
+          $result=$this->ajax_gpc_admin_rbuilderFillCaddie($_REQUEST['fillMode'], $_REQUEST['requestNumber']);
+          break;
+      }
+      GPCAjax::returnResult($result);
+    }
+
+
+
+    /*------------------------------------------------------------------------*
+     *
+     * ADMIN FUNCTIONS
+     *
+     *----------------------------------------------------------------------- */
+
+    /**
+     * fill the caddie with the result of the requestNumber
+     *
+     * @param String $fillMode : 'addCaddie' or 'replaceCaddie'
+     * @param Integer $requestNumber : number of the request in cache
+     * @return String :
+     */
+    private function ajax_gpc_admin_rbuilderFillCaddie($mode, $requestNumber)
+    {
+      global $user;
+
+      switch($mode)
+      {
+        case "replace":
+          $sql="DELETE FROM ".CADDIE_TABLE." WHERE user_id = '".$user['id']."';";
+          pwg_query($sql);
+        case "add":
+          $sql="INSERT IGNORE INTO ".CADDIE_TABLE."
+                  SELECT '".$user['id']."', image_id
+                  FROM ".$this->tables['result_cache']."
+                  WHERE id = '$requestNumber';";
+          pwg_query($sql);
+          break;
+      }
+    }
+
+  } //class
+
+
+  $returned=new GPC_Ajax($prefixeTable, __FILE__);
+?>
Index: /extensions/GrumPluginClasses/language/fr_FR/rbuilder.lang.php
===================================================================
--- /extensions/GrumPluginClasses/language/fr_FR/rbuilder.lang.php	(revision 6893)
+++ /extensions/GrumPluginClasses/language/fr_FR/rbuilder.lang.php	(revision 6894)
@@ -11,3 +11,10 @@
 $lang['gpc_rb_textOR']='Au moins un critère doit être vérifié';
 
+$lang['gpc_manage_caddie'] = 'Gestion du panier';
+$lang['gpc_add_caddie'] = 'Le résultat viens s\'ajouter au panier';
+$lang['gpc_replace_caddie'] = 'Le résultat viens remplacer le panier';
+$lang['gpc_the_caddie_is_updated'] = 'Le panier a été mis à jour';
+$lang['gpc_something_is_wrong_on_the_server_side'] = 'Une anomalie est survenue lors de la connexion avec le serveur';
+
+
 ?>
Index: /extensions/GrumPluginClasses/main.inc.php
===================================================================
--- /extensions/GrumPluginClasses/main.inc.php	(revision 6893)
+++ /extensions/GrumPluginClasses/main.inc.php	(revision 6894)
@@ -53,9 +53,9 @@
 |         |            |   included)
 |         |            |
-| 3.2.0   | 2010/06/20 | * Enhance GPCTabSheet functionnalities
+| 3.2.0   | 2010/09/12 | * Enhance GPCTabSheet functionnalities
 |         |            |   - possibility to choose tab classes
 |         |            | * Add the simpleTip.js
-|         |            |
-|         |            |
+|         |            | * Enhance GPCRequestBuilder functionnalities
+|         |            |   - now abble to manage complex request with multi-record
 |         |            |
 |         |            |
Index: /extensions/GrumPluginClasses/templates/GPCRequestBuilder_search.tpl
===================================================================
--- /extensions/GrumPluginClasses/templates/GPCRequestBuilder_search.tpl	(revision 6893)
+++ /extensions/GrumPluginClasses/templates/GPCRequestBuilder_search.tpl	(revision 6894)
@@ -68,4 +68,13 @@
           }
           break;
+        case 'fillCaddie':
+          /* function 'fillCaddie' : allows to fill the caddie with the search result
+           *
+           */
+          if(arguments.length==2)
+          {
+            fillCaddie(arguments[1], this.getRequestNumber());
+          }
+          break;
       }
     }
@@ -132,5 +141,5 @@
         //$('#'+options.requestResultContent).html("");
         show('buildQuery');
-        alert('Something is wrong on the server-side !');
+        alert('{/literal}{"gpc_something_is_wrong_on_the_server_side"|@translate}{literal}');
       }
     }
@@ -160,5 +169,5 @@
       else
       {
-        alert('Something is wrong on the server-side !');
+        alert('{/literal}{"gpc_something_is_wrong_on_the_server_side"|@translate}{literal}');
       }
     }
@@ -203,4 +212,37 @@
     }
 
+    /**
+     * fill the caddie with the search results
+     * @param String mode : 'add' or 'fill'
+     */
+    var fillCaddie = function (mode, requestNumber)
+    {
+      $('#iMenuCaddieImg').css('display', 'inline-block');
+      $('#iMenuCaddieItems ul').css('display', 'none');
+
+      $.ajax(
+        {
+          type: "POST",
+          url: "plugins/GrumPluginClasses/gpc_ajax.php",
+          async: true,
+          data: { ajaxfct:"admin.rbuilder.fillCaddie", fillMode:mode, requestNumber:requestNumber },
+          success:
+            function(msg)
+            {
+              $('#iMenuCaddieImg').css('display', 'none');
+              $('#iMenuCaddieItems ul').css('display', 'block');
+              alert('{/literal}{"gpc_the_caddie_is_updated"|@translate}{literal}');
+            },
+          error:
+            function(msg)
+            {
+              $('#iMenuCaddieImg').css('display', 'none');
+              $('#iMenuCaddieItems ul').css('display', 'block');
+              alert('{/literal}{"gpc_something_is_wrong_on_the_server_side"|@translate}{literal}');
+            },
+        }
+      );
+    }
+
     init(optionsToSet);
   }
@@ -252,4 +294,5 @@
   {$dialogBox.content}
 {/foreach}
+<div id='iRBCaddieNfo'></div>
 
 <form>
@@ -264,5 +307,5 @@
 
       <div id='iMenuCriterions' >
-        <div id='iMenuCTitle' class='gcLink gcBgInput cbButtons'>{'gpc_rb_add_criterions'|@translate}</div>
+        <div id='iMenuCTitle' class='gcLink gcBgInput cbButtons'>{'gpc_rb_add_criterions'|@translate}&nbsp;&dArr;</div>
         <div id='iMenuCItems'>
           <ul class='gcBgInput'>
@@ -292,5 +335,20 @@
     <div class='gcBgInput gcTextInput'>
       <div id='iPagesNavigator' style='float:right;'></div>
-      <div style='text-align:left;padding:4px;'>{'gpc_rb_number_of_item_found'|@translate}&nbsp;:&nbsp;<span id='iResultQueryNfo'></span></div>
+      <div style='text-align:left;padding:4px;'>
+        {'gpc_rb_number_of_item_found'|@translate}&nbsp;:&nbsp;<span id='iResultQueryNfo'></span>
+        <div id='iMenuCaddie' style='display:inline-block;'>
+          <div id='iMenuCaddieBar'>
+            <div id='iMenuCaddieText' class='gcLink gcBgInput'>{'gpc_manage_caddie'|@translate}&dArr;
+            <div id='iMenuCaddieImg' style='display:none;width:16px;height:16px;background:url(./plugins/GrumPluginClasses/icons/processing.gif) no-repeat 0 0 transparent;'>&nbsp;</div>
+            <div id='iMenuCaddieItems'>
+              <ul class='gcBgInput'>
+                <li class='gcBgInput'><a onclick="im.doAction('fillCaddie', 'add');">{'gpc_add_caddie'|@translate}</a></li>
+                <li class='gcBgInput'><a onclick="im.doAction('fillCaddie', 'replace');">{'gpc_replace_caddie'|@translate}</a></li>
+              </ul>
+            </div>
+          </div>
+
+        </span>
+      </div>
     </div>
 
