Merge branch 'xorg/master' into xorg/f/xnet-accounts
authorStéphane Jacob <sj@m4x.org>
Fri, 7 Jan 2011 13:44:53 +0000 (14:44 +0100)
committerStéphane Jacob <sj@m4x.org>
Fri, 7 Jan 2011 13:44:53 +0000 (14:44 +0100)
1  2 
classes/user.php
classes/xnetpage.php
configs/mails.conf
htdocs/javascript/password.js
modules/xnet.php
modules/xnetgrp.php
templates/xnet/skin.tpl
templates/xnetgrp/membres-del.tpl
templates/xnetgrp/membres-edit.tpl

diff --combined classes/user.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /***************************************************************************
-  *  Copyright (C) 2003-2010 Polytechnique.org                              *
+  *  Copyright (C) 2003-2011 Polytechnique.org                              *
   *  http://opensource.polytechnique.org/                                   *
   *                                                                         *
   *  This program is free software; you can redistribute it and/or modify   *
  
  class User extends PlUser
  {
+     const PERM_API_USER_READONLY = 'api_user_readonly';
+     const PERM_DIRECTORY_AX      = 'directory_ax';
+     const PERM_DIRECTORY_PRIVATE = 'directory_private';
+     const PERM_EDIT_DIRECTORY    = 'edit_directory';
+     const PERM_FORUMS            = 'forums';
+     const PERM_GROUPS            = 'groups';
+     const PERM_LISTS             = 'lists';
+     const PERM_MAIL              = 'mail';
+     const PERM_PAYMENT           = 'payment';
      private $_profile_fetched = false;
      private $_profile = null;
  
@@@ -37,7 -47,7 +47,7 @@@
          }
  
          if ($login instanceof User) {
-             $machin->id();
+             return $login->id();
          }
  
          if ($login instanceof Profile) {
              $joins .= XDB::format("LEFT JOIN group_members AS gpm ON (gpm.uid = a.uid AND gpm.asso_id = {?})\n", $globals->asso('id'));
              $fields[] = 'gpm.perms AS group_perms';
              $fields[] = 'gpm.comm AS group_comm';
+             $fields[] = 'gpm.position AS group_position';
          }
          if (count($fields) > 0) {
              $fields = ', ' . implode(', ', $fields);
                                        IF (ab.alias IS NULL, NULL, CONCAT(ab.alias, \'@' . $globals->mail->domain . '\')) AS bestalias,
                                        IF (ab.alias IS NULL, NULL, CONCAT(ab.alias, \'@' . $globals->mail->domain2 . '\')) AS bestalias_alternate,
                                        a.email, a.full_name, a.directory_name, a.display_name, a.sex = \'female\' AS gender,
-                                       IF(a.state = \'active\', at.perms, \'\') AS perms,
-                                       a.email_format, a.is_admin, a.state, a.type, a.skin,
+                                       IF(a.state = \'active\', CONCAT(at.perms, \',\', IF(a.user_perms IS NULL, \'\', a.user_perms)), \'\') AS perms,
+                                       a.user_perms, a.email_format, a.is_admin, a.state, a.type, at.description AS type_description, a.skin,
                                        FIND_IN_SET(\'watch\', a.flags) AS watch, a.comment,
                                        a.weak_password IS NOT NULL AS weak_access, g.g_account_name IS NOT NULL AS googleapps,
                                        a.token IS NOT NULL AS token_access, a.token, a.last_version,
          $this->perm_flags = self::makePerms($this->perms, $this->is_admin);
      }
  
+     public function setPerms($perms)
+     {
+         $this->perms = $perms;
+         $this->perm_flags = null;
+     }
      // We do not want to store the password in the object.
      // So, fetch it 'on demand'
      public function password()
          return $this->profile()->promo();
      }
  
+     public function category()
+     {
+         $promo = $this->promo();
+         if (!empty($promo)) {
+             return $promo;
+         } else {
+             return $this->type_description;
+         }
+     }
      public function firstName()
      {
          if (!$this->hasProfile()) {
  
      /** Return the main profile attached with this account if any.
       */
-     public function profile($forceFetch = false)
+     public function profile($forceFetch = false, $fields = 0x0000, $visibility = null)
      {
          if (!$this->_profile_fetched || $forceFetch) {
              $this->_profile_fetched = true;
-             $this->_profile = Profile::get($this);
+             $this->_profile = Profile::get($this, $fields, $visibility);
          }
          return $this->_profile;
      }
          return !is_null($this->profile());
      }
  
+     /** Return true if given a reference to the profile of this user.
+      */
+     public function isMyProfile($other)
+     {
+         if (!$other) {
+             return false;
+         } else if ($other instanceof Profile) {
+             $profile = $this->profile();
+             return $profile && $profile->id() == $other->id();
+         }
+         return false;
+     }
      /** Check if the user can edit to given profile.
       */
      public function canEdit(Profile $profile)
      {
-         // XXX: Check permissions (e.g. secretary permission)
-         //      and flags from the profile
+         if ($this->checkPerms(User::PERM_EDIT_DIRECTORY)) {
+             return true;
+         }
          return XDB::fetchOneCell('SELECT  pid
                                      FROM  account_profiles
                                     WHERE  uid = {?} AND pid = {?}',
  
      // Groupes X
      private $groups = null;
-     public function groups()
+     public function groups($institutions = false, $onlyPublic = false)
      {
          if (is_null($this->groups)) {
-             $this->groups = XDB::fetchAllAssoc('asso_id', 'SELECT  asso_id, perms, comm
-                                                              FROM  group_members
+             $this->groups = XDB::fetchAllAssoc('asso_id', 'SELECT  gm.asso_id, gm.perms, gm.comm,
+                                                                    g.diminutif, g.nom, g.site, g.cat,
+                                                                    g.pub
+                                                              FROM  group_members AS gm
+                                                        INNER JOIN  groups AS g ON (g.id = gm.asso_id)
                                                              WHERE  uid = {?}',
                                                  $this->id());
          }
-         return $this->groups;
-     }
-     public function groupNames($institutions = false)
-     {
-         if ($institutions) {
-             $where = ' AND (g.cat = \'GroupesX\' OR g.cat = \'Institutions\')';
+         if (!$institutions && !$onlyPublic) {
+             return $this->groups;
          } else {
-             $where = '';
+             $result = array();
+             foreach ($this->groups as $id=>$data) {
+                 if ($institutions) {
+                     if ($data['cat'] != Group::CAT_GROUPESX && $data['cat'] != Group::CAT_INSTITUTIONS) {
+                         continue;
+                     }
+                 }
+                 if ($onlyPublic) {
+                     if ($data['pub'] != 'public') {
+                         continue;
+                     }
+                 }
+                 $result[$id] = $data;
+             }
+             return $result;
          }
-         return XDB::fetchAllAssoc('SELECT  g.diminutif, g.nom, g.site
-                                      FROM  group_members AS gm
-                                 LEFT JOIN  groups AS g ON (g.id = gm.asso_id)
-                                     WHERE  gm.uid = {?}' . $where,
-                                   $this->id());
      }
  
 +    public function groupCount()
 +    {
 +        return XDB::fetchOneCell('SELECT  COUNT(DISTINCT(asso_id))
 +                                    FROM  group_members
 +                                   WHERE  uid = {?}',
 +                                 $this->id());
 +    }
 +
 +    public function inGroup($asso_id)
 +    {
 +        $res = XDB::fetchOneCell('SELECT  COUNT(*)
 +                                    FROM  group_members
 +                                   WHERE  uid = {?} AND asso_id = {?}',
 +                                 $this->id(), $asso_id);
 +        return ($res > 0);
 +    }
 +
      /**
       * Clears a user.
       *  *always deletes in: account_lost_passwords, register_marketing,
          }
  
          if ($clearAll) {
+             global $globals;
              $groupIds = XDB::iterator('SELECT  asso_id
                                           FROM  group_members
                                          WHERE  uid = {?}',
                                        $this->id());
              while ($groupId = $groupIds->next()) {
                  $group = Group::get($groupId);
-                 if ($group->notif_unsub) {
+                 if (!empty($group) && $group->notif_unsub) {
                      $mailer = new PlMailer('xnetgrp/unsubscription-notif.mail.tpl');
                      $admins = $group->iterAdmins();
                      while ($admin = $admins->next()) {
                  }
              }
  
-             $tables = array('account_auth_openid', 'gannounce_read', 'contacts',
-                             'email_options', 'gemail_send_save', 'emails',
-                             'forum_innd', 'gforum_profiles', 'forum_subs',
-                             'gapps_accounts', 'ggapps_nicknames', 'group_announces_read',
-                             'group_members', 'ggroup_member_sub_requests', 'reminder', 'requests',
-                             'requests_hidden');
+             $tables = array('account_auth_openid', 'announce_read', 'contacts',
+                             'email_options', 'email_send_save', 'emails',
+                             'forum_innd', 'forum_profiles', 'forum_subs',
+                             'group_announces_read', 'group_members',
+                             'group_member_sub_requests', 'reminder', 'requests',
+                             'requests_hidden', 'aliases');
              foreach ($tables as $t) {
                  XDB::execute('DELETE FROM  ' . $t . '
                                      WHERE  uid = {?}',
-                     $this->id());
+                              $this->id());
+             }
+             foreach (array('gapps_accounts', 'gapps_nicknames') as $t) {
+                 XDB::execute('DELETE FROM  ' . $t . '
+                                     WHERE  l_userid = {?}',
+                              $this->id());
              }
  
              XDB::execute("UPDATE  accounts
              if ($globals->mailstorage->googleapps_domain) {
                  require_once 'googleapps.inc.php';
  
-                 if (GoogleAppsAccount::account_status($uid)) {
-                     $account = new GoogleAppsAccount($user);
+                 if (GoogleAppsAccount::account_status($this->id())) {
+                     $account = new GoogleAppsAccount($this);
                      $account->suspend();
                  }
              }
          }
  
-         $mmlist = new MMList($this);
+         $mmlist = new MMList(S::user());
          $mmlist->kill($this->hruid, $clearAll);
      }
  
          }
  
          // Updates user in following tables.
-         foreach (array('group_announces', 'payment_transactions', 'log_sessions') as $table) {
+         foreach (array('group_announces', 'payment_transactions', 'log_sessions', 'group_events') as $table) {
              XDB::execute('UPDATE  ' . $table . '
                               SET  uid = {?}
                             WHERE  uid = {?}',
                           $newuser->id(), $this->id());
          }
-         XDB::execute('UPDATE  group_events
-                          SET  organisateur_uid = {?}
-                        WHERE  organisateur_uid = {?}',
-                      $newuser->id(), $this->id());
  
          // Merges user in following tables, ie updates when possible, then deletes remaining occurences of the old user.
          foreach (array('group_announces_read', 'group_event_participants', 'group_member_sub_requests', 'group_members') as $table) {
          XDB::execute('UPDATE  log_last_sessions
                           SET  id = {?}
                         WHERE  uid = {?}',
-                      $newuser->id());
+                      $lastSession, $newuser->id());
          XDB::execute('DELETE FROM  accounts
                              WHERE  uid = {?}',
                       $this->id());
          if ($is_admin) {
              $flags->addFlag(PERMS_ADMIN);
          }
+         // Access to private directory implies access to 'less'-private version.
+         if ($flags->hasFlag('directory_private')) {
+             $flags->addFlag('directory_ax');
+         }
          return $flags;
      }
  
diff --combined classes/xnetpage.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /***************************************************************************
-  *  Copyright (C) 2003-2010 Polytechnique.org                              *
+  *  Copyright (C) 2003-2011 Polytechnique.org                              *
   *  http://opensource.polytechnique.org/                                   *
   *                                                                         *
   *  This program is free software; you can redistribute it and/or modify   *
@@@ -43,8 -43,6 +43,8 @@@ class XnetPage extends PlPag
          }
          $this->addJsLink('jquery.js');
          $this->addJsLink('overlib.js');
 +        $this->addJsLink('secure_hash.js');
 +        $this->addJsLink('sha1.js');
          $this->addJsLink('wiki.js');
          $this->addJsLink('xorg.js');
          $this->setTitle('Les associations polytechniciennes');
@@@ -83,9 -81,6 +83,9 @@@
          $sub = array();
          $sub['tous les groupes'] = 'plan';
          $sub['documentation']     = 'Xnet';
 +        if (S::user()->type == 'xnet') {
 +            $sub['mon compte'] = 'member/edit';
 +        }
          $sub['signaler un bug']   = array('href' => 'send_bug/'.$_SERVER['REQUEST_URI'], 'class' => 'popup_840x600');
          $menu["no_title"]   = $sub;
  
diff --combined configs/mails.conf
@@@ -62,10 -62,6 +62,10 @@@ cc=validation+googleapps@polytechnique.
  [xnet_unsubscription]
  from="Gestion des groupes X sur Polytechnique.net" <support@polytechnique.org>
  
 +[xnet_unsubscription_account]
 +from=br@staff.polytechnique.org
 +to=br@staff.polytechnique.org
 +
  [registration]
  from="Webmaster Polytechnique.org" <web@polytechnique.org>
  to=registration+watch@staff.m4x.org
@@@ -75,3 -71,6 +75,6 @@@ replyto=registration+watch@staff.m4x.or
  from="Polytechnique.org" <validation_modification@polytechnique.org>
  cc="Polytechnique.org" <validation_modification@polytechnique.org>
  
+ [xnet_notification]
+ from="Polytechnique.org" <contact@polytechnique.org>
@@@ -1,5 -1,5 +1,5 @@@
  /***************************************************************************
-  *  Copyright (C) 2003-2010 Polytechnique.org                              *
+  *  Copyright (C) 2003-2011 Polytechnique.org                              *
   *  http://opensource.polytechnique.org/                                   *
   *                                                                         *
   *  This program is free software; you can redistribute it and/or modify   *
@@@ -21,9 -21,6 +21,9 @@@
  function hashResponse(password1, password2, hasConfirmation) {
      pw1 = $('[name=' + password1 + ']').val();
  
 +    if (pw1 == '********') {
 +        return true;
 +    }
      if (hasConfirmation) {
          pw2 = $('[name=' + password2 + ']').val();
          if (pw1 != pw2) {
@@@ -31,6 -28,8 +31,6 @@@
              return false;
          }
          $('[name=' + password2 + ']').val('');
 -    } else if (pw1 == '********') {
 -        return true;
      }
  
      if (pw1.length < 6) {
diff --combined modules/xnet.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /***************************************************************************
-  *  Copyright (C) 2003-2010 Polytechnique.org                              *
+  *  Copyright (C) 2003-2011 Polytechnique.org                              *
   *  http://opensource.polytechnique.org/                                   *
   *                                                                         *
   *  This program is free software; you can redistribute it and/or modify   *
@@@ -33,7 -33,6 +33,7 @@@ class XnetModule extends PLModul
              'plan'        => $this->make_hook('plan',      AUTH_PUBLIC),
              'photo'       => $this->make_hook('photo',     AUTH_MDP),
              'autologin'   => $this->make_hook('autologin', AUTH_MDP),
 +            'edit'        => $this->make_hook('edit',      AUTH_MDP, 'user'),
  
              'Xnet'        => $this->make_wiki_hook(),
          );
          echo '$.ajax({ url: "'.$url.'?forceXml=1", dataType: "xml", success: function(xml) { $("body",xml).insertBefore("body"); $("body:eq(1)").remove(); }});';
          exit;
      }
 +
 +    function handler_edit(&$page)
 +    {
 +        global $globals;
 +
 +        $user = S::user();
 +        if (empty($user)) {
 +            return PL_NOT_FOUND;
 +        }
 +        if ($user->type != 'xnet') {
 +            pl_redirect('index');
 +        }
 +
 +        $page->changeTpl('xnet/edit.tpl');
 +        if (Post::has('change')) {
 +            S::assert_xsrf_token();
 +
 +            if ($user->groupCount() == 0 && Post::t('delete') == 'OUI') {
 +                XDB::execute('DELETE FROM  accounts
 +                                    WHERE  uid = {?}',
 +                             $user->id());
 +                pl_redirect('index');
 +            }
 +
 +            // Convert user status to X
 +            if (!Post::blank('login_X')) {
 +                $forlife = $this->changeLogin($page, $user, Post::t('login_X'));
 +                if ($forlife) {
 +                    pl_redirect('index');
 +                }
 +            }
 +
 +            // Update user info
 +            XDB::query('UPDATE  accounts
 +                           SET  full_name = {?}, directory_name = {?}, display_name = {?},
 +                                sex = {?}, email = {?}
 +                         WHERE  uid = {?}',
 +                       Post::t('full_name'), Post::t('directory_name'), Post::t('display_name'),
 +                       (Post::t('sex') == 'male') ? 'male' : 'female', Post::t('email'), $user->id());
 +            // If user is of type xnet and new password is given.
 +            if (!Post::blank('pwhash')) {
 +                XDB::query('UPDATE  accounts
 +                               SET  password = {?}
 +                             WHERE  uid = {?}',
 +                           Post::t('pwhash'), $user->id());
 +            }
 +            if (XDB::affectedRows()) {
 +                $page->trigSuccess('Données mises à jour.');
 +            }
 +        }
 +
 +        $page->addJsLink('password.js');
 +        $page->assign('user', $user);
 +    }
  }
  
  // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
diff --combined modules/xnetgrp.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /***************************************************************************
-  *  Copyright (C) 2003-2010 Polytechnique.org                              *
+  *  Copyright (C) 2003-2011 Polytechnique.org                              *
   *  http://opensource.polytechnique.org/                                   *
   *                                                                         *
   *  This program is free software; you can redistribute it and/or modify   *
@@@ -48,7 -48,7 +48,7 @@@ class XnetGrpModule extends PLModul
              '%grp/member/new/ajax' => $this->make_hook('admin_member_new_ajax', AUTH_MDP,    'user', NO_AUTH),
              '%grp/member/del'      => $this->make_hook('admin_member_del',      AUTH_MDP,    'groupadmin'),
  
-             '%grp/rss'             => $this->make_hook('rss',                   AUTH_PUBLIC, 'user', NO_HTTPS),
+             '%grp/rss'             => $this->make_token_hook('rss',             AUTH_PUBLIC),
              '%grp/announce/new'    => $this->make_hook('edit_announce',         AUTH_MDP,    'groupadmin'),
              '%grp/announce/edit'   => $this->make_hook('edit_announce',         AUTH_MDP,    'groupadmin'),
              '%grp/announce/photo'  => $this->make_hook('photo_announce',        AUTH_PUBLIC),
                      $page->trigError('Ni le nom ni le diminutif du groupe ne peuvent être vide.');
                      return;
                  }
+                 $axDate = make_datetime(Post::v('axDate'));
+                 if (Post::t('axDate') != '') {
+                     $axDate = make_datetime(Post::v('axDate'))->format('Y-m-d');
+                 } else {
+                     $axDate = null;
+                 }
                  XDB::execute(
                      "UPDATE  groups
                          SET  nom={?}, diminutif={?}, cat={?}, dom={?},
                               descr={?}, site={?}, mail={?}, resp={?},
-                              forum={?}, mail_domain={?}, ax={?}, pub={?},
+                              forum={?}, mail_domain={?}, ax={?}, axDate = {?}, pub={?},
                               sub_url={?}, inscriptible={?}, unsub_url={?},
-                              flags={?}
+                              flags = {?}, welcome_msg = {?}
                        WHERE  id={?}",
                        Post::v('nom'), Post::v('diminutif'),
-                       Post::v('cat'), Post::i('dom'),
+                       Post::v('cat'), (Post::i('dom') == 0) ? null : Post::i('dom'),
                        Post::v('descr'), $site,
                        Post::v('mail'), Post::v('resp'),
                        Post::v('forum'), Post::v('mail_domain'),
-                       Post::has('ax'), Post::v('pub'),
+                       Post::has('ax'), $axDate, Post::v('pub'),
                        Post::v('sub_url'), Post::v('inscriptible'),
-                       Post::v('unsub_url'), $flags, $globals->asso('id'));
+                       Post::v('unsub_url'), $flags, Post::t('welcome_msg'),
+                       $globals->asso('id'));
                  if (Post::v('mail_domain')) {
                      XDB::execute('INSERT IGNORE INTO virtual_domains (domain) VALUES({?})',
                                             Post::v('mail_domain'));
                      "UPDATE  groups
                          SET  descr={?}, site={?}, mail={?}, resp={?},
                               forum={?}, pub= {?}, sub_url={?},
-                              unsub_url={?},flags={?}
+                              unsub_url = {?}, flags = {?}, welcome_msg = {?}
                        WHERE  id={?}",
                        Post::v('descr'), $site,
                        Post::v('mail'), Post::v('resp'),
                        Post::v('forum'), Post::v('pub'),
                        Post::v('sub_url'), Post::v('unsub_url'),
-                       $flags, $globals->asso('id'));
+                       $flags, Post::t('welcome_msg'),
+                       $globals->asso('id'));
              }
  
  
          global $globals;
  
          $page->changeTpl('xnetgrp/mail.tpl');
-         $mmlist = new MMList(S::v('uid'), S::v('password'),
-                            $globals->asso('mail_domain'));
+         $mmlist = new MMList(S::user(), $globals->asso('mail_domain'));
          $page->assign('listes', $mmlist->get_lists());
          $page->assign('user', S::user());
-         $page->addJsLink('ajax.js');
  
          if (Post::has('send')) {
              S::assert_xsrf_token();
              $mailer->setSubject('[' . $globals->asso('nom') . '] Demande d\'inscription');
              $message = ($user->isFemale() ? 'Chère' : 'Cher') . " Camarade,\n"
                       . "\n"
-                      . "  Suite à ta demande d'adhésion à " . $globals->asso('nom') . ",\n"
-                      . "j'ai le plaisir de t'annoncer que ton inscription a été validée !\n"
+                      . "  Suite à ta demande d'adhésion à " . $globals->asso('nom')
+                      . ", j'ai le plaisir de t'annoncer que ton inscription a été validée !\n"
+                      . (is_null($globals->asso('welcome_msg')) ? '' : "\n" . $globals->asso('welcome_msg') . "\n")
                       . "\n"
                       . "Bien cordialement,\n"
                       . "-- \n"
                       . S::user()->fullName() . '.';
-             $mailer->setTxtBody($message);
+             $mailer->setTxtBody(wordwrap($message, 72));
              $mailer->send();
          }
      }
          global $globals;
  
          $page->changeTpl('xnetgrp/membres-add.tpl');
-         $page->addJsLink('ajax.js');
  
          if (is_null($email)) {
              return;
              }
          } else {
              // User is of type xnet.
-             list($firstname, $lastname) = explode('@', $email);
-             $hruid = User::makeHrid($firstname, $lastname, 'ext');
+             list($mbox, $domain) = explode('@', strtolower($email));
+             $hruid = User::makeHrid($mbox, $domain, 'ext');
              // User might already have an account (in another group for example).
              $user = User::get($hruid);
  
              // If the user has no account yet, creates new account: build names from email address.
              if (empty($user)) {
-                 $display_name = ucwords(strtolower(substr($hruid, strpos('.', $hruid))));
-                 $full_name = ucwords(strtolower(str_replace('.', ' ', substr($email, strpos('@', $email)))));
-                 XDB::execute('INSERT INTO  accounts (hruid, display_name, full_name, email, type)
-                                    VALUES  ({?}, {?}, {?}, {?}, \'xnet\')',
-                              $hruid, $display_name, $full_name, $email);
+                 $parts = explode('.', $mbox);
+                 if (count($parts) == 1) {
+                     $display_name = $full_name = $directory_name = ucfirst($mbox);
+                 } else {
+                     $firstname = ucfirst($parts[0]);
+                     $lastname = ucwords(implode(' ', array_slice($parts, 1)));
+                     $display_name = $firstname;
+                     $full_name = "$firstname $lastname";
+                     $directory_name = strtoupper($lastname) . " " . $firstname;
+                 }
+                 XDB::execute('INSERT INTO  accounts (hruid, display_name, full_name, directory_name,
+                                            email, type)
+                                    VALUES  ({?}, {?}, {?}, {?}, {?}, \'xnet\')',
+                              $hruid, $display_name, $full_name, $directory_name, $email);
                  $user = User::get($hruid);
              }
          }
  
          if ($user) {
-             XDB::execute('REPLACE INTO  group_members (uid, asso_id)
-                                 VALUES  ({?}, {?})',
+             XDB::execute('INSERT IGNORE INTO  group_members (uid, asso_id)
+                                       VALUES  ({?}, {?})',
                           $user->id(), $globals->asso('id'));
              $this->removeSubscriptionRequest($user->id());
              pl_redirect('member/' . $user->login());
              return true;
          }
  
-         $mmlist = new MMList($user, $domain);
+         $mmlist = new MMList(S::user(), $domain);
          $listes = $mmlist->get_lists($user->forlifeEmail());
  
          $may_update = may_update();
      {
          $page->changeTpl('xnetgrp/membres-del.tpl');
          $user = S::user();
 -        $uid  = S::user()->id();
 -        if (empty($uid)) {
 +        if (empty($user)) {
              return PL_NOT_FOUND;
          }
          $page->assign('self', true);
 -        $page->assign('user', $uid);
 +        $page->assign('user', $user);
  
          if (!Post::has('confirm')) {
              return;
              S::assert_xsrf_token();
          }
  
 +        $hasSingleGroup = ($user->groupCount() == 1);
 +
          if ($this->unsubscribe($user)) {
 -            $page->trigSuccess('Vous avez été désinscrit du groupe avec succès.');
 +            $page->trigSuccess('Tu as été désinscrit du groupe avec succès.');
          } else {
 -            $page->trigWarning('Vous avez été désinscrit du groupe, mais des erreurs se sont produites lors des désinscriptions des alias et des listes de diffusion.');
 +            $page->trigWarning('Tu as été désinscrit du groupe, mais des erreurs se sont produites lors des désinscriptions des alias et des listes de diffusion.');
 +        }
 +        if ($user->type == 'xnet' && $hasSingleGroup && Post::has('accountDeletion')) {
 +            XDB::execute('DELETE FROM  acounts
 +                                WHERE  uid = {?}',
 +                         $user->id());
 +            $page->trigSuccess('Ton compte a bien été supprimé.');
          }
          $page->assign('is_member', is_member(true));
      }
          if (empty($user)) {
              return PL_NOT_FOUND;
          }
 +
 +        global $globals;
 +
 +        if (!$user->inGroup($globals->asso('id'))) {
 +            pl_redirect('annuaire');
 +        }
 +
 +        $page->assign('self', false);
          $page->assign('user', $user);
  
          if (!Post::has('confirm')) {
              S::assert_xsrf_token();
          }
  
 +        $hasSingleGroup = ($user->groupCount() == 1);
 +
          if ($this->unsubscribe($user)) {
              $page->trigSuccess("{$user->fullName()} a été désinscrit du groupe&nbsp;!");
          } else {
              $page->trigWarning("{$user->fullName()} a été désinscrit du groupe, mais des erreurs subsistent&nbsp;!");
          }
 +
 +        // Either deletes or notifies site administrators if it was the last group
 +        // of a xnet account.
 +        if ($user->type == 'xnet' && $hasSingleGroup) {
 +            if ($user->state == 'pending') {
 +                // If the user has never logged in the site, we delete her account.
 +                XDB::execute('DELETE FROM  acounts
 +                                    WHERE  uid = {?}',
 +                             $user->id());
 +            } else {
 +                // It the user has already logged in the site, we notify site
 +                // administrators that there is a new xnet account without any
 +                // group.
 +                $mailer = new PlMailer('xnetgrp/unsubscription.mail.tpl');
 +                $mailer->assign('user', $user);
 +                $mailer->assign('groupId', $globals->asso('id'));
 +                $mailer->assign('groupName', $globals->asso('nom'));
 +                $mailer->send();
 +            }
 +        }
      }
  
 -    private function changeLogin(PlPage &$page, PlUser &$user, MMList &$mmlist, $login)
 +    private function changeLogin(PlPage &$page, PlUser &$user, $login)
      {
          // Search the user's uid.
          $xuser = User::getSilent($login);
      {
          global $globals;
  
 -        $page->changeTpl('xnetgrp/membres-edit.tpl');
 -
          $user = User::getSilent($user);
          if (empty($user)) {
              return PL_NOT_FOUND;
          }
  
-         $mmlist = new MMList($user, $globals->asso('mail_domain'));
 +        if (!$user->inGroup($globals->asso('id'))) {
 +            pl_redirect('annuaire');
 +        }
 +
 +        $page->changeTpl('xnetgrp/membres-edit.tpl');
 +
+         $mmlist = new MMList(S::user(), $globals->asso('mail_domain'));
  
          if (Post::has('change')) {
              S::assert_xsrf_token();
  
              // Convert user status to X
              if (!Post::blank('login_X')) {
 -                $forlife = $this->changeLogin($page, $user, $mmlist, Post::t('login_X'));
 +                $forlife = $this->changeLogin($page, $user, Post::t('login_X'));
                  if ($forlife) {
                      pl_redirect('member/' . $forlife);
                  }
                             Post::t('full_name'), Post::t('directory_name'), Post::t('display_name'),
                             (Post::t('sex') == 'male') ? 'male' : 'female', Post::t('email'),
                             (Post::t('type') == 'xnet') ? 'xnet' : 'virtual', $user->id());
 +                // If user is of type xnet and new password is given.
 +                if (!Post::blank('pwhash') && Post::t('type') == 'xnet') {
 +                    XDB::query('UPDATE  accounts
 +                                   SET  password = {?}
 +                                 WHERE  uid = {?}',
 +                               Post::t('pwhash'), $user->id());
 +                }
              } else if (!$user->perms) {
                  XDB::query('UPDATE  accounts
                                 SET  email = {?}
                             Post::t('email'), $user->id());
              }
              if (XDB::affectedRows()) {
 -                $page->trigSuccess('Données de l\'utilisateur mise à jour.');
 +                $page->trigSuccess('Données de l\'utilisateur mises à jour.');
              }
  
              // Update group params for user
              $perms = Post::v('group_perms');
              $comm  = Post::t('comm');
-             if ($user->group_perms != $perms || $user->group_comm != $comm) {
+             $position = (Post::t('group_position') == '') ? null : Post::v('group_position');
+             if ($user->group_perms != $perms || $user->group_comm != $comm || $user->group_position != $position) {
                  XDB::query('UPDATE  group_members
-                                SET  perms = {?}, comm = {?}
+                                SET  perms = {?}, comm = {?}, position = {?}
                               WHERE  uid = {?} AND asso_id = {?}',
-                             ($perms == 'admin') ? 'admin' : 'membre', $comm,
+                             ($perms == 'admin') ? 'admin' : 'membre', $comm, $position,
                              $user->id(), $globals->asso('id'));
                  if (XDB::affectedRows()) {
                      if ($perms != $user->group_perms) {
                      if ($comm != $user->group_comm) {
                          $page->trigSuccess('Commentaire mis à jour.');
                      }
+                     if ($position != $user->group_position) {
+                         $page->trigSuccess('Poste mis à jour.');
+                     }
                  }
              }
  
              }
          }
  
+         $res = XDB::rawFetchAllAssoc('SHOW COLUMNS FROM group_members LIKE \'position\'');
+         $positions = str_replace(array('enum(', ')', '\''), '', $res[0]['Type']);
 +        $page->addJsLink('password.js');
 +        $page->assign('onlyGroup', ($user->groupCount() == 1));
          $page->assign('user', $user);
          $page->assign('listes', $mmlist->get_lists($user->forlifeEmail()));
          $page->assign('alias', $user->emailAliases($globals->asso('mail_domain'), 'user', true));
+         $page->assign('positions', explode(',', $positions));
      }
  
-     function handler_rss(&$page, $user = null, $hash = null)
+     function handler_rss(PlPage& $page, PlUser& $user)
      {
          global $globals;
          $page->assign('asso', $globals->asso());
  
          $this->load('feed.inc.php');
          $feed = new XnetGrpEventFeed();
-         return $feed->run($page, $user, $hash, false);
+         return $feed->run($page, $user, false);
      }
  
      private function upload_image(PlPage &$page, PlUpload &$upload)
                                   $aid, $imgtype, $imgx, $imgy, $upload->getContents());
                  }
                  if ($art['xorg']) {
-                     require_once('validations.inc.php');
                      $article = new EvtReq("[{$globals->asso('nom')}] " . $art['titre'], $fulltext,
                                      $art['promo_min'], $art['promo_max'], $art['expiration'], "", S::user(),
                                      $upload);
                      $upload->rm();
                  }
                  if ($art['nl']) {
-                     require_once('validations.inc.php');
                      $article = new NLReq(S::user(), $globals->asso('nom') . " : " .$art['titre'],
                                           $art['texte'], $art['contact_html']);
                      $article->submit();
                             $art['id'], $globals->asso('id'));
                  if ($art['photo'] && $upload->exists()) {
                      list($imgx, $imgy, $imgtype) = $upload->imageInfo();
-                     XDB::execute("REPLACE INTO  group_announces_photo
-                                            SET  eid = {?}, attachmime = {?}, x = {?}, y = {?}, attach = {?}",
-                                  $aid, $imgtype, $imgx, $imgy, $upload->getContents());
+                     XDB::execute('INSERT INTO  group_announces_photo (eid, attachmime, attach, x, y)
+                                        VALUES  ({?}, {?}, {?}, {?}, {?})
+                       ON DUPLICATE KEY UPDATE  attachmime = VALUES(attachmime), attach = VALUES(attach), x = VALUES(x), y = VALUES(y)',
+                                  $aid, $imgtype, $upload->getContents(), $imgx, $imgy);
                      $upload->rm();
                  }
              }
diff --combined templates/xnet/skin.tpl
@@@ -1,6 -1,6 +1,6 @@@
  {**************************************************************************}
  {*                                                                        *}
- {*  Copyright (C) 2003-2010 Polytechnique.org                             *}
+ {*  Copyright (C) 2003-2011 Polytechnique.org                             *}
  {*  http://opensource.polytechnique.org/                                  *}
  {*                                                                        *}
  {*  This program is free software; you can redistribute it and/or modify  *}
            {/foreach}
            {if t($asso)}{assign var=asso_id value=$asso->id}{/if}
            {if t($smarty.session.suid)}{assign var=suid value=true}{else}{assign var=suid value=false}{/if}
 -          {if $asso && $is_admin ||
 +          {if t($asso) && $is_admin ||
                        ($suid && ($smarty.session.suid.perms->hasFlag('admin') ||
                                                  $smarty.session.suid.may_update[$asso_id]))}
            <h1>Voir le site comme&hellip;</h1>
              - <a href="send_bug/{ $smarty.server.REQUEST_URI }" class="popup_840x600">signaler un bug</a>
            {/if}
            <br />
-           Plat/al {#globals.version#} - © Copyright 1999-2010 <a href="http://x-org.polytechnique.org/">Association Polytechnique.org</a>
+           Plat/al {#globals.version#} - © Copyright 1999-2011 <a href="http://x-org.polytechnique.org/">Association Polytechnique.org</a>
            <div class="pem">
              <a href="{$globals->baseurl}/pem/{$platal->pl_self()|replace:'/':'_'}/200">Liste1</a>
              <a href="{$globals->baseurl}/pem/{$platal->pl_self()|replace:'/':'_'}/400">Liste2</a>
@@@ -1,6 -1,6 +1,6 @@@
  {**************************************************************************}
  {*                                                                        *}
- {*  Copyright (C) 2003-2010 Polytechnique.org                             *}
+ {*  Copyright (C) 2003-2011 Polytechnique.org                             *}
  {*  http://opensource.polytechnique.org/                                  *}
  {*                                                                        *}
  {*  This program is free software; you can redistribute it and/or modify  *}
@@@ -20,7 -20,7 +20,7 @@@
  {*                                                                        *}
  {**************************************************************************}
  
 -{if $smarty.post.confirm}
 +{if t($smarty.post.confirm)}
  
  <p class="descr">
  {if !$self}
      {if $self}
      Êtes-vous sûr de vouloir vous désinscrire du groupe {$asso->nom} et de toutes
      les listes de diffusion associées&nbsp;?
 +    {if $user->type eq 'xnet' && $user->groupCount() eq 1}
 +    <br />C'est le seul groupe auquel tu es actuellement inscrit sur polytechnique.net.
 +    Si, malgré cela, tu souhaites garder ton accès à Polytechnique.net, décoche la case
 +    ci-dessous.<br />
 +    <label><input type="checkbox" name="accountDeletion" checked="checked" />Supprimer mon compte.</label>
 +    {/if}
      {else}
      Êtes-vous sûr de vouloir supprimer {$user->fullName()} du groupe,
      lui retirer tous les droits associés à son statut de membre
      et le désabonner de toutes les listes de diffusion du groupe&nbsp;?
      {/if}
      </p>
 -    <input type='submit' name='confirm' value='Oui, je {if $self}me{else}le{/if} désinscris complètement du groupe !' />
 +    <input type="submit" name="confirm" value="Oui, je {if $self}me{else}le{/if} désinscris complètement du groupe !" />
    </div>
  </form>
  
@@@ -1,6 -1,6 +1,6 @@@
  {**************************************************************************}
  {*                                                                        *}
- {*  Copyright (C) 2003-2010 Polytechnique.org                             *}
+ {*  Copyright (C) 2003-2011 Polytechnique.org                             *}
  {*  http://opensource.polytechnique.org/                                  *}
  {*                                                                        *}
  {*  This program is free software; you can redistribute it and/or modify  *}
@@@ -28,7 -28,6 +28,7 @@@
        document.getElementById('prenom').style.display = state;
        document.getElementById('sexe').style.display = state;
        document.getElementById('make_X').style.display = state;
 +      document.getElementById('password').style.display = state;
    }
  
    function showXInput(box)
  <form method="post" action="{$platal->ns}member/{$platal->argv[1]}">
    {xsrf_token_field}
    <table cellpadding="0" cellspacing="0" class='tinybicol'>
+     {if hasPerm('admin')}
+     <tr class="pair">
+       <td class="titre">
+         Identifiant unique&nbsp;:
+       </td>
+       <td>
+         {$user->hruid}
+         <a href="https://www.polytechnique.org/admin/user/{$user->hruid}">{icon name="user_edit" title="Administer"}</a>
+       </td>
+     </tr>
+     {/if}
      <tr class="pair">
        <td class="titre">
          Permissions&nbsp;:
          </select>
        </td>
      </tr>
+     <tr class="pair">
+       <td class="titre">
+         Poste&nbsp;:
+       </td>
+       <td>
+         <select name="group_position">
+           <option value=""{if $user->group_position eq ''} selected="selected"{/if}></option>
+           {foreach from=$positions item=position}
+           <option value="{$position}"{if $user->group_position eq $position} selected="selected"{/if}>{$position}</option>
+           {/foreach}
+         </select>
+       </td>
+     </tr>
      {if !$user->profile()}
      <tr class="impair">
        <td class="titre">
        </td>
      </tr>
      {if $user->type eq 'xnet'}
 +    <tr class="impair" id="password">
 +      <td class="titre">Mot de passe&nbsp;:</td>
 +      <td>
 +        <div style="float: left">
 +          <input type="text" name="new_plain_password" size="10" maxlength="256" value="********" />
 +          <input type="hidden" name="pwhash" value="" />
 +        </div>
 +        <div style="float: left; margin-top: 5px;">
 +          {checkpasswd prompt="new_plain_password" submit="dummy_none"}
 +        </div>
 +        {if !$onlyGroup}
 +        <div style="clear: both">
 +          <small class="error">
 +            Attention, cet utilisateur est inscrit à d'autres groupes, changer son mot de passe modifiera aussi ses accès aux autres groupes.
 +          </small>
 +        </div>
 +        {/if}
 +      </td>
 +    </tr>
      <tr id="make_X">
        <td colspan="2">
          <span id="make_X_cb">
  
    <div class="center">
      <br />
 -    <input type="submit" name='change' value="Valider ces changements" />
 +    <input type="submit" name='change' value="Valider ces changements" onclick="return hashResponse('new_plain_password', false, false);" />
      &nbsp;
      <input type="reset" value="Annuler ces changements" />
    </div>