Initial version of the PTA webservice.
authorRaphaël Barrois <raphael.barrois@polytechnique.org>
Fri, 3 Jun 2011 08:27:16 +0000 (10:27 +0200)
committerRaphaël Barrois <raphael.barrois@polytechnique.org>
Tue, 9 Aug 2011 22:40:46 +0000 (00:40 +0200)
Signed-off-by: Raphaël Barrois <raphael.barrois@polytechnique.org>
classes/profile.php
classes/userfilter.php
classes/userfilter/conditions.inc.php
include/partnersharing.inc.php [new file with mode: 0644]
include/profilefields.inc.php
modules/ptawebservice.php [new file with mode: 0644]
modules/ptawebservice/request.inc.php [new file with mode: 0644]
upgrade/1.1.3/03_pta.sql [new file with mode: 0644]

index 6b06231..e9ff7b5 100644 (file)
@@ -110,10 +110,11 @@ class Profile implements PlExportable
     const FETCH_JOB_TERMS      = 0x000200;
     const FETCH_MENTOR_TERMS   = 0x000400;
     const FETCH_DELTATEN       = 0x000800;
+    const FETCH_PARTNER        = 0x001000;
 
     const FETCH_MINIFICHES   = 0x00012D; // FETCH_ADDRESSES | FETCH_EDU | FETCH_JOBS | FETCH_NETWORKING | FETCH_PHONES
 
-    const FETCH_ALL          = 0x000FFF; // OR of FETCH_*
+    const FETCH_ALL          = 0x001FFF; // OR of FETCH_*
 
     static public $descriptions = array(
         'search_names'    => 'Noms',
@@ -458,7 +459,8 @@ class Profile implements PlExportable
      * Clears a profile.
      *  *always deletes in: profile_addresses, profile_binets, profile_deltaten,
      *      profile_job, profile_langskills, profile_mentor, profile_networking,
-     *      profile_phones, profile_skills, watch_profile
+     *      profile_partnersharing_settings, profile_phones, profile_skills,
+     *      watch_profile
      *  *always keeps in: profile_corps, profile_display, profile_education,
      *      profile_medals, profile_*_names, profile_photos, search_name
      *  *modifies: profiles
@@ -469,7 +471,7 @@ class Profile implements PlExportable
             'profile_job', 'profile_langskills', 'profile_mentor',
             'profile_networking', 'profile_skills', 'watch_profile',
             'profile_phones', 'profile_addresses', 'profile_binets',
-            'profile_deltaten');
+            'profile_deltaten', 'profile_partnersharing_settings');
 
         foreach ($tables as $t) {
             XDB::execute('DELETE FROM  ' . $t . '
@@ -913,6 +915,25 @@ class Profile implements PlExportable
         return $this->medals->medals;
     }
 
+    /** Sharing data with partner websites
+     */
+    private $partners_settings = null;
+    public function setPartnersSettings(ProfilePartnerSharing $partners_settings)
+    {
+        $this->partners_settings = $partners_settings;
+    }
+
+    public function getPartnerSettings($partner_id)
+    {
+        if ($this->partners_settings == null && !$this->fetched(self::FETCH_PARTNER)) {
+            $this->setPartnersSettings($this->getProfileField(self::FETCH_PARTNER));
+        }
+        if ($this->partners_settings == null) {
+            return PartnerSettings::getEmpty($partner_id);
+        }
+        return $this->partners_settings->get($partner_id);
+    }
+
     public function compareNames($firstname, $lastname)
     {
         $_lastname  = mb_strtoupper($this->lastName());
index 2314915..c46db70 100644 (file)
@@ -1407,6 +1407,40 @@ 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';
+    }
 }
 // }}}
 // {{{ class ProfileFilter
index 351ad2a..bf060ca 100644 (file)
@@ -1616,6 +1616,72 @@ class UFC_MarketingHash extends UserFilterCondition
     }
 }
 // }}}
+// {{{ class UFC_PartnerSharing
+/** Filters users, keeping only those sharing data with a given partner.
+ */
+class UFC_PartnerSharing extends UserFilterCondition
+{
+    const PTA = 'pta';
+
+    private $partner;
+
+    public function __construct($partner)
+    {
+        $this->partner = $partner;
+    }
+
+    public function buildCondition(PlFilter $uf)
+    {
+        $sub = $uf->addPartnerSharingFilter($this->partner);
+        return XDB::format("$sub.exposed_uid IS NOT NULL");
+    }
+}
+// }}}
+// {{{ class UFC_PartnerSharingEmail
+/** Filters users, keeping only those allowing emails to be sent by
+ * a given partner.
+ */
+class UFC_PartnerSharingEmail extends UserFilterCondition
+{
+    private $partner;
+
+    public function __construct($partner)
+    {
+        $this->partner = $partner;
+    }
+
+    public function buildCondition(PlFilter $uf)
+    {
+        $sub = $uf->addPartnerSharingFilter($this->partner);
+        return XDB::format("$sub.allow_email IN ('digest', 'direct')");
+    }
+}
+// }}}
+// {{{ class UFC_PartnerSharingID
+/** Filters users according to a list of partner-known IDs
+ */
+class UFC_PartnerSharingID extends UserFilterCondition
+{
+    private $partner;
+    private $ids;
+
+    public function __construct($partner)
+    {
+        $this->partner = $partner;
+        $ids = func_get_args();
+        array_shift($ids);
+        $this->ids   = pl_flatten($ids);
+    }
+
+    public function buildCondition(PlFilter $uf)
+    {
+        $uf->requireProfiles();
+        $ids = $this->ids;
+        $sub = $uf->addPartnerSharingFilter($this->partner);
+        return XDB::format("$sub.exposed_uid IN {?}", $ids);
+    }
+}
+// }}}
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
 ?>
diff --git a/include/partnersharing.inc.php b/include/partnersharing.inc.php
new file mode 100644 (file)
index 0000000..7a7b6c7
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2011 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+// {{{ class PartnerSharing
+// Holds data about a "directory partner".
+class PartnerSharing
+{
+    public $id;
+    public $shortname;
+    public $name;
+    public $url;
+    public $has_directory = false;
+    public $has_bulkmail = false;
+    public $default_sharing_level = Visibility::VIEW_NONE;
+    protected $api_uid = null;
+
+    protected function __construct(array $data)
+    {
+        foreach ($data as $key => $val) {
+            $this->$key = $val;
+        }
+    }
+
+    public function apiUser()
+    {
+        return User::getSilentWithUID($this->api_uid);
+    }
+
+    public static function fetchByAPIUser(User $user)
+    {
+        $res = XDB::fetchOneAssoc('SELECT  id, shortname, name, url,
+                                           has_directory, has_bulkmail,
+                                           default_sharing_level, api_uid
+                                     FROM  profile_partnersharing_enum
+                                    WHERE  api_uid = {?}', $user->uid);
+        if ($res == null) {
+            return null;
+        } else {
+            return new PartnerSharing($res);
+        }
+    }
+
+    public static function fetchById($id)
+    {
+        $res = XDB::fetchOneAssoc('SELECT  id, shortname, name, url,
+                                           has_directory, has_bulkmail,
+                                           default_sharing_level, api_uid
+                                     FROM  profile_partnersharing_enum
+                                    WHERE  id = {?}', $id);
+        if ($res == null) {
+            return null;
+        } else {
+            return new PartnerSharing($res);
+        }
+    }
+}
+// }}}
+// {{{ class PartnerSettings
+class PartnerSettings
+{
+    public $exposed_uid;
+    public $sharing_level;
+    public $allow_email = false;
+    protected $partner_id;
+    public $partner = null;
+
+    public function __construct(array $data)
+    {
+        foreach ($data as $key => $val) {
+            $this->$key = $val;
+        }
+        $this->partner = PartnerSharing::fetchById($this->partner_id);
+        $this->sharing_visibility = Visibility::get($this->sharing_level);
+    }
+
+    public static function getEmpty($partner_id)
+    {
+        $data = array(
+            'partner_id' => $partner_id,
+            'exposed_uid' => 0,
+            'sharing_level' => Visibility::VIEW_NONE,
+            'allow_email' => false,
+        );
+        return new PartnerSettings($data);
+    }
+}
+// }}}
+
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+?>
index 6bcf258..9500224 100644 (file)
@@ -36,6 +36,7 @@ abstract class ProfileField
         Profile::FETCH_MENTOR_COUNTRY => 'ProfileMentoringCountries',
         Profile::FETCH_JOB_TERMS      => 'ProfileJobTerms',
         Profile::FETCH_MENTOR_TERMS   => 'ProfileMentoringTerms',
+        Profile::FETCH_PARTNER        => 'ProfilePartnerSharing',
     );
 
     /** The profile to which this field belongs
@@ -719,6 +720,45 @@ class ProfileMentoringTerms extends ProfileJobTerms
     }
 }
 // }}}
+// {{{ class ProfilePartnerSharing                    [ Field ]
+class ProfilePartnerSharing extends ProfileField
+{
+    public function __construct(PlInnerSubIterator $it)
+    {
+        require_once 'partnersharing.inc.php';
+
+        $this->pid = $it->value();
+        while ($partner_settings = $it->next()) {
+            $settings = new PartnerSettings($partner_settings);
+            $this->partners_settings[$settings->partner->id] = $settings;
+        }
+    }
+
+    public static function fetchData(array $pids, Visibility $visibility)
+    {
+        $data = XDB::iterator('SELECT  ppss.pid, ppss.exposed_uid, ppss.sharing_level,
+                                       ppss.allow_email, ppss.partner_id,
+                                       ppse.shortname AS partner_shortname,
+                                       ppse.name AS partner_name,
+                                       ppse.url AS partner_url
+                                 FROM  profile_partnersharing_settings AS ppss
+                            LEFT JOIN  profile_partnersharing_enum AS ppse ON (ppss.partner_id = ppse.id)
+                                WHERE  ppss.pid IN {?}
+                             ORDER BY  ' . XDB::formatCustomOrder('ppss.pid', $pids),
+                                 $pids);
+        return PlIteratorUtils::subIterator($data, PlIteratorUtils::arrayValueCallback('pid'));
+    }
+
+    public function get($partner_id)
+    {
+        if (isset($this->partners_settings[$partner_id])) {
+            return $this->partners_settings[$partner_id];
+        } else {
+            return PartnerSettings::getEmpty($partner_id);
+        }
+    }
+}
+// }}}
 // {{{ class CompanyList
 class CompanyList
 {
@@ -780,6 +820,7 @@ class CompanyList
         return null;
     }
 }
+// }}}
 
 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
 ?>
diff --git a/modules/ptawebservice.php b/modules/ptawebservice.php
new file mode 100644 (file)
index 0000000..badc0dd
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2011 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+
+class PTAWebServiceModule extends PlModule
+{
+    function handlers()
+    {
+        return array(
+            'pta/ws/directory/1/search'     => $this->make_api_hook('search',   AUTH_COOKIE, 'api_user_readonly'),
+            'pta/ws/bulkmail/1/get_context' => $this->make_api_hook('bulkmail', AUTH_COOKIE, 'api_user_readonly'),
+            'pta/picture'                   => $this->make_hook('picture_token', AUTH_PUBLIC),
+        );
+    }
+
+    function handler_search(PlPage $page, PlUser $authUser, $payload)
+    {
+        require_once 'partnersharing.inc.php';
+        $partner = PartnerSharing::fetchByAPIUser($authUser);
+        if ($partner == null || !$partner->has_directory) {
+            return PL_FORBIDDEN;
+        }
+
+        $this->load('request.inc.php');
+
+        $payload = new PlDict($payload);
+
+        $errors = WSDirectoryRequest::validatePayload($payload);
+
+        if (count($errors)) {
+            foreach ($errors as $error_code) {
+                $page->trigError(WSDirectoryRequest::$ERROR_MESSAGES[$error_code]);
+            }
+            return PL_BAD_REQUEST;
+        }
+
+        // Processing
+        $request = new WSDirectoryRequest($partner, $payload);
+        $request->assignToPage($page);
+        return PL_JSON;
+    }
+
+    function handler_bulkmail(PlPage $page, PlUser $authUser, $payload)
+    {
+        require_once 'partnersharing.inc.php';
+        $partner = PartnerSharing::fetchByAPIUser($authUser);
+        if ($partner == null || !$partner->has_bulkmail) {
+            return PL_FORBIDDEN;
+        }
+
+        if (!isset($payload['uids'])) {
+            $page->trigError('Malformed query.');
+            return PL_BAD_REQUEST;
+        }
+
+        $uids = $payload['uids'];
+
+        $pf = new UserFilter(
+            new PFC_And(
+                new UFC_PartnerSharingID($partner->id, $uids),
+                new UFC_HasValidEmail(),
+                new UFC_PartnerSharingEmail($partner->id)
+            ));
+
+        $contexts = array();
+        foreach ($pf->iterUsers() as $user) {
+            $contexts[] = array(
+                'name' => $user->fullName(),
+                'email' => $user->bestEmail(),
+                'gender' => $user->isFemale() ? 'woman' : 'man',
+            );
+        }
+        $page->jsonAssign('contexts', $contexts);
+        return PL_JSON;
+    }
+
+    function handler_picture_token(PlPage $page, $size, $token)
+    {
+        XDB::rawExecute('DELETE FROM  profile_photo_tokens
+                               WHERE  expires <= NOW()');
+        $pid = XDB::fetchOneCell('SELECT  pid
+                                    FROM  profile_photo_tokens
+                                   WHERE  token = {?}', $token);
+        if ($pid != null) {
+            $res = XDB::fetchOneAssoc('SELECT  attach, attachmime, x, y, last_update
+                                         FROM  profile_photos
+                                        WHERE  pid = {?}', $pid);
+            $photo = PlImage::fromData($res['attach'], 'image/' . $res['attachmime'], $res['x'], $res['y'], $res['last_update']);
+            $photo->send();
+        } else {
+            return PL_NOT_FOUND;
+        }
+    }
+}
+
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+?>
diff --git a/modules/ptawebservice/request.inc.php b/modules/ptawebservice/request.inc.php
new file mode 100644 (file)
index 0000000..a6c9d3a
--- /dev/null
@@ -0,0 +1,752 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2011 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+
+class WSDirectoryRequest
+{
+    // Default number of returned results.
+    const DEFAULT_AMOUNT = 20;
+
+    public $fields;
+    public $criteria;
+    public $order = array();
+    public $amount = 0;
+    protected $partner = null;
+
+    const ORDER_RAND = 'rand';
+    const ORDER_NAME = 'name';
+    const ORDER_PROMOTION = 'promotion';
+
+    public static $ORDER_CHOICES = array(
+        self::ORDER_RAND,
+        self::ORDER_NAME,
+        self::ORDER_PROMOTION,
+    );
+
+    public function __construct($partner, PlDict $payload)
+    {
+        $this->partner = $partner;
+        global $globals;
+
+        $this->fields = array_intersect($payload->v('fields'), WSRequestFields::$CHOICES);
+        $this->order = array_intersect($payload->v('order', array()), self::$ORDER_CHOICES);
+
+        $this->criteria = array();
+        $criteria = new PlDict($payload->v('criteria'));
+        foreach (WSRequestCriteria::$CHOICES_SIMPLE as $criterion) {
+            if ($criteria->has($criterion)) {
+                $this->criteria[$criterion] = $criteria->s($criterion);
+            }
+        }
+        foreach (WSRequestCriteria::$CHOICES_ENUM as $criterion) {
+            if ($criteria->has($criterion)) {
+                $this->criteria[$criterion] = $criteria->s($criterion);
+            }
+        }
+        foreach (WSRequestCriteria::$CHOICES_LIST as $criterion) {
+            if ($criteria->has($criterion)) {
+                $this->criteria[$criterion] = $criteria->v($criterion);
+            }
+        }
+
+        // Amount may not exceed $globals->pta->max_result_per_query.
+        $amount = $payload->i('amount', self::DEFAULT_AMOUNT);
+        $this->amount = min($amount, $globals->pta->max_result_per_query);
+    }
+
+    public function get()
+    {
+        $cond = $this->getCond();
+        $cond->addChild(new UFC_PartnerSharing($this->partner->id));
+        $pf = new ProfileFilter($cond, $this->getOrders());
+        $pf->restrictVisibilityForPartner($this->partner->id);
+        $response = array();
+        $matches = $pf->getTotalProfileCount();
+        $response['matches'] = $matches;
+
+        $profiles = array();
+        if ($matches) {
+            // TODO : improve fetching by passing an adequate FETCH field
+            $iter = $pf->iterProfiles(new PlLimit($this->amount), 0x0000, Visibility::VIEW_PRIVATE);
+            while ($profile = $iter->next()) {
+                if ($profile->getPartnerSettings($this->partner->id)->exposed_uid !== 0) {
+                    $profile_data = new WSRequestEntry($this->partner, $profile);
+                    $profiles[] = $profile_data->getFields($this->fields);
+                }
+            }
+        }
+        $response['profiles'] = $profiles;
+        return $response;
+    }
+
+    public function assignToPage(PlPage $page)
+    {
+        $response = $this->get();
+        $page->jsonAssign('matches', $response['matches']);
+        $page->jsonAssign('profiles', $response['profiles']);
+    }
+
+    /** Compute the orders to use for the current request.
+     * @return array of PlFilterOrder
+     */
+    protected function getOrders()
+    {
+        $orders = array();
+        foreach ($this->order as $order)
+        {
+            switch ($order) {
+            case self::ORDER_RAND:
+                $orders[] = new PFO_Random();
+                break;
+            case self::ORDER_NAME:
+                $orders[] = new UFO_Name(Profile::DN_SORT);
+                break;
+            case self::ORDER_PROMOTION:
+                $orders[] = new UFO_Promo();
+                break;
+            default:
+                break;
+            }
+        }
+        return $orders;
+    }
+
+    /** Compute the conditions to use for the current request.
+     * @return A PlFilterCondition instance (actually a PFC_And)
+     */
+    protected function getCond()
+    {
+        $cond = new PFC_And();
+        foreach ($this->criteria as $criterion => $value) {
+            switch ($criterion) {
+
+            // ENUM fields
+            case WSRequestCriteria::SCHOOL:
+                // Useless criterion: we don't need to check on origin school
+                if (WSRequestCriteria::$CHOICES_ENUM[$criterion][$value]) {
+                    $cond->addChild(new PFC_True());
+                } else {
+                    $cond->addChild(new PFC_False());
+                };
+                break;
+            case WSRequestCriteria::DIPLOMA:
+                $diploma = WSRequestCriteria::$CHOICES_ENUM[$criterion][$value];
+                $id_X = XDB::fetchOneCell('SELECT  id
+                                             FROM  profile_education_enum
+                                            WHERE  abbreviation = {?}', 'X');
+                $cond->addChildren(array(
+                    new UFC_EducationSchool($id_X),
+                    new UFC_EducationDegree($diploma),
+                ));
+                break;
+
+            // TEXT fields
+            case WSRequestCriteria::FIRSTNAME:
+            case WSRequestCriteria::LASTNAME:
+                $cond->addChild(new UFC_NameTokens($value, UFC_NameTokens::FLAG_PUBLIC, false, false, $criterion));
+                break;
+            case WSRequestCriteria::PROMOTION:
+                $cond->addChild(new PFC_Or(
+                    new UFC_Promo(UserFilter::OP_EQUALS,
+                                  UserFilter::GRADE_ING,
+                                  $value),
+                    new UFC_Promo(UserFilter::OP_EQUALS,
+                                  UserFilter::GRADE_MST,
+                                  $value),
+                    new UFC_Promo(UserFilter::OP_EQUALS,
+                                  UserFilter::GRADE_PHD,
+                                  $value)
+                ));
+                break;
+            case WSRequestCriteria::ALT_DIPLOMA:
+                $cond->addChild(
+                    new UFC_EducationDegree(
+                        DirEnum::getIds(DirEnum::EDUDEGREES, $value)));
+                break;
+            case WSRequestCriteria::DIPLOMA_FIELD:
+                $cond->addChild(
+                    new UFC_EducationField(
+                        DirEnum::getIds(DirEnum::EDUFIELDS, $value)));
+                break;
+            case WSRequestCriteria::CITY:
+                $cond->addChild(
+                    new UFC_AddressField($value,
+                                         UFC_AddressField::FIELD_LOCALITY,
+                                         UFC_Address::TYPE_HOME,
+                                         UFC_Address::FLAG_CURRENT));
+                break;
+            case WSRequestCriteria::COUNTRY:
+                $cond->addChild(
+                    new UFC_AddressField($value,
+                                         UFC_AddressField::FIELD_COUNTRY,
+                                         UFC_Address::TYPE_HOME,
+                                         UFC_Address::FLAG_CURRENT));
+                break;
+            case WSRequestCriteria::ZIPCODE:
+                $cond->addChild(
+                    new UFC_AddressField($value,
+                                         UFC_AddressField::FIELD_ZIPCODE,
+                                         UFC_Address::TYPE_HOME,
+                                         UFC_Address::FLAG_CURRENT));
+                break;
+            case WSRequestCriteria::JOB_ANY_COUNTRY:
+                $cond->addChild(
+                    new UFC_AddressField($value,
+                                         UFC_AddressField::FIELD_COUNTRY,
+                                         UFC_Address::TYPE_PRO,
+                                         UFC_Address::FLAG_ANY));
+                break;
+            case WSRequestCriteria::JOB_CURRENT_CITY:
+                $cond->addChild(
+                    new UFC_AddressField($value,
+                                         UFC_AddressField::FIELD_LOCALITY,
+                                         UFC_Address::TYPE_PRO,
+                                         UFC_Address::FLAG_ANY));
+                break;
+            case WSRequestCriteria::JOB_ANY_COMPANY:
+            case WSRequestCriteria::JOB_CURRENT_COMPANY:
+                $cond->addChild(
+                    new UFC_Job_Company(UFC_Job_Company::JOBNAME,
+                                        $value));
+                break;
+            case WSRequestCriteria::JOB_ANY_SECTOR:
+            case WSRequestCriteria::JOB_CURRENT_SECTOR:
+            case WSRequestCriteria::JOB_CURRENT_TITLE:
+                $cond->addChild(
+                    new UFC_Job_Terms(DirEnum::getIds(DirEnum::JOBTERMS, $value)));
+                break;
+
+            // LIST fields
+            case WSRequestCriteria::HOBBIES:
+                $subcond = new PFC_Or();
+                foreach ($value as $val) {
+                    $subcond->addChild(new UFC_Comment($value));
+                }
+                $cond->addChild($subcond);
+                break;
+            case WSRequestCriteria::JOB_COMPETENCIES:
+            case WSRequestCriteria::JOB_RESUME:
+            case WSRequestCriteria::PROFESSIONAL_PROJECT:
+                $subcond = new PFC_Or();
+                foreach ($value as $val) {
+                    $subcond->addChild(
+                        new UFC_Job_Description($value, UserFilter::JOB_USERDEFINED));
+                }
+                $cond->addChild($subcond);
+                break;
+            case WSRequestCriteria::NOT_UID:
+                $cond->addChild(
+                    new PFC_Not(
+                        new UFC_PartnerSharingID($this->partner->id, $value)));
+                break;
+            default:
+                break;
+            }
+        }
+
+        return $cond;
+    }
+
+    /** Input validation
+     */
+    const ERROR_MISSING_FIELDS = 'missing_fields';
+    const ERROR_MISSING_CRITERIA = 'missing_criteria';
+    const ERROR_MALFORMED_AMOUNT = 'malformed_amount';
+    const ERROR_MALFORMED_ORDER = 'malformed_order';
+
+    public static $ERROR_MESSAGES = array(
+        self::ERROR_MISSING_FIELDS => "The 'fields' field is mandatory.",
+        self::ERROR_MISSING_CRITERIA => "The 'criteria' field is mandatory.",
+        self::ERROR_MALFORMED_AMOUNT => "The 'amount' value is invalid (expected an int)",
+        self::ERROR_MALFORMED_ORDER => "The 'order' value is invalid (expected an array)",
+    );
+
+    /** Static method performing all input validation on the payload.
+     * @param PlDict $payload The payload to validate
+     * @return array Errors discovered when validating input
+     */
+    public static function validatePayload(PlDict $payload)
+    {
+        $errors = array();
+        if (!$payload->has('fields')) {
+            $errors[] = self::ERROR_MISSING_FIELDS;
+        }
+        if (!$payload->has('criteria')) {
+            $errors[] = self::ERROR_MISSING_CRITERIA;
+        }
+
+        if ($payload->has('amount') && $payload->i('amount', -1) < 0) {
+            $errors[] = self::ERROR_MALFORMED_AMOUNT;
+        }
+
+        if (!is_array($payload->v('order', array()))) {
+            $errors[] = self::ERROR_MALFORMED_ORDER;
+        }
+
+        return $errors;
+    }
+}
+
+// {{{ WSRequestEntry
+/** Performs field retrieval for a profile.
+ */
+class WSRequestEntry
+{
+    private $profile = null;
+    private $partner = null;
+    private $settings = null;
+
+    public function __construct($partner, $profile)
+    {
+        $this->partner = $partner;
+        $this->profile = $profile;
+        $this->settings = $this->profile->getPartnerSettings($this->partner->id);
+    }
+
+    public function isVisible($level)
+    {
+        return $this->settings->sharing_visibility->isVisible($level);
+    }
+
+    public function getFields($fields)
+    {
+        $data = array();
+        foreach ($fields as $field)
+        {
+            $val = $this->getFieldValue($field);
+            if ($val !== null) {
+                $data[$field] = $val;
+            }
+        }
+        $data['uid'] = $this->settings->exposed_uid;
+        return $data;
+    }
+
+    protected function getFieldValue($field)
+    {
+        // Shortcut
+        $p = $this->profile;
+
+        switch ($field) {
+        case WSRequestFields::UID:
+            // UID is always included
+            return;
+        case WSRequestFields::BIRTHDATE:
+        case WSRequestFields::FAMILY_POSITION:
+        case WSRequestFields::HONORARY_TITLES:
+        case WSRequestFields::LANGS:
+        case WSRequestFields::JOB_COMPETENCIES:
+        case WSRequestFields::RESUME:
+        case WSRequestFields::PROFESSIONAL_PROJECT:
+        case WSRequestFields::HOBBIES:
+            // Ignored fields
+            return;
+
+        // Public fields
+        case WSRequestFields::FIRSTNAME:
+            return $p->firstName();
+        case WSRequestFields::LASTNAME:
+            return $p->lastName();
+        case WSRequestFields::GENDER:
+            if ($p->isFemale()) {
+                return WSRequestFields::GENDER_WOMAN;
+            } else {
+                return WSRequestFields::GENDER_MAN;
+            }
+        case WSRequestFields::SCHOOL:
+            return WSRequestCriteria::SCHOOL_X;
+        case WSRequestFields::DIPLOMA:
+            $edu = $p->getEducations(Profile::EDUCATION_MAIN);
+            if (count($edu)) {
+                return WSRequestFields::profileDegreeToWSDiploma(
+                    array_pop($edu)->degree);
+            } else {
+                return null;
+            }
+        case WSRequestFields::DIPLOMA_FIELD:
+            $edu = $p->getEducations(Profile::EDUCATION_MAIN);
+            if (count($edu)) {
+                return array_pop($edu)->field;
+            } else {
+                return null;
+            }
+        case WSRequestFields::PROMOTION:
+            return $p->yearpromo();
+        case WSRequestFields::ALT_DIPLOMAS:
+            $diplomas = array();
+            foreach ($p->getExtraEducations() as $edu) {
+                $diplomas[] = WSRequestFields::profileDegreeToWSDiploma(
+                    $edu->degree);
+            }
+            return $diplomas;
+
+        // Other generic profile fields
+        case WSRequestFields::EMAIL:
+            if ($this->settings->sharing_visibility->isVisible(Visibility::EXPORT_PRIVATE)) {
+                // If sharing "all" data, share best email.
+                return $p->displayEmail();
+            } elseif ($this->settings->sharing_visibility->isVisible(Visibility::EXPORT_AX)) {
+                // If sharing "AX" level, share "AX" email.
+                return $p->email_directory;
+            } else {
+                // Otherwise, don't share.
+                return null;
+            }
+        case WSRequestFields::MOBILE_PHONE:
+            $phones = $p->getPhones(Phone::TYPE_MOBILE);
+            if (count($phones)) {
+                $phone = array_pop($phones);
+                if ($this->isVisible($phone->pub)) {
+                    return $phone->display;
+                }
+            }
+            return null;
+        case WSRequestFields::PIC_SMALL:
+        case WSRequestFields::PIC_MEDIUM:
+        case WSRequestFields::PIC_LARGE:
+            if ($this->isVisible($p->photo_pub)) {
+                $token = sha1(uniqid(rand(), true));
+                XDB::execute('DELETE FROM  profile_photo_tokens
+                                    WHERE  pid = {?}', $p->pid);
+                XDB::execute('INSERT INTO  profile_photo_tokens
+                                      SET  pid = {?}, token = {?},
+                                           expires = ADDTIME(NOW(), \'0:05:00\'',
+                                           $p->pid, $token);
+                $size_mappings = array(
+                    WSRequestFields::PIC_SMALL => 'small',
+                    WSRequestFields::PIC_MEDIUM => 'medium',
+                    WSRequestFields::PIC_LARGE => 'large',
+                );
+                $size = $size_mappings[$field];
+                return pl_url("pta/picture/$size/$token");
+            } else {
+                return null;
+            }
+
+        // Address related
+        case WSRequestFields::CURRENT_CITY:
+            $address = $p->getMainAddress();
+            if ($address != null && $this->isVisible($address->pub)) {
+                return $address->localityName;
+            } else {
+                return null;
+            }
+        case WSRequestFields::CURRENT_COUNTRY:
+            $address = $p->getMainAddress();
+            if ($address != null && $this->isVisible($address->pub)) {
+                return $address->countryId;
+            } else {
+                return null;
+            }
+        case WSRequestFields::ADDRESS:
+            $address = $p->getMainAddress();
+            if ($address != null && $this->isVisible($address->pub)) {
+                return $this->addressToResponse($address);
+            } else {
+                return null;
+            }
+
+        // Job related
+        case WSRequestFields::CURRENT_COMPANY:
+            $job = $p->getMainJob();
+            if ($job != null && $this->isVisible($job->pub)) {
+                return $job->company->name;
+            } else {
+                return null;
+            }
+        case WSRequestFields::JOB:
+            $jobs = $p->getJobs(Profile::JOBS_ALL);
+            $res = array();
+            foreach ($jobs as $job) {
+                if ($this->isVisible($job->pub)) {
+                    $jobs[] = $this->jobToResponse($job);
+                }
+            }
+            return $jobs;
+        case WSRequestFields::MINI_RESUME:
+            if ($this->isVisible(Visibility::EXPORT_PRIVATE)) {
+                return $p->cv;
+            } else {
+                return null;
+            }
+
+        // Community
+        case WSRequestFields::GROUPS:
+            $groups = array();
+            if ($this->isVisible(Visibility::EXPORT_PRIVATE)) {
+                foreach ($p->owner()->groups(true, true) as $group) {
+                    $groups[] = array('name' => $group['nom']);
+                }
+            }
+            return $groups;
+        case WSRequestFields::FRIENDS:
+            $friends = array();
+            if ($this->isVisible(Visibility::EXPORT_PRIVATE)) {
+                while ($contact = $p->owner()->iterContacts()) {
+                    $cps = $contact->getPartnerSettings(UFC_PartnerSharing::PTA);
+                    if ($cps->sharing_visibility->isVisible(Visibility::EXPORT_PRIVATE)) {
+                        $friends[] = $cps->exposed_uid;
+                    }
+                }
+            }
+            return $friends;
+        case WSRequestFields::NETWORKING:
+            $networks = array();
+            if ($this->isVisible(Visibility::EXPORT_PRIVATE)) {
+                foreach ($p->getNetworking(Profile::NETWORKING_ALL) as $nw) {
+                    $networks[] = array(
+                        'network' => $nw['name'],
+                        'login' => $nw['address'],
+                    );
+                }
+            }
+            return $networks;
+
+        default:
+            return null;
+        }
+    }
+
+    protected function jobToResponse($job)
+    {
+        $data = array();
+        $data['company'] = $job->company->name;
+        $data['title'] = $job->description;
+        $data['sector'] = array_pop($job->terms);
+        $data['entry'] = null;
+        $data['left'] = null;
+        foreach($job->phones() as $phone) {
+            if ($this->isVisible($phone->pub)) {
+                $data['phone'] = $phone->display;
+                break;
+            }
+        }
+        if ($job->address && $this->isVisible($job->address->pub)) {
+            $data['address'] = $this->addressToResponse($job->address);
+        }
+        return $data;
+    }
+
+    protected function addressToResponse($address)
+    {
+        $data = array();
+        $data['street'] = $address->postalText;
+        $data['zipcode'] = $address->postalCode;
+        $data['city'] = $address->localityName;
+        $data['country'] = $address->countryId;
+        $data['latitude'] = $address->latitude;
+        $data['longitude'] = $address->longitude;
+        return $data;
+    }
+}
+// }}}
+// {{{ WSRequestCriteria
+/** Holds all enums and related mappings for criterias.
+ */
+class WSRequestCriteria
+{
+    const FIRSTNAME = 'firstname';
+    const LASTNAME = 'lastname';
+    const SCHOOL = 'school';
+    const DIPLOMA = 'diploma';
+    const DIPLOMA_FIELD = 'diploma_field';
+    const PROMOTION = 'promotion';
+    const HOBBIES = 'hobbies';
+    const ZIPCODE = 'zipcode';
+    const CITY = 'city';
+    const COUNTRY = 'country';
+    const JOB_CURRENT_SECTOR = 'job_current_sector';
+    const JOB_CURRENT_TITLE = 'job_current_title';
+    const JOB_CURRENT_COMPANY = 'job_current_company';
+    const JOB_CURRENT_CITY = 'job_current_city';
+    const JOB_CURRENT_COUNTRY = 'job_current_country';
+    const JOB_ANY_SECTOR = 'job_any_sector';
+    const JOB_ANY_COMPANY = 'job_any_company';
+    const JOB_ANY_COUNTRY = 'job_any_country';
+    const JOB_RESUME = 'job_resume';
+    const JOB_COMPETENCIES = 'job_competencies';
+    const PROFESSIONAL_PROJECT = 'professional_project';
+    const ALT_DIPLOMA = 'alt_diploma';
+    const NOT_UID = 'not_uid';
+
+    public static $CHOICES_SIMPLE = array(
+        self::FIRSTNAME,
+        self::LASTNAME,
+        self::PROMOTION,
+        self::ALT_DIPLOMA,
+        self::DIPLOMA_FIELD,
+        self::CITY,
+        self::ZIPCODE,
+        self::COUNTRY,
+        self::JOB_ANY_COUNTRY,
+        self::JOB_CURRENT_CITY,
+        self::JOB_CURRENT_COUNTRY,
+        self::JOB_ANY_COMPANY,
+        self::JOB_ANY_SECTOR,
+        self::JOB_CURRENT_COMPANY,
+        self::JOB_CURRENT_SECTOR,
+        self::JOB_CURRENT_TITLE,
+    );
+
+    const SCHOOL_AGRO = 'agro';
+    const SCHOOL_ENSAE = 'ensae';
+    const SCHOOL_ENSCP = 'enscp';
+    const SCHOOL_ENST = 'enst';
+    const SCHOOL_ENSTA = 'ensta';
+    const SCHOOL_ESPCI = 'espci';
+    const SCHOOL_GADZ = 'gadz';
+    const SCHOOL_HEC = 'hec';
+    const SCHOOL_MINES = 'ensmp';
+    const SCHOOL_PONTS = 'enpc';
+    const SCHOOL_SUPELEC = 'supelec';
+    const SCHOOL_SUPOP = 'supop';
+    const SCHOOL_X = 'X';
+
+    const DIPLOMA_ING = 'ING';
+    const DIPLOMA_MASTER = 'MASTER';
+    const DIPLOMA_PHD = 'PHD';
+
+    public static $CHOICES_ENUM = array(
+        self::SCHOOL => array(
+            self::SCHOOL_AGRO => false,
+            self::SCHOOL_ENSAE => false,
+            self::SCHOOL_ENSCP => false,
+            self::SCHOOL_ENST => false,
+            self::SCHOOL_ENSTA => false,
+            self::SCHOOL_ESPCI => false,
+            self::SCHOOL_GADZ => false,
+            self::SCHOOL_HEC => false,
+            self::SCHOOL_MINES => false,
+            self::SCHOOL_PONTS => false,
+            self::SCHOOL_SUPELEC => false,
+            self::SCHOOL_SUPOP => false,
+            self::SCHOOL_X => true,
+        ),
+        self::DIPLOMA => array(
+            self::DIPLOMA_ING => UserFilter::GRADE_ING,
+            self::DIPLOMA_MASTER => UserFilter::GRADE_MST,
+            self::DIPLOMA_PHD => UserFilter::GRADE_PHD,
+        ),
+    );
+
+    public static $CHOICES_LIST = array(
+        self::HOBBIES,
+        self::JOB_COMPETENCIES,
+        self::JOB_RESUME,
+        self::NOT_UID,
+        self::PROFESSIONAL_PROJECT,
+    );
+}
+
+// }}}
+// {{{ WSRequestFields
+/** Holds all enums for fields.
+ */
+class WSRequestFields
+{
+    const UID = 'uid';
+    const FIRSTNAME = 'firstname';
+    const LASTNAME = 'lastname';
+    const BIRTHDATE = 'birthdate';
+    const GENDER = 'gender';
+    const FAMILY_POSITION = 'family_position';
+    const SCHOOL = 'school';
+    const DIPLOMA = 'diploma';
+    const DIPLOMA_FIELD = 'diploma_field';
+    const PROMOTION = 'promotion';
+    const ALT_DIPLOMAS = 'alt_diplomas';
+    const CURRENT_COMPANY = 'current_company';
+    const CURRENT_CITY = 'current_city';
+    const CURRENT_COUNTRY = 'current_country';
+    const MOBILE_PHONE = 'mobile_phone';
+    const HONORARY_TITLES = 'honorary_titles';
+    const EMAIL = 'email';
+    const PIC_SMALL = 'pic_small';
+    const PIC_MEDIUM = 'pic_medium';
+    const PIC_LARGE = 'pic_large';
+    const ADDRESS = 'address';
+    const JOB = 'job';
+    const GROUPS = 'groups';
+    const LANGS = 'langs';
+    const JOB_COMPETENCIES = 'job_competencies';
+    const MINI_RESUME = 'mini_resume';
+    const RESUME = 'resume';
+    const PROFESSIONAL_PROJECT = 'professional_project';
+    const HOBBIES = 'hobbies';
+    const FRIENDS = 'friends';
+    const NETWORKING = 'networking';
+
+    const GENDER_MAN = 'man';
+    const GENDER_WOMAN = 'woman';
+
+    public static $CHOICES = array(
+        self::UID,
+        self::FIRSTNAME,
+        self::LASTNAME,
+        self::BIRTHDATE,
+        self::GENDER,
+        self::FAMILY_POSITION,
+        self::SCHOOL,
+        self::DIPLOMA,
+        self::DIPLOMA_FIELD,
+        self::PROMOTION,
+        self::ALT_DIPLOMAS,
+        self::CURRENT_COMPANY,
+        self::CURRENT_CITY,
+        self::CURRENT_COUNTRY,
+        self::MOBILE_PHONE,
+        self::HONORARY_TITLES,
+        self::EMAIL,
+        self::PIC_SMALL,
+        self::PIC_MEDIUM,
+        self::PIC_LARGE,
+        self::ADDRESS,
+        self::JOB,
+        self::GROUPS,
+        self::LANGS,
+        self::JOB_COMPETENCIES,
+        self::MINI_RESUME,
+        self::RESUME,
+        self::PROFESSIONAL_PROJECT,
+        self::HOBBIES,
+        self::FRIENDS,
+        self::NETWORKING,
+    );
+
+    public static function profileDegreeToWSDiploma($degree)
+    {
+        switch ($degree) {
+        case Profile::DEGREE_X:
+            return self::DIPLOMA_ING;
+        case Profile::DEGREE_M:
+            return self::DIPLOMA_MASTER;
+        case Profile::DEGREE_D:
+            return self::DIPLOMA_PHD;
+        default:
+            return null;
+        }
+    }
+
+}
+// }}}
+
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+?>
diff --git a/upgrade/1.1.3/03_pta.sql b/upgrade/1.1.3/03_pta.sql
new file mode 100644 (file)
index 0000000..cb050e7
--- /dev/null
@@ -0,0 +1,37 @@
+CREATE TABLE IF NOT EXISTS profile_partnersharing_enum (
+  id int(6) unsigned NOT NULL,
+  api_uid int(11) unsigned NULL,
+  shortname varchar(64) NOT NULL DEFAULT '',
+  name varchar(255) NOT NULL DEFAULT '',
+  url varchar(255) NOT NULL DEFAULT '',
+  default_sharing_level enum('admin', 'private', 'ax', 'public', 'none') DEFAULT 'none',
+  has_directory int(1) unsigned NOT NULL DEFAULT 0,
+  has_bulkmail int(1) unsigned NOT NULL DEFAULT 0,
+  PRIMARY KEY (id),
+  FOREIGN KEY (api_uid) REFERENCES accounts (uid) ON DELETE SET NULL ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO profile_partnersharing_enum
+        SET shortname = 'pta', name = 'ParisTech Alumni', url = 'http://www.paristech-alumni.org', default_sharing_level = 'public', has_directory = 1, has_bulkmail = 1;
+
+CREATE TABLE IF NOT EXISTS profile_partnersharing_settings (
+  pid int(11) unsigned NOT NULL,
+  partner_id int(6) unsigned NOT NULL,
+  exposed_uid varchar(255) NOT NULL,
+  sharing_level enum('admin', 'private', 'ax', 'public', 'none') DEFAULT 'none',
+  allow_email enum('none', 'digest', 'direct') DEFAULT 'direct',
+  last_connection datetime NULL,
+  PRIMARY KEY (pid, partner_id),
+  KEY (partner_id, exposed_uid),
+  FOREIGN KEY (pid) REFERENCES profiles (pid) ON DELETE CASCADE ON UPDATE CASCADE,
+  FOREIGN KEY (partner_id) REFERENCES profile_partnersharing_enum (id) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS profile_photo_tokens (
+  pid int(11) unsigned NOT NULL,
+  token varchar(255) NOT NULL,
+  expires datetime NOT NULL,
+  PRIMARY KEY (pid),
+  KEY (token),
+  FOREIGN KEY (pid) REFERENCES profiles (pid) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;