Moving to GitHub.
[platal.git] / classes / userfilter.php
index 2312723..dffa8a7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /***************************************************************************
- *  Copyright (C) 2003-2010 Polytechnique.org                              *
+ *  Copyright (C) 2003-2014 Polytechnique.org                              *
  *  http://opensource.polytechnique.org/                                   *
  *                                                                         *
  *  This program is free software; you can redistribute it and/or modify   *
@@ -85,7 +85,9 @@ class UserFilter extends PlFilter
     private $orderby = null;
 
     // Store the current 'search' visibility.
-    private $profile_visibility = null;
+    private $visibility = null;
+    // If the 'search' visibility should be based on a DB field instead.
+    private $visibility_field = null;
 
     private $lastusercount = null;
     private $lastprofilecount = null;
@@ -117,27 +119,50 @@ class UserFilter extends PlFilter
         }
 
         // This will set the visibility to the default correct level.
-        $this->profile_visibility = new ProfileVisibility();
+        $this->visibility = Visibility::defaultForRead();
     }
 
-    public function getVisibilityLevels()
-    {
-        return $this->profile_visibility->levels();
-    }
-
-    public function getVisibilityLevel()
-    {
-        return $this->profile_visibility->level();
-    }
-
-    public function restrictVisibilityTo($level)
-    {
-        $this->profile_visibility->setLevel($level);
+    /** Get the SQL condition to filter by visibility level for a field.
+     * This will return a SQL condition which evaluates to True if the given
+     * field display level is available from the current access level.
+     * @param $field Name of the field holding a display level
+     * @return string SQL condition, properly escaped, for that field.
+     */
+    public function getVisibilityConditionForField($field)
+    {
+        if ($this->visibility_field != null) {
+            // Use enum 'bit' arithmetic.
+            // Display levels are ordered as 'hidden, private, ax, public'
+            // Thus ax > private.
+            // The $sub.display_level cell will contain the 'most private' display
+            // level available based on $field. If it is 'ax' and $field is
+            // 'private','ax' <= 'private' is false.
+            $sub = $this->addVisibilityFieldFilter($this->visibility_field);
+            return $sub . '.best_display_level + 0 <= 0 + ' . $field;
+        } else {
+            $sub = $this->addVisibilityAbsoluteFilter($this->visibility->level());
+            return $sub . '.best_display_level + 0 <= 0 + ' . $field;
+        }
     }
 
-    public function getVisibilityCondition($field)
+    /** Get the SQL condition to filter by a given visibility level.
+     * @param $level One of Visibility::EXPORT_*
+     * @return string A SQL condition, properly escaped, which evaluates to 'true' if the $level can be viewed with the current access level.
+     */
+    public function getVisibilityConditionAbsolute($level)
     {
-        return $field . ' IN ' . XDB::formatArray($this->getVisibilityLevels());
+        if ($this->visibility_field != null) {
+            // The $sub.display_levels cell will contain allowed display levels
+            // for an access level of $this->visibility_field.
+            $sub = $this->addVisibilityFieldFilter($this->visibility_field);
+            return XDB::format('FIND_IN_SET({?}, ' . $sub . '.display_levels)', $level);
+        } else {
+            if ($this->visibility->isVisible($level)) {
+                return 'TRUE';
+            } else {
+                return 'FALSE';
+            }
+        }
     }
 
     private function buildQuery()
@@ -214,7 +239,7 @@ class UserFilter extends PlFilter
         return $groups;
     }
 
-    private function getUIDList($uids = null, PlLimit &$limit)
+    private function getUIDList($uids = null, PlLimit $limit)
     {
         $this->requireAccounts();
         $this->buildQuery();
@@ -232,7 +257,7 @@ class UserFilter extends PlFilter
         return $fetched;
     }
 
-    private function getPIDList($pids = null, PlLimit &$limit)
+    private function getPIDList($pids = null, PlLimit $limit)
     {
         $this->requireProfiles();
         $this->buildQuery();
@@ -260,7 +285,7 @@ class UserFilter extends PlFilter
 
     /** Check that the user match the given rule.
      */
-    public function checkUser(PlUser &$user)
+    public function checkUser(PlUser $user)
     {
         $this->requireAccounts();
         $this->buildQuery();
@@ -272,7 +297,7 @@ class UserFilter extends PlFilter
 
     /** Check that the profile match the given rule.
      */
-    public function checkProfile(Profile &$profile)
+    public function checkProfile(Profile $profile)
     {
         $this->requireProfiles();
         $this->buildQuery();
@@ -470,16 +495,62 @@ class UserFilter extends PlFilter
 
     public function export()
     {
-        $export = array('condition' => $this->root->export());
+        $export = array('conditions' => $this->root->export());
         if (!empty($this->sort)) {
-            $export['sort'] = array();
+            $export['sorts'] = array();
             foreach ($this->sort as $sort) {
-                $export['sort'][] = $sort->export();
+                $export['sorts'][] = $sort->export();
             }
         }
         return $export;
     }
 
+    public function exportConditions()
+    {
+        return $this->root->export();
+    }
+
+    public static function fromExport(array $export)
+    {
+        $export = new PlDict($export);
+        if (!$export->has('conditions')) {
+            throw new Exception("Cannot build a user filter without conditions");
+        }
+        $cond = UserFilterCondition::fromExport($export->v('conditions'));
+        $sorts = null;
+        if ($export->has('sorts')) {
+            $sorts = array();
+            foreach ($export->v('sorts') as $sort) {
+                $sorts[] = UserFilterOrder::fromExport($sort);
+            }
+        }
+        return new UserFilter($cond, $sorts);
+    }
+
+    public static function fromJSon($json)
+    {
+        $export = json_decode($json, true);
+        if (is_null($export)) {
+            throw new Exception("Invalid json: $json");
+        }
+        return self::fromExport($json);
+    }
+
+    public static function fromExportedConditions(array $export)
+    {
+        $cond = UserFilterCondition::fromExport($export);
+        return new UserFilter($cond);
+    }
+
+    public static function fromJSonConditions($json)
+    {
+        $export = json_decode($json, true);
+        if (is_null($export)) {
+            throw new Exception("Invalid json: $json");
+        }
+        return self::fromExportedConditions($json);
+    }
+
     static public function getLegacy($promo_min, $promo_max)
     {
         if ($promo_min != 0) {
@@ -497,12 +568,12 @@ class UserFilter extends PlFilter
 
     static public function sortByName()
     {
-        return array(new UFO_Name(Profile::LASTNAME), new UFO_Name(Profile::FIRSTNAME));
+        return array(new UFO_Name());
     }
 
     static public function sortByPromo()
     {
-        return array(new UFO_Promo(), new UFO_Name(Profile::LASTNAME), new UFO_Name(Profile::FIRSTNAME));
+        return array(new UFO_Promo(), new UFO_Name());
     }
 
     static private function getDBSuffix($string)
@@ -630,44 +701,6 @@ class UserFilter extends PlFilter
         return $joins;
     }
 
-    /** NAMES
-     */
-
-    static public function assertName($name)
-    {
-        if (!DirEnum::getID(DirEnum::NAMETYPES, $name)) {
-            Platal::page()->kill('Invalid name type: ' . $name);
-        }
-    }
-
-    private $pn  = array();
-    public function addNameFilter($type, $variant = null)
-    {
-        $this->requireProfiles();
-        if (!is_null($variant)) {
-            $ft  = $type . '_' . $variant;
-        } else {
-            $ft = $type;
-        }
-        $sub = '_' . $ft;
-        self::assertName($ft);
-
-        if (!is_null($variant) && $variant == 'other') {
-            $sub .= $this->option++;
-        }
-        $this->pn[$sub] = DirEnum::getID(DirEnum::NAMETYPES, $ft);
-        return $sub;
-    }
-
-    protected function nameJoins()
-    {
-        $joins = array();
-        foreach ($this->pn as $sub => $type) {
-            $joins['pn' . $sub] = PlSqlJoin::left('profile_name', '$ME.pid = $PID AND $ME.typeid = {?}', $type);
-        }
-        return $joins;
-    }
-
     /** NAMETOKENS
      */
     private $name_tokens = array();
@@ -718,9 +751,9 @@ class UserFilter extends PlFilter
 
     /** EDUCATION
      */
-    const GRADE_ING = 'Ing.';
-    const GRADE_PHD = 'PhD';
-    const GRADE_MST = 'M%';
+    const GRADE_ING = Profile::DEGREE_X;
+    const GRADE_PHD = Profile::DEGREE_D;
+    const GRADE_MST = Profile::DEGREE_M;
     static public function isGrade($grade)
     {
         return ($grade !== 0) && ($grade == self::GRADE_ING || $grade == self::GRADE_PHD || $grade == self::GRADE_MST);
@@ -767,7 +800,7 @@ class UserFilter extends PlFilter
         foreach ($this->pepe as $grade => $sub) {
             if ($this->isGrade($grade)) {
                 $joins['pe' . $sub] = PlSqlJoin::left('profile_education', '$ME.eduid = pee.id AND $ME.pid = $PID');
-                $joins['pede' . $sub] = PlSqlJoin::inner('profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid AND $ME.abbreviation LIKE {?}', $grade);
+                $joins['pede' . $sub] = PlSqlJoin::inner('profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid AND $ME.degree LIKE {?}', $grade);
             } else {
                 $joins['pe' . $sub] = PlSqlJoin::left('profile_education', '$ME.pid = $PID');
                 $joins['pee' . $sub] = PlSqlJoin::inner('profile_education_enum', '$ME.id = pe' . $sub . '.eduid');
@@ -800,6 +833,15 @@ class UserFilter extends PlFilter
         return $sub;
     }
 
+    private $gpfm = array();
+    public function addGroupFormerMemberFilter()
+    {
+        $this->requireAccounts();
+        $sub = '_' . $this->option++;
+        $this->gpfm[] = $sub;
+        return $sub;
+    }
+
     protected function groupJoins()
     {
         $joins = array();
@@ -814,6 +856,29 @@ class UserFilter extends PlFilter
                 $joins['gpm' . $sub] = PlSqlJoin::left('group_members', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
             }
         }
+        foreach ($this->gpfm as $sub) {
+            $joins['gpfm' . $sub] = PlSqlJoin::left('group_former_members', '$ME.uid = $UID');
+        }
+        return $joins;
+    }
+
+    /** NLS
+     */
+    private $nls = array();
+    public function addNewsLetterFilter($nlid)
+    {
+        $this->requireAccounts();
+        $sub = 'nl_' . $nlid;
+        $this->nls[$nlid] = $sub;
+        return $sub;
+    }
+
+    protected function newsLetterJoins()
+    {
+        $joins = array();
+        foreach ($this->nls as $key => $sub) {
+            $joins[$sub] = PlSqlJoin::left('newsletter_ins', '$ME.nlid = {?} AND $ME.uid = $UID', $key);
+        }
         return $joins;
     }
 
@@ -848,74 +913,78 @@ class UserFilter extends PlFilter
 
     /** EMAILS
      */
-    private $e = array();
+    private $ra = array();
+    /** Allows filtering by redirection.
+     * @param $email If null, enable a left join on the email redirection table
+     *  (email_redirect_account); otherwise, perform a left join on users having
+     *  that email as a redirection.
+     * @return Suffix to use to access the adequate table.
+     */
     public function addEmailRedirectFilter($email = null)
     {
         $this->requireAccounts();
-        return $this->register_optional($this->e, $email);
-    }
-
-    private $ve = array();
-    public function addVirtualEmailFilter($email = null)
+        return $this->register_optional($this->ra, $email);
+    }
+
+    const ALIAS_BEST      = 'bestalias';
+    const ALIAS_FORLIFE   = 'forlife';
+    const ALIAS_AUXILIARY = 'alias_aux';
+    private $sa = array();
+    /** Allows filtering by source email.
+     * @param $email If null, enable a left join on the email source table
+     *  (email_source_account); otherwise, perform a left join on users having
+     *  that email as a source email.
+     * @return Suffix to use to access the adequate table.
+     */
+    public function addAliasFilter($email = null)
     {
-        $this->addAliasFilter(self::ALIAS_FORLIFE);
-        return $this->register_optional($this->ve, $email);
+        $this->requireAccounts();
+        return $this->register_optional($this->sa, $email);
     }
 
-    const ALIAS_BEST    = 'bestalias';
-    const ALIAS_FORLIFE = 'forlife';
-    private $al = array();
-    public function addAliasFilter($alias = null)
+    private $with_rf = false;
+    /** Allows filtering by active redirection.
+     * @return Suffix to use to access the adequate table.
+     */
+    public function addActiveEmailRedirectFilter($email = null)
     {
         $this->requireAccounts();
-        return $this->register_optional($this->al, $alias);
+        $this->with_rf = true;
     }
 
     protected function emailJoins()
     {
         global $globals;
         $joins = array();
-        foreach ($this->e as $sub=>$key) {
-            if (is_null($key)) {
-                $joins['e' . $sub] = PlSqlJoin::left('emails', '$ME.uid = $UID AND $ME.flags != \'filter\'');
+        foreach ($this->ra as $sub => $redirections) {
+            if (is_null($redirections)) {
+                $joins['ra' . $sub] = PlSqlJoin::left('email_redirect_account', '$ME.uid = $UID AND $ME.type != \'imap\'');
             } else {
-                if (!is_array($key)) {
-                    $key = array($key);
+                if (!is_array($redirections)) {
+                    $key = array($redirections);
                 }
-                $joins['e' . $sub] = PlSqlJoin::left('emails', '$ME.uid = $UID AND $ME.flags != \'filter\'
-                                                               AND $ME.email IN {?}', $key);
+                $joins['ra' . $sub] = PlSqlJoin::left('email_redirect_account', '$ME.uid = $UID AND $ME.type != \'imap\'
+                                                                                 AND $ME.redirect IN {?}', $redirections);
             }
         }
-        foreach ($this->al as $sub=>$key) {
-            if (is_null($key)) {
-                $joins['al' . $sub] = PlSqlJoin::left('aliases', '$ME.uid = $UID AND $ME.type IN (\'alias\', \'a_vie\')');
-            } else if ($key == self::ALIAS_BEST) {
-                $joins['al' . $sub] = PlSqlJoin::left('aliases', '$ME.uid = $UID AND $ME.type IN (\'alias\', \'a_vie\') AND  FIND_IN_SET(\'bestalias\', $ME.flags)');
-            } else if ($key == self::ALIAS_FORLIFE) {
-                $joins['al' . $sub] = PlSqlJoin::left('aliases', '$ME.uid = $UID AND $ME.type = \'a_vie\'');
+        foreach ($this->sa as $sub => $emails) {
+            if (is_null($emails)) {
+                $joins['sa' . $sub] = PlSqlJoin::left('email_source_account', '$ME.uid = $UID');
+            } else if ($sub == self::ALIAS_BEST) {
+                $joins['sa' . $sub] = PlSqlJoin::left('email_source_account', '$ME.uid = $UID AND FIND_IN_SET(\'bestalias\', $ME.flags)');
+            } else if ($sub == self::ALIAS_FORLIFE) {
+                $joins['sa' . $sub] = PlSqlJoin::left('email_source_account', '$ME.uid = $UID AND $ME.type = \'forlife\'');
+            } else if ($sub == self::ALIAS_AUXILIARY) {
+                $joins['sa' . $sub] = PlSqlJoin::left('email_source_account', '$ME.uid = $UID AND $ME.type = \'alias_aux\'');
             } else {
-                if (!is_array($key)) {
-                    $key = array($key);
+                if (!is_array($emails)) {
+                    $key = array($emails);
                 }
-                $joins['al' . $sub] = PlSqlJoin::left('aliases', '$ME.uid = $UID AND $ME.type IN (\'alias\', \'a_vie\')
-                                                                  AND $ME.alias IN {?}', $key);
+                $joins['sa' . $sub] = PlSqlJoin::left('email_source_account', '$ME.uid = $UID AND $ME.email IN {?}', $emails);
             }
         }
-        foreach ($this->ve as $sub=>$key) {
-            if (is_null($key)) {
-                $joins['v' . $sub] = PlSqlJoin::left('virtual', '$ME.type = \'user\'');
-            } else {
-                if (!is_array($key)) {
-                    $key = array($key);
-                }
-                $joins['v' . $sub] = PlSqlJoin::left('virtual', '$ME.type = \'user\' AND $ME.alias IN {?}', $key);
-            }
-            $joins['vr' . $sub] = PlSqlJoin::left('virtual_redirect',
-                                                  '$ME.vid = v' . $sub . '.vid
-                                                   AND ($ME.redirect IN (CONCAT(al_forlife.alias, \'@\', {?}),
-                                                                         CONCAT(al_forlife.alias, \'@\', {?}),
-                                                                         a.email))',
-                                                  $globals->mail->domain, $globals->mail->domain2);
+        if ($this->with_rf) {
+            $joins['rf'] = PlSqlJoin::left('email_redirect_account', '$ME.uid = $UID AND $ME.type != \'imap\' AND $ME.flags = \'active\'');;
         }
         return $joins;
     }
@@ -923,44 +992,28 @@ class UserFilter extends PlFilter
 
     /** ADDRESSES
      */
-    private $with_pa = false;
-    public function addAddressFilter()
+    private $types = array();
+    public function addAddressFilter($type)
     {
         $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 = false;
-    public function addAddressLocalityFilter()
-    {
-        $this->requireProfiles();
-        $this->addAddressFilter();
-        $this->with_pal = true;
-        return 'gl';
+        $sub = '_' . $this->option++;
+        $this->types[$type] = $sub;
+        return $sub;
     }
 
     protected function addressJoins()
     {
         $joins = array();
-        if ($this->with_pa) {
-            $joins['pa'] = PlSqlJoin::left('profile_addresses', '$ME.pid = $PID');
-        }
-        if ($this->with_pac) {
-            $joins['gc'] = PlSqlJoin::left('geoloc_countries', '$ME.iso_3166_1_a2 = pa.countryID');
-        }
-        if ($this->with_pal) {
-            $joins['gl'] = PlSqlJoin::left('geoloc_localities', '$ME.id = pa.localityID');
+        foreach ($this->types as $type => $sub) {
+            $joins['pa' . $sub] = PlSqlJoin::inner('profile_addresses', '$ME.pid = $PID');
+            $joins['pac' . $sub] = PlSqlJoin::inner('profile_addresses_components',
+                                                    '$ME.pid = pa' . $sub . '.pid AND $ME.jobid = pa' . $sub . '.jobid AND $ME.groupid = pa' . $sub . '.groupid AND $ME.type = pa' . $sub . '.type AND $ME.id = pa' . $sub . '.id');
+            $joins['pace' . $sub] = PlSqlJoin::inner('profile_addresses_components_enum',
+                                                     '$ME.id = pac' . $sub . '.component_id AND FIND_IN_SET({?}, $ME.types)', $type);
         }
+
         return $joins;
     }
 
@@ -976,10 +1029,10 @@ class UserFilter extends PlFilter
         $this->requireProfiles();
         $this->pc = true;
         if ($type == UFC_Corps::CURRENT) {
-            $pce['pcec'] = 'current_corpsid';
+            $this->pce['pcec'] = 'current_corpsid';
             return 'pcec';
         } else if ($type == UFC_Corps::ORIGIN) {
-            $pce['pceo'] = 'original_corpsid';
+            $this->pce['pceo'] = 'original_corpsid';
             return 'pceo';
         }
     }
@@ -1131,6 +1184,37 @@ class UserFilter extends PlFilter
         return $joins;
     }
 
+    /** DELTATEN
+     */
+    private $dts = array();
+    const DELTATEN = 1;
+    const DELTATEN_MESSAGE = 2;
+    // TODO: terms
+
+    public function addDeltaTenFilter($type)
+    {
+        $this->requireProfiles();
+        switch ($type) {
+        case self::DELTATEN:
+            $this->dts['pdt'] = 'profile_deltaten';
+            return 'pdt';
+        case self::DELTATEN_MESSAGE:
+            $this->dts['pdtm'] = 'profile_deltaten';
+            return 'pdtm';
+        default:
+            Platal::page()->killError("Undefined DeltaTen filter.");
+        }
+    }
+
+    protected function deltatenJoins()
+    {
+        $joins = array();
+        foreach ($this->dts as $sub => $tab) {
+            $joins[$sub] = PlSqlJoin::left($tab, '$ME.pid = $PID');
+        }
+        return $joins;
+    }
+
     /** MENTORING
      */
 
@@ -1143,7 +1227,7 @@ class UserFilter extends PlFilter
 
     public function addMentorFilter($type)
     {
-        $this->requireAccounts();
+        $this->requireProfiles();
         switch($type) {
         case self::MENTOR:
             $this->pms['pm'] = 'profile_mentor';
@@ -1293,9 +1377,79 @@ class UserFilter extends PlFilter
             return array();
         }
     }
+
+
+    /** PARTNER SHARING
+     */
+
+    // Lists partner shortnames in use, as a $partner_shortname => true map.
+    private $ppss = array();
+
+    /** Add a filter on user having settings for a given partner.
+     * @param $partner_id the ID of the partner
+     * @return the name of the table to use in joins (e.g ppss_$partner_id).
+     */
+    public function addPartnerSharingFilter($partner_id)
+    {
+        $this->requireProfiles();
+        $sub = "ppss_" . $partner_id;
+        $this->ppss[$sub] = $partner_id;
+        return $sub;
+    }
+
+    protected function partnerSharingJoins()
+    {
+        $joins = array();
+        foreach ($this->ppss as $sub => $partner_id) {
+            $joins[$sub] = PlSqlJoin::left('profile_partnersharing_settings', '$ME.pid = $PID AND $ME.partner_id = {?} AND $ME.sharing_level != \'none\'', $partner_id);
+        }
+        return $joins;
+    }
+
+    public function restrictVisibilityForPartner($partner_id)
+    {
+        $sub = $this->addPartnerSharingFilter($partner_id);
+        $this->visibility_field = $sub . '.sharing_level';
+    }
+
+    /** VISIBILITY
+     */
+    private $vlevels = array();
+    private $vfields = array();
+    public function addVisibilityAbsoluteFilter($level)
+    {
+        $sub = 'pvel_' . $level;
+        $this->vlevels[$level] = $sub;
+        return $sub;
+    }
+
+    public function addVisibilityFieldFilter($field)
+    {
+        $sub = 'pvef_' . self::getDBSuffix($field);
+        $this->vfields[$field] = $sub;
+        return $sub;
+    }
+
+    /** Since this method might perform inner joins on tables which have been
+     * joined previously (e.g when using addVisibilityFieldFilter), it has to
+     * come after the Joins() methods for those tables.
+     * This is due to the implementation logic for discovering joins and the
+     * ordering used by PHP introspection.
+     */
+    protected function visibilityJoins()
+    {
+        $joins = array();
+        foreach ($this->vlevels as $level => $sub) {
+            $joins[$sub] = PlSqlJoin::inner('profile_visibility_enum', '$ME.access_level = {?}', $level);
+        }
+        foreach ($this->vfields as $field => $sub) {
+            $joins[$sub] = PlSqlJoin::inner('profile_visibility_enum', '$ME.access_level = ' . $field);
+        }
+        return $joins;
+    }
+
 }
 // }}}
-
 // {{{ class ProfileFilter
 class ProfileFilter extends UserFilter
 {
@@ -1326,5 +1480,5 @@ class ProfileFilter extends UserFilter
 }
 // }}}
 
-// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker fenc=utf-8:
 ?>