Improve UFC_Nationality, adds UFC_Binets, UFC_Formation
[platal.git] / classes / userfilter.php
index 586ab71..85f89b9 100644 (file)
  *     adequate joins. It must return the 'WHERE' condition to use
  *     with the filter.
  */
-interface UserFilterCondition
+interface UserFilterCondition extends PlFilterCondition
 {
-    /** Check that the given user matches the rule.
-     */
-    public function buildCondition(UserFilter &$uf);
 }
 // }}}
 
@@ -47,7 +44,7 @@ interface UserFilterCondition
  */
 class UFC_Profile implements UserFilterCondition
 {
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         return '$PID IS NOT NULL';
     }
@@ -77,7 +74,7 @@ class UFC_Promo implements UserFilterCondition
         }
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if ($this->grade == UserFilter::DISPLAY) {
             $sub = $uf->addDisplayFilter();
@@ -91,6 +88,30 @@ class UFC_Promo implements UserFilterCondition
 }
 // }}}
 
+// {{{ class UFC_Formation
+class UFC_Formation implements UserFilterCondition
+{
+    private $eduid;
+    private $edutext;
+
+    public function __construct($eduid = null, $edutext = null)
+    {
+        $this->eduid = $eduid;
+        $this->edutext = $edutext;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        $sub = $uf->addEducationFilter();
+        if ($this->eduid != null) {
+            return 'pe' . $sub . '.eduid = ' . XDB::format('{?}', $this->eduid);
+        } else {
+            return 'pee' . $sub . '.name LIKE ' . XDB::format('CONCAT( \'%\', {?}, \'%\')', $this->edutext);
+        }
+    }
+}
+// }}}
+
 // {{{ class UFC_Name
 /** Filters users based on name
  * @param $type Type of name field on which filtering is done (firstname, lastname...)
@@ -122,7 +143,7 @@ class UFC_Name implements UserFilterCondition
         return str_replace('$ME', 'pn' . $sub, $where);
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $left = '$ME.name';
         $op   = ' LIKE ';
@@ -151,6 +172,93 @@ class UFC_Name implements UserFilterCondition
 }
 // }}}
 
+// {{{ class UFC_NameTokens
+/** Selects users based on tokens in their name (for quicksearch)
+ * @param $tokens An array of tokens to search
+ * @param $flags Flags the tokens must have (e.g 'public' for public search)
+ * @param $soundex (bool) Whether those tokens are fulltext or soundex
+ */
+class UFC_NameTokens implements UserFilterCondition
+{
+    /* Flags */
+    const FLAG_PUBLIC = 'public';
+
+    private $tokens;
+    private $flags;
+    private $soundex;
+    private $exact;
+
+    public function __construct($tokens, $flags = array(), $soundex = false, $exact = false)
+    {
+        $this->tokens = $tokens;
+        if (is_array($flags)) {
+            $this->flags = $flags;
+        } else {
+            $this->flags = array($flags);
+        }
+        $this->soundex = $soundex;
+        $this->exact = $exact;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        $sub = $uf->addNameTokensFilter(!($this->exact || $this->soundex));
+        $conds = array();
+        if ($this->soundex) {
+            $conds[] = $sub . '.soundex IN ' . XDB::formatArray($this->tokens);
+        } else if ($this->exact) {
+            $conds[] = $sub . '.token IN ' . XDB::formatArray($this->tokens);
+        } else {
+            $tokconds = array();
+            foreach ($this->tokens as $token) {
+                $tokconds[] = $sub . '.token LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $token);
+            }
+            $conds[] = implode(' OR ', $tokconds);
+        }
+
+        if ($this->flags != null) {
+            $conds[] = $sub . '.flags IN ' . XDB::formatArray($this->flags);
+        }
+
+        return implode(' AND ', $conds);
+    }
+}
+// }}}
+
+// {{{ class UFC_Nationality
+class UFC_Nationality implements UserFilterCondition
+{
+    private $nid;
+    private $ntext;
+
+    public function __construct($nid = null, $ntext = null)
+    {
+        $this->nid = $nid;
+        $this->ntext = $ntext;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        if ($this->nid != null) {
+            $uf->requireProfiles();
+            $nat = XDB::format('{?}', $this->nid);
+            $conds = array(
+                'p.nationality1 = ' . $nat,
+                'p.nationality2 = ' . $nat,
+                'p.nationality3 = ' . $nat,
+            );
+            return implode(' OR ', $conds);
+        } else {
+            $sub = $uf->addNationalityJoins();
+            $nat = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->ntext);
+            $cond = $sub . '.nationality LIKE ' . $nat . ' OR ' .
+                    $sub . '.nationalityFR LIKE ' . $nat;
+            return $cond;
+        }
+    }
+}
+// }}}
+
 // {{{ class UFC_Dead
 /** Filters users based on death date
  * @param $comparison Comparison operator
@@ -167,7 +275,7 @@ class UFC_Dead implements UserFilterCondition
         $this->date = $date;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $str = 'p.deathdate IS NOT NULL';
         if (!is_null($this->comparison)) {
@@ -197,7 +305,7 @@ class UFC_Registered implements UserFilterCondition
         $this->date = $date;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if ($this->active) {
             $date = 'a.uid IS NOT NULL AND a.state = \'active\'';
@@ -228,7 +336,7 @@ class UFC_ProfileUpdated implements UserFilterCondition
         $this->date = $date;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         return 'p.last_change ' . $this->comparison . XDB::format(' {?}', date('Y-m-d H:i:s', $this->date));
     }
@@ -251,7 +359,7 @@ class UFC_Birthday implements UserFilterCondition
         $this->date = $date;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         return 'p.next_birthday ' . $this->comparison . XDB::format(' {?}', date('Y-m-d', $this->date));
     }
@@ -270,7 +378,7 @@ class UFC_Sex implements UserFilterCondition
         $this->sex = $sex;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if ($this->sex != User::GENDER_MALE && $this->sex != User::GENDER_FEMALE) {
             return self::COND_FALSE;
@@ -296,7 +404,7 @@ class UFC_Group implements UserFilterCondition
         $this->anim = $anim;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addGroupFilter($this->group);
         $where = 'gpm' . $sub . '.perms IS NOT NULL';
@@ -308,6 +416,49 @@ class UFC_Group implements UserFilterCondition
 }
 // }}}
 
+// {{{ class UFC_Binet
+class UFC_Binet implements UserFilterCondition
+{
+    private $binetid;
+    private $binetname;
+
+    public function __construct($binetid = null, $binetname = null)
+    {
+        $this->binetid = $binetid;
+        $this->binetname = $binetname;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        if ($this->binetid != null) {
+            $sub = $uf->addBinetsFilter();
+            return $sub . 'binet_id = ' . XDB::format('{?}', $this->binetid);
+        } else {
+            $sub = $uf->addBinetsFilter(true);
+            return $sub . 'test LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->binetname);
+        }
+    }
+}
+// }}}
+
+// {{{ class UFC_Section
+class UFC_Section implements UserFilterCondition
+{
+    private $section;
+
+    public function __construct($section)
+    {
+        $this->section = $section;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        $uf->requireProfiles();
+        return 'p.section = ' . XDB::format('{?}', $this->section);
+    }
+}
+// }}}
+
 // {{{ class UFC_Email
 /** Filters users based on email address
  * @param $email Email whose owner we are looking for
@@ -320,7 +471,7 @@ class UFC_Email implements UserFilterCondition
         $this->email = $email;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if (User::isForeignEmailAddress($this->email)) {
             $sub = $uf->addEmailRedirectFilter($this->email);
@@ -349,7 +500,7 @@ class UFC_EmailList implements UserFilterCondition
         $this->emails = $emails;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $email   = null;
         $virtual = null;
@@ -357,7 +508,7 @@ class UFC_EmailList implements UserFilterCondition
         $cond = array();
 
         if (count($this->emails) == 0) {
-            return UserFilterCondition::COND_TRUE;
+            return PlFilterCondition::COND_TRUE;
         }
 
         foreach ($this->emails as $entry) {
@@ -385,29 +536,22 @@ class UFC_EmailList implements UserFilterCondition
 // }}}
 
 // {{{ class UFC_Address
-/** Filters users based on their address
- * @param $text Text for filter in fulltext search
- * @param $textSearchMode Mode for search (PREFIX, SUFFIX, ...)
- * @param $type Filter on address type
- * @param $flags Filter on address flags
- * @param $countryId Filter on address countryId
- * @param $administrativeAreaId Filter on address administrativeAreaId
- * @param $subAdministrativeAreaId Filter on address subAdministrativeAreaId
- * @param $localityId Filter on address localityId
- * @param $postalCode Filter on address postalCode
- */
-class UFC_Address implements UserFilterCondition
+abstract class UFC_Address implements UserFilterCondition
 {
-    /** Flags for text search
+    /** Valid address type ('hq' is reserved for company addresses)
      */
-    const PREFIX    = 0x0001;
-    const SUFFIX    = 0x0002;
-    const CONTAINS  = 0x0003;
+    const TYPE_HOME = 1;
+    const TYPE_PRO  = 2;
+    const TYPE_ANY  = 3;
 
-    /** Valid address type ('hq' is reserved for company addresses)
+    /** Text for these types
      */
-    const TYPE_HOME = 'home';
-    const TYPE_PRO  = 'job';
+    protected static $typetexts = array(
+        self::TYPE_HOME => 'home',
+        self::TYPE_PRO  => 'pro',
+    );
+
+    protected $type;
 
     /** Flags for addresses
      */
@@ -422,7 +566,7 @@ class UFC_Address implements UserFilterCondition
 
     /** Text of these flags
      */
-    private static $flagtexts = array(
+    protected static $flagtexts = array(
         self::FLAG_CURRENT => 'current',
         self::FLAG_TEMP    => 'temporary',
         self::FLAG_SECOND  => 'secondary',
@@ -430,27 +574,136 @@ class UFC_Address implements UserFilterCondition
         self::FLAG_CEDEX   => 'cedex',
     );
 
-    /** Data of the filter
+    protected $flags;
+
+    public function __construct($type = null, $flags = null)
+    {
+        $this->type  = $type;
+        $this->flags = $flags;
+    }
+
+    protected function initConds($sub)
+    {
+        $conds = array();
+        $types = array();
+        foreach (self::$typetexts as $flag => $type) {
+            if ($flag & $this->type) {
+                $types[] = $type;
+            }
+        }
+        if (count($types)) {
+            $conds[] = $sub . '.type IN ' . XDB::formatArray($types);
+        }
+
+        if ($this->flags != self::FLAG_ANY) {
+            foreach(self::$flagtexts as $flag => $text) {
+                if ($flag & $this->flags) {
+                    $conds[] = 'FIND_IN_SET(' . XDB::format('{?}', $text) . ', ' . $sub . '.flags)';
+                }
+            }
+        }
+        return $conds;
+    }
+
+}
+// }}}
+
+// {{{ class UFC_AddressText
+/** Select users based on their address, using full text search
+ * @param $text Text for filter in fulltext search
+ * @param $textSearchMode Mode for search (PREFIX, SUFFIX, ...)
+ * @param $type Filter on address type
+ * @param $flags Filter on address flags
+ * @param $country Filter on address country
+ * @param $locality Filter on address locality
+ */
+class UFC_AddressText extends UFC_Address
+{
+    /** Flags for text search
      */
+    const PREFIX    = 0x0001;
+    const SUFFIX    = 0x0002;
+    const CONTAINS  = 0x0003;
+
     private $text;
-    private $type;
-    private $flags;
+    private $textSearchMode;
+
+    public function __construct($text = null, $textSearchMode = self::CONTAINS,
+        $type = null, $flags = self::FLAG_ANY, $country = null, $locality = null)
+    {
+        parent::__construct($type, $flags);
+        $this->text           = $text;
+        $this->textSearchMode = $textSearchMode;
+        $this->country        = $country;
+        $this->locality       = $locality;
+    }
+
+    private function mkMatch($txt)
+    {
+        $op = ' LIKE ';
+        if (($this->textSearchMode & self::CONTAINS) == 0) {
+            $right = XDB::format('{?}', $this->text);
+            $op = ' = ';
+        } else if (($this->mode & self::CONTAINS) == self::PREFIX) {
+            $right = XDB::format('CONCAT({?}, \'%\')', $this->text);
+        } else if (($this->mode & self::CONTAINS) == self::SUFFIX) {
+            $right = XDB::format('CONCAT(\'%\', {?})', $this->text);
+        } else {
+            $right = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->text);
+        }
+        return $op . $right;
+    }
+
+    public function buildCondition(PlFilter &$uf)
+    {
+        $sub = $uf->addAddressFilter();
+        $conds = $this->initConds($sub);
+        if ($this->text != null) {
+            $conds[] = $sub . '.text' . $this->mkMatch($this->text);
+        }
+
+        if ($this->country != null) {
+            $subc = $uf->addAddressCountryFilter();
+            $subconds = array();
+            $subconds[] = $subc . '.country' . $this->mkMatch($this->country);
+            $subconds[] = $subc . '.countryFR' . $this->mkMatch($this->country);
+            $conds[] = implode(' OR ', $subconds);
+        }
+
+        if ($this->locality != null) {
+            $subl = $uf->addAddressLocalityFilter();
+            $conds[] = $subl . '.name' . $this->mkMatch($this->locality);
+        }
+
+        return implode(' AND ', $conds);
+    }
+}
+// }}}
+
+// {{{ class UFC_AddressFields
+/** Filters users based on their address,
+ * @param $type Filter on address type
+ * @param $flags Filter on address flags
+ * @param $countryId Filter on address countryId
+ * @param $administrativeAreaId Filter on address administrativeAreaId
+ * @param $subAdministrativeAreaId Filter on address subAdministrativeAreaId
+ * @param $localityId Filter on address localityId
+ * @param $postalCode Filter on address postalCode
+ */
+class UFC_AddressFields extends UFC_Address
+{
+    /** Data of the filter
+     */
     private $countryId;
     private $administrativeAreaId;
     private $subAdministrativeAreaId;
     private $localityId;
     private $postalCode;
 
-    private $textSearchMode;
-
-    public function __construct($text = null, $textSearchMode = self::CONTAINS,
-        $type = null, $flags = self::FLAG_ANY, $countryId = null, $administrativeAreaId = null,
+    public function __construct($type = null, $flags = self::FLAG_ANY, $countryId = null, $administrativeAreaId = null,
         $subAdministrativeAreaId = null, $localityId = null, $postalCode = null)
     {
-        $this->text                      = $text;
-        $this->textSearchMode            = $textSearchMode;
-        $this->type                      = $type;
-        $this->flags                     = $flags;
+        parent::__construct($type, $flags);
         $this->countryId                 = $countryId;
         $this->administrativeAreaId      = $administrativeAreaId;
         $this->subAdministrativeAreaId   = $subAdministrativeAreaId;
@@ -458,37 +711,10 @@ class UFC_Address implements UserFilterCondition
         $this->postalCode                = $postalCode;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addAddressFilter();
-        $conds = array();
-        if ($this->text != null) {
-            $left = $sub . '.text ';
-            $op = ' LIKE ';
-            if (($this->textSearchMode & self::CONTAINS) == 0) {
-                $right = XDB::format('{?}', $this->text);
-                $op = ' = ';
-            } else if (($this->mode & self::CONTAINS) == self::PREFIX) {
-                $right = XDB::format('CONCAT({?}, \'%\')', $this->text);
-            } else if (($this->mode & self::CONTAINS) == self::SUFFIX) {
-                $right = XDB::format('CONCAT(\'%\', {?})', $this->text);
-            } else {
-                $right = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->text);
-            }
-            $conds[] = $left . $op . $right;
-        }
-
-        if ($this->type != null) {
-            $conds[] = $sub . '.type = ' . XDB::format('{?}', $this->type);
-        }
-
-        if ($this->flags != self::FLAG_ANY) {
-            foreach(self::$flagtexts as $flag => $text) {
-                if ($flag & $this->flags) {
-                    $conds[] = 'FIND_IN_SET(' . XDB::format('{?}', $text) . ', ' . $sub . '.flags)';
-                }
-            }
-        }
+        $conds = $this->initConds();
 
         if ($this->countryId != null) {
             $conds[] = $sub . '.countryId = ' . XDB::format('{?}', $this->countryId);
@@ -530,7 +756,7 @@ class UFC_Corps implements UserFilterCondition
         $this->type  = $type;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         /** Tables shortcuts:
          * pc for profile_corps,
@@ -556,7 +782,7 @@ class UFC_Corps_Rank implements UserFilterCondition
         $this->rank = $rank;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         /** Tables shortcuts:
          * pcr for profile_corps_rank
@@ -596,7 +822,7 @@ class UFC_Job_Company implements UserFilterCondition
         }
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addJobCompanyFilter();
         $cond  = $sub . '.' . $this->type . ' = ' . XDB::format('{?}', $this->value);
@@ -625,7 +851,7 @@ class UFC_Job_Sectorization implements UserFilterCondition
         $this->subsubsector = $subsubsector;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         // No need to add the JobFilter, it will be done by addJobSectorizationFilter
         $conds = array();
@@ -657,25 +883,29 @@ class UFC_Job_Description implements UserFilterCondition
     /** Meta-filters
      * Built with binary OR on UserFilter::JOB_*
      */
-    const ANY = 31;
+    const ANY = 63;
     const SECTORIZATION = 15;
 
     private $description;
     private $fields;
 
-    public function __construct($description)
+    public function __construct($description, $fields)
     {
         $this->fields = $fields;
         $this->description = $description;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $conds = array();
         if ($this->fields & UserFilter::JOB_USERDEFINED) {
             $sub = $uf->addJobFilter();
             $conds[] = $sub . '.description LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
         }
+        if ($this->fields & UserFilter::JOB_CV) {
+            $uf->requireProfiles();
+            $conds[] = 'p.cv LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
+        }
         if ($this->fields & UserFilter::JOB_SECTOR) {
             $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SECTOR);
             $conds[] = $sub . '.name LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
@@ -711,7 +941,7 @@ class UFC_Networking implements UserFilterCondition
         $this->value = $value;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addNetworkingFilter();
         $conds = array();
@@ -754,7 +984,7 @@ class UFC_Phone implements UserFilterCondition
         $this->phone_type = format_phone_number($phone_type);
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addPhoneFilter();
         $conds = array();
@@ -786,7 +1016,7 @@ class UFC_Medal implements UserFilterCondition
         $this->grade = $grade;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $conds = array();
         $sub = $uf->addMedalFilter();
@@ -812,7 +1042,7 @@ class UFC_Mentor_Expertise implements UserFilterCondition
         $this->expertise = $expertise;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addMentorFilter(UserFilter::MENTOR_EXPERTISE);
         return $sub . '.expertise LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\'', $this->expertise);
@@ -833,7 +1063,7 @@ class UFC_Mentor_Country implements UserFilterCondition
         $this->country = $country;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addMentorFilter(UserFilter::MENTOR_COUNTRY);
         return $sub . '.country = ' . XDB::format('{?}', $this->country);
@@ -857,7 +1087,7 @@ class UFC_Mentor_Sectorization implements UserFilterCondition
         $this->subsubsector = $subsector;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addMentorFilter(UserFilter::MENTOR_SECTOR);
         $conds = array();
@@ -889,7 +1119,7 @@ abstract class UFC_UserRelated implements UserFilterCondition
  */
 class UFC_Contact extends UFC_UserRelated
 {
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $sub = $uf->addContactFilter($this->user->id());
         return 'c' . $sub . '.contact IS NOT NULL';
@@ -902,14 +1132,14 @@ class UFC_Contact extends UFC_UserRelated
  */
 class UFC_WatchRegistration extends UFC_UserRelated
 {
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if (!$this->user->watch('registration')) {
-            return UserFilterCondition::COND_FALSE;
+            return PlFilterCondition::COND_FALSE;
         }
         $uids = $this->user->watchUsers();
         if (count($uids) == 0) {
-            return UserFilterCondition::COND_FALSE;
+            return PlFilterCondition::COND_FALSE;
         } else {
             return '$UID IN ' . XDB::formatArray($uids);
         }
@@ -931,11 +1161,11 @@ class UFC_WatchPromo extends UFC_UserRelated
         $this->grade = $grade;
     }
 
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         $promos = $this->user->watchPromos();
         if (count($promos) == 0) {
-            return UserFilterCondition::COND_FALSE;
+            return PlFilterCondition::COND_FALSE;
         } else {
             $sube = $uf->addEducationFilter(true, $this->grade);
             $field = 'pe' . $sube . '.' . UserFilter::promoYear($this->grade);
@@ -950,10 +1180,10 @@ class UFC_WatchPromo extends UFC_UserRelated
  */
 class UFC_WatchContact extends UFC_Contact
 {
-    public function buildCondition(UserFilter &$uf)
+    public function buildCondition(PlFilter &$uf)
     {
         if (!$this->user->watchContacts()) {
-            return UserFilterCondition::COND_FALSE;
+            return PlFilterCondition::COND_FALSE;
         }
         return parent::buildCondition($uf);
     }
@@ -1047,6 +1277,17 @@ class UFO_Name extends UserFilterOrder
 }
 // }}}
 
+// {{{ class UFO_Score
+class UFO_Score extends UserFilterOrder
+{
+    protected function getSortTokens(UserFilter &$uf)
+    {
+        $sub = $uf->addNameTokensFilter();
+        return 'SUM(' . $sub . '.score)';
+    }
+}
+// }}}
+
 // {{{ class UFO_Registration
 /** Sorts users based on registration date
  */
@@ -1200,10 +1441,17 @@ class UserFilter extends PlFilter
         }
         if (is_null($this->query)) {
             $where = $this->root->buildCondition($this);
+            if ($this->with_forced_sn) {
+                $this->requireProfiles();
+                $from = 'search_name AS sn';
+            } else if ($this->with_accounts) {
+                $from = 'accounts AS a';
+            } else {
+                $this->requireProfiles();
+                $from = 'profiles AS p';
+            }
             $joins = $this->buildJoins();
-            $this->query = 'FROM  accounts AS a
-                       LEFT JOIN  account_profiles AS ap ON (ap.uid = a.uid AND FIND_IN_SET(\'owner\', ap.perms))
-                       LEFT JOIN  profiles AS p ON (p.pid = ap.pid)
+            $this->query = 'FROM  ' . $from . '
                                ' . $joins . '
                            WHERE  (' . $where . ')';
         }
@@ -1211,6 +1459,7 @@ class UserFilter extends PlFilter
 
     private function getUIDList($uids = null, PlLimit &$limit)
     {
+        $this->requireAccounts();
         $this->buildQuery();
         $lim = $limit->getSql();
         $cond = '';
@@ -1226,20 +1475,58 @@ class UserFilter extends PlFilter
         return $fetched;
     }
 
+    private function getPIDList($pids = null, PlLimit &$limit)
+    {
+        $this->requireProfiles();
+        $this->buildQuery();
+        $lim = $limit->getSql();
+        $cond = '';
+        if (!is_null($pids)) {
+            $cond = ' AND p.pid IN ' . XDB::formatArray($pids);
+        }
+        $fetched = XDB::fetchColumn('SELECT  SQL_CALC_FOUND_ROWS  p.pid
+                                    ' . $this->query . $cond . '
+                                   GROUP BY  p.pid
+                                    ' . $this->orderby . '
+                                    ' . $lim);
+        $this->lastcount = (int)XDB::fetchOneCell('SELECT FOUND_ROWS()');
+        return $fetched;
+    }
+
     /** Check that the user match the given rule.
      */
     public function checkUser(PlUser &$user)
     {
+        $this->requireAccounts();
         $this->buildQuery();
         $count = (int)XDB::fetchOneCell('SELECT  COUNT(*)
                                         ' . $this->query . XDB::format(' AND a.uid = {?}', $user->id()));
         return $count == 1;
     }
 
-    /** Filter a list of user to extract the users matching the rule.
+    /** Check that the profile match the given rule.
+     */
+    public function checkProfile(Profile &$profile)
+    {
+        $this->requireProfiles();
+        $this->buildQuery();
+        $count = (int)XDB::fetchOneCell('SELECT  COUNT(*)
+                                        ' . $this->query . XDB::format(' AND p.pid = {?}', $profile->id()));
+        return $count == 1;
+    }
+
+    /** Default filter is on users
      */
     public function filter(array $users, PlLimit &$limit)
     {
+        return $this->filterUsers($users, $limit);
+    }
+
+    /** Filter a list of users to extract the users matching the rule.
+     */
+    public function filterUsers(array $users, PlLimit &$limit)
+    {
+        $this->requireAccounts();
         $this->buildQuery();
         $table = array();
         $uids  = array();
@@ -1260,16 +1547,51 @@ class UserFilter extends PlFilter
         return $output;
     }
 
+    /** Filter a list of profiles to extract the users matching the rule.
+     */
+    public function filterProfiles(array $profiles, PlLimit &$limit)
+    {
+        $this->requireProfiles();
+        $this->buildQuery();
+        $table = array();
+        $pids  = array();
+        foreach ($profiles as $profile) {
+            if ($profile instanceof Profile) {
+                $pid = $profile->id();
+            } else {
+                $pid = $profile;
+            }
+            $pids[] = $pid;
+            $table[$pid] = $profile;
+        }
+        $fetched = $this->getPIDList($pids, $limit);
+        $output = array();
+        foreach ($fetched as $pid) {
+            $output[] = $table[$pid];
+        }
+        return $output;
+    }
+
     public function getUIDs(PlLimit &$limit)
     {
         return $this->getUIDList(null, $limit);
     }
 
+    public function getPIDs(PlLimit &$limit)
+    {
+        return $this->getPIDList(null, $limit);
+    }
+
     public function getUsers(PlLimit &$limit)
     {
         return User::getBulkUsersWithUIDs($this->getUIDs($limit));
     }
 
+    public function getProfiles(PlLimit &$limit)
+    {
+        return Profile::getBulkProfilesWithPIDs($this->getPIDs($limit));
+    }
+
     public function get(PlLimit &$limit)
     {
         return $this->getUsers($limit);
@@ -1279,7 +1601,12 @@ class UserFilter extends PlFilter
     {
         if (is_null($this->lastcount)) {
             $this->buildQuery();
-            return (int)XDB::fetchOneCell('SELECT  COUNT(DISTINCT a.uid)
+            if ($this->with_accounts) {
+                $field = 'a.uid';
+            } else {
+                $field = 'p.pid';
+            }
+            return (int)XDB::fetchOneCell('SELECT  COUNT(DISTINCT ' . $field . ')
                                           ' . $this->query);
         } else {
             return $this->lastcount;
@@ -1349,12 +1676,51 @@ class UserFilter extends PlFilter
         return $sub;
     }
 
+    /** PROFILE VS ACCOUNT
+     */
+    private $with_profiles  = false;
+    private $with_accounts  = false;
+    private $with_forced_sn = false;
+    public function requireAccounts()
+    {
+        $this->with_accounts = true;
+    }
+
+    public function requireProfiles()
+    {
+        $this->with_profiles = true;
+    }
+
+    /** Forces the "FROM" to use search_name instead of accounts or profiles */
+    public function forceSearchName()
+    {
+        $this->with_forced_sn = true;
+    }
+
+    protected function accountJoins()
+    {
+        $joins = array();
+        /** Quick search is much more efficient with sn first and PID second */
+        if ($this->with_forced_sn) {
+            $joins['p'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profiles', '$PID = sn.uid');
+            if ($this->with_accounts) {
+                $joins['ap'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'account_profiles', '$ME.pid = $PID');
+                $joins['a'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'accounts', '$UID = ap.uid');
+            }
+        } else if ($this->with_profiles && $this->with_accounts) {
+            $joins['ap'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'account_profiles', '$ME.uid = $UID AND FIND_IN_SET(\'owner\', ap.perms)');
+            $joins['p'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profiles', '$PID = ap.pid');
+        }
+        return $joins;
+    }
+
     /** DISPLAY
      */
     const DISPLAY = 'display';
     private $pd = false;
     public function addDisplayFilter()
     {
+        $this->requireProfiles();
         $this->pd = true;
         return '';
     }
@@ -1414,6 +1780,7 @@ class UserFilter extends PlFilter
     private $pn  = array();
     public function addNameFilter($type, $variant = null)
     {
+        $this->requireProfiles();
         if (!is_null($variant)) {
             $ft  = $type . '_' . $variant;
         } else {
@@ -1438,6 +1805,48 @@ class UserFilter extends PlFilter
         return $joins;
     }
 
+    /** NAMETOKENS
+     */
+    private $with_sn = false;
+    // Set $doingQuickSearch to true if you wish to optimize the query
+    public function addNameTokensFilter($doingQuickSearch = false)
+    {
+        $this->requireProfiles();
+        $this->with_forced_sn = ($this->with_forced_sn || $doingQuickSearch);
+        $this->with_sn = true;
+        return 'sn';
+    }
+
+    protected function nameTokensJoins()
+    {
+        /* We don't return joins, since with_sn forces the SELECT to run on search_name first */
+        if ($this->with_sn && !$this->with_forced_sn) {
+            return array(
+                'sn' => new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'search_name', '$ME.uid = $PID')
+            );
+        } else {
+            return array();
+        }
+    }
+
+    /** NATIONALITY
+     */
+
+    private $with_nat = false;
+    public function addNationalityFilter()
+    {
+        $this->with_nat = true;
+        return 'ngc';
+    }
+
+    protected function nationalityJoins()
+    {
+        $joins = array();
+        if ($this->with_nat) {
+            $joins['ngc'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'geoloc_countries', '$ME.iso_3166_1_a2 = p.nationality1 OR $ME.iso_3166_1_a2 = p.nationality2 OR $ME.iso_3166_1_a2 = p.nationality3');
+        }
+        return $joins;
+    }
 
     /** EDUCATION
      */
@@ -1466,6 +1875,7 @@ class UserFilter extends PlFilter
     private $with_pee = false;
     public function addEducationFilter($x = false, $grade = null)
     {
+        $this->requireProfiles();
         if (!$x) {
             $index = $this->option;
             $sub   = $this->option++;
@@ -1506,6 +1916,7 @@ class UserFilter extends PlFilter
     private $gpm = array();
     public function addGroupFilter($group = null)
     {
+        $this->requireAccounts();
         if (!is_null($group)) {
             if (ctype_digit($group)) {
                 $index = $sub = $group;
@@ -1539,11 +1950,41 @@ class UserFilter extends PlFilter
         return $joins;
     }
 
+    /** BINETS
+     */
+
+    private $with_bi = false;
+    private $with_bd = false;
+    public function addBinetsFilter($with_enum = false;)
+    {
+        $this->requireProfiles();
+        $this->with_bi = true;
+        if ($with_enum) {
+            $this->with_bd = true;
+            return 'bd';
+        } else {
+            return 'bi';
+        }
+    }
+
+    protected function binetsJoins()
+    {
+        $joins = array();
+        if ($this->with_bi) {
+            $joins['bi'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'binets_ins', '$ME.uid = $PID');
+        }
+        if ($this->with_bd) {
+            $joins['bd'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'binets_def', '$ME.id = bi.binet_id');
+        }
+        return $joins;
+    }
+
     /** EMAILS
      */
     private $e = array();
     public function addEmailRedirectFilter($email = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->e, $email);
     }
 
@@ -1559,6 +2000,7 @@ class UserFilter extends PlFilter
     private $al = array();
     public function addAliasFilter($alias = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->al, $alias);
     }
 
@@ -1605,16 +2047,41 @@ class UserFilter extends PlFilter
     private $with_pa = false;
     public function addAddressFilter()
     {
+        $this->requireProfiles();
         $this->with_pa = true;
         return 'pa';
     }
 
+    private $with_pac = false;
+    public function addAddressCountryFilter()
+    {
+        $this->requireProfiles();
+        $this->addAddressFilter();
+        $this->with_pac = true;
+        return 'gc';
+    }
+
+    private $with_pal = true;
+    public function addAddressLocalityFilter()
+    {
+        $this->requireProfiles();
+        $this->addAddressFilter();
+        $this->with_pal = true;
+        return 'gl';
+    }
+
     protected function addressJoins()
     {
         $joins = array();
         if ($this->with_pa) {
             $joins['pa'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_address', '$ME.pid = $PID');
         }
+        if ($this->with_pac) {
+            $joins['gc'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'geoloc_countries', '$ME.iso_3166_1_a2 = pa.countryID');
+        }
+        if ($this->with_pal) {
+            $joins['gl'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'geoloc_localities', '$ME.id = pa.localityID');
+        }
         return $joins;
     }
 
@@ -1627,6 +2094,7 @@ class UserFilter extends PlFilter
     private $pcr = false;
     public function addCorpsFilter($type)
     {
+        $this->requireProfiles();
         $this->pc = true;
         if ($type == UFC_Corps::CURRENT) {
             $pce['pcec'] = 'current_corpsid';
@@ -1639,6 +2107,7 @@ class UserFilter extends PlFilter
 
     public function addCorpsRankFilter()
     {
+        $this->requireProfiles();
         $this->pc = true;
         $this->pcr = true;
         return 'pcr';
@@ -1667,6 +2136,7 @@ class UserFilter extends PlFilter
     const JOB_SUBSUBSECTOR = 4;
     const JOB_ALTERNATES = 8;
     const JOB_USERDEFINED = 16;
+    const JOB_CV = 32;
 
     /** Joins :
      * pj => profile_job
@@ -1685,6 +2155,7 @@ class UserFilter extends PlFilter
 
     public function addJobFilter()
     {
+        $this->requireProfiles();
         $this->with_pj = true;
         return 'pj';
     }
@@ -1744,6 +2215,7 @@ class UserFilter extends PlFilter
     private $with_pnw = false;
     public function addNetworkingFilter()
     {
+        $this->requireAccounts();
         $this->with_pnw = true;
         return 'pnw';
     }
@@ -1764,6 +2236,7 @@ class UserFilter extends PlFilter
 
     public function addPhoneFilter()
     {
+        $this->requireAccounts();
         $this->with_ptel = true;
         return 'ptel';
     }
@@ -1783,6 +2256,7 @@ class UserFilter extends PlFilter
     private $with_pmed = false;
     public function addMedalFilter()
     {
+        $this->requireProfiles();
         $this->with_pmed = true;
         return 'pmed';
     }
@@ -1806,6 +2280,7 @@ class UserFilter extends PlFilter
 
     public function addMentorFilter($type)
     {
+        $this->requireAccounts();
         switch($type) {
         case MENTOR_EXPERTISE:
             $pms['pme'] = 'profile_mentor';
@@ -1835,6 +2310,7 @@ class UserFilter extends PlFilter
     private $cts = array();
     public function addContactFilter($uid = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->cts, is_null($uid) ? null : 'user_' . $uid);
     }
 
@@ -1857,18 +2333,21 @@ class UserFilter extends PlFilter
     private $wn = array();
     public function addWatchRegistrationFilter($uid = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->wn, is_null($uid) ? null : 'user_' . $uid);
     }
 
     private $wp = array();
     public function addWatchPromoFilter($uid = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->wp, is_null($uid) ? null : 'user_' . $uid);
     }
 
     private $w = array();
     public function addWatchFilter($uid = null)
     {
+        $this->requireAccounts();
         return $this->register_optional($this->w, is_null($uid) ? null : 'user_' . $uid);
     }