Merge remote branch 'origin/xorg/maint' into xorg/master
authorFlorent Bruneau <florent.bruneau@polytechnique.org>
Mon, 31 Jan 2011 19:21:13 +0000 (20:21 +0100)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Mon, 31 Jan 2011 19:21:22 +0000 (20:21 +0100)
Conflicts:
ChangeLog
Makefile
classes/userfilter/conditions.inc.php
core
htdocs/javascript/do_challenge_response.js
htdocs/javascript/password.js
htdocs/javascript/wiki.js
htdocs/javascript/xorg.js
include/massmailer.inc.php
modules/admin.php
modules/api.php
modules/axletter/axletter.inc.php
templates/admin/user.tpl
templates/axletter/admin.tpl
templates/axletter/edit.tpl
templates/axletter/index.tpl
templates/axletter/show.tpl
templates/emails/antispam.tpl
templates/emails/index.tpl
templates/events/form.tpl
templates/gadgets/ig-skin.tpl
templates/lists/index.tpl
templates/profile/fiche_referent.tpl
templates/search/index.tpl
templates/skin/common.bandeau.head.tpl
templates/skin/common.title.header.tpl
templates/survey/show_textarea.tpl
templates/survey/success.tpl

Signed-off-by: Florent Bruneau <florent.bruneau@polytechnique.org>
14 files changed:
1  2 
ChangeLog
Makefile
classes/group.php
classes/user.php
classes/xnetpage.php
classes/xorg.php
core
htdocs/javascript/xorg.js
include/ufbuilder.inc.php
modules/admin.php
modules/profile.php
modules/profile/jobs.inc.php
modules/register.php
templates/admin/index.tpl

diff --combined ChangeLog
+++ b/ChangeLog
@@@ -1,47 -1,5 +1,47 @@@
  ================================================================================
- VERSION 1.0.2                                                         XX XX XXXX
 +VERSION 1.1.0                                                         XX XX XXXX
 +
 +Bug/Wish:
 +
 +    * Admin:
 +      - #1320: Add administrations pages for country and language edition  -JAC
 +      - #1371: Allows email edition before mailing list validation         -JAC
 +
 +    * Carnet:
 +      - #1139: Fixes contact pdf export with pictures                      -JAC
 +
 +    * Core:
 +      - #1040: Adapts login page for non-X users                           -FRU
 +      - #1325: Fixes csv downloading with IE8                              -JAC
 +
 +    * Emails:
 +      - #1201: Increases allowed email size                                -JAC
 +
 +    * Payments:
 +      - #1314,1295: Finally fix "has paid" lists                           -Xel
 +
 +    * Profile:
 +      - #1288: Adds explaination about job keywords in profile edition     -JAC
 +      - #1294: Fixes email in job information                              -JAC
 +      - #1322: Removes duplicated city and postal code in vcard addresses  -JAC
 +      - #1323: Displays email aliases in vcards when public                -JAC
 +
 +    * Search:
 +      - #1283: Restricts second operation on promo search                  -Car
 +      - #1312: Adds search on subadministrativearea                        -JAC
 +      - #1313: Fixes advanced form js in IE7                               -Car
 +
 +    * Xnet:
 +      - #1347: Fixes menu when changing rights in Xnet                     -JAC
 +
 +    * XnetEvent:
 +      - #1233: Enables event subscribtion notification                     -JAC
 +
 +    * XnetGrp:
 +      - #1230: Adds custom welcome message for group subscription          -JAC
 +
 +================================================================================
+ VERSION 1.0.2                                                         31 01 2011
  
  Bug/Wish:
  
          - #1352: Fixes csv downloading with IE8.                           -JAC
          - #1355: Mode with propagation of the skinning mode                -FRU
  
+     * Newsletter:
+         - #774:  Enable custom newsletters for groups                      -XEL
+         - #1047: Filter the list of previous AXletters                     -XEL
      * Payments:
          - #1290: Fixes display of payments on xnet                         -JAC
          - #1318: Fixes PHP errors in PayPal payments                       -JAC
@@@ -74,7 -36,7 +78,7 @@@
  
      * Search:
          - #1365: Outputs csv of postal addresses for advanced search       -JAC
-         - #1366: Adds 'by AX id' advanced search for admins                -Xel
+         - #1366: Adds 'by AX id' advanced search for admins                -XEL
  
      * XnetEvent:
          - #1373: Fixes acceptance of non member in xnet events.            -JAC
@@@ -116,7 -78,7 +120,7 @@@ Bug/Wish
  
      * Search:
          - #1177: Clean url in advanced search                              -Car
-         - #1251: More intuitive networking search                          -Xel
+         - #1251: More intuitive networking search                          -XEL
          - #1283: Restrict second operation on promo search                 -Car
  
      * Survey:
@@@ -147,7 -109,7 +151,7 @@@ From 1.0.0 branch
          - Fix promotion_ml reminder                                        -FRU
  
      * Forums:
-         - #1247: Fix RSS feed                                              -Xel
+         - #1247: Fix RSS feed                                              -XEL
  
      * Lists:
          - Fix inclusion of banana for achives and moderation preview       -FRU
diff --combined Makefile
+++ b/Makefile
@@@ -73,10 -73,7 +73,10 @@@ htdocs/.htaccess: htdocs/.htaccess.in M
  ##
  ## static content
  ##
 -static: htdocs/javascript@VERSION
 +static: htdocs/javascript/core.js htdocs/javascript@VERSION
 +
 +htdocs/javascript/core.js:
 +      cd htdocs/javascript/ && ln -s ../../core/htdocs/javascript/core.js
  
  %@VERSION: % Makefile ChangeLog
        cd $< && rm -f $(VERSION) && ln -sf . $(VERSION)
@@@ -122,10 -119,10 +122,10 @@@ wiki/pub/skins/empty
  
  get-wiki:
        @if ! test -d wiki; then                                          \
 -          wget http://www.pmwiki.org/pub/pmwiki/pmwiki-latest.tgz;      \
 -          tar -xzvf pmwiki-latest.tgz;                                  \
 -          rm pmwiki-latest.tgz;                                         \
 -          mv pmwiki-* wiki;                                             \
 +              wget http://www.pmwiki.org/pub/pmwiki/pmwiki-latest.tgz;      \
 +              tar -xzvf pmwiki-latest.tgz;                                  \
 +              rm pmwiki-latest.tgz;                                         \
 +              mv pmwiki-* wiki;                                             \
        fi
  
  ##
  openid: get-openid spool/openid/store
  
  # There is no obvious way to automatically use the latest version
 -OPENID_VERSION = 2.1.3
 +OPENID_VERSION = 2.2.2
 +OPENID_COMMIT  = 782224d
  get-openid:
        @if ! test -d include/Auth; then                                  \
 -          wget http://openidenabled.com/files/php-openid/packages/php-openid-$(OPENID_VERSION).tar.bz2; \
 -          tar -xjf php-openid-$(OPENID_VERSION).tar.bz2;                \
 -          mv php-openid-$(OPENID_VERSION)/Auth include/;                \
 -          rm php-openid-$(OPENID_VERSION).tar.bz2;                      \
 -          rm -r php-openid-$(OPENID_VERSION);                           \
 +              wget --no-check-certificate                                   \
 +                      https://github.com/openid/php-openid/tarball/$(OPENID_VERSION) \
 +                      -O php-openid-$(OPENID_VERSION).tar.gz; \
 +              tar -xzf php-openid-$(OPENID_VERSION).tar.gz;                \
 +              mv openid-php-openid-$(OPENID_COMMIT)/Auth include/;                \
 +              rm php-openid-$(OPENID_VERSION).tar.gz;                      \
 +              rm -r openid-php-openid-$(OPENID_COMMIT);                           \
        fi
  
  spool/openid/store:
@@@ -184,7 -178,7 +184,7 @@@ JQUERY_PLUGINS=color for
  JQUERY_PLUGINS_PATHES=$(addprefix htdocs/javascript/jquery.,$(addsuffix .js,$(JQUERY_PLUGINS)))
  
  JQUERY_UI_VERSION=1.8.7
 -JQUERY_UI=core tabs widget
 +JQUERY_UI=core widget tabs datepicker
  JQUERY_UI_PATHES=$(addprefix htdocs/javascript/jquery.ui.,$(addsuffix .js,$(JQUERY_UI)))
  
  JQUERY_TMPL_VERSION=vBeta1.0.0
@@@ -196,13 -190,7 +196,13 @@@ JSTREE_PATH=htdocs/javascript/jquery.js
  # TODO: jquery.autocomplete.js should rather be downloaded from an official source. The issue
  # is that the version we use is not available anymore on the Internet, and the latest version
  # we could use is not backward compatible with our current code.
 -jquery: htdocs/javascript/jquery.js $(JQUERY_PLUGINS_PATHES) $(JQUERY_UI_PATHES) $(JQUERY_TMPL_PATH) $(JSTREE_PATH)
 +jquery: htdocs/javascript/jquery.xorg.js htdocs/javascript/jquery.ui.xorg.js $(JSTREE_PATH)
 +
 +htdocs/javascript/jquery.xorg.js: htdocs/javascript/jquery.js $(JQUERY_PLUGINS_PATHES) $(JQUERY_TMPL_PATH) htdocs/javascript/jquery.autocomplete.js
 +      cat $^ > $@
 +
 +htdocs/javascript/jquery.ui.xorg.js: $(JQUERY_UI_PATHES) htdocs/javascript/jquery.ui.datepicker-fr.js
 +      cat $^ > $@
  
  htdocs/javascript/jquery-$(JQUERY_VERSION).min.js: DOWNLOAD_SRC = http://jquery.com/src/$(@F)
  htdocs/javascript/jquery-$(JQUERY_VERSION).min.js:
@@@ -221,21 -209,17 +221,29 @@@ htdocs/javascript/jquery.ui-$(JQUERY_UI
  htdocs/javascript/jquery.ui-$(JQUERY_UI_VERSION).%.js:
        @$(download)
  
 -$(JQUERY_UI_PATHES): htdocs/javascript/jquery.ui.%.js: htdocs/javascript/jquery.ui-$(JQUERY_UI_VERSION).%.js
 +htdocs/javascript/jquery.ui-$(JQUERY_UI_VERSION).datepicker-fr.js: DOWNLOAD_SRC = http://jquery-ui.googlecode.com/svn/tags/$(JQUERY_UI_VERSION)/ui/minified/i18n/jquery.ui.datepicker-fr.min.js
 +htdocs/javascript/jquery.ui-$(JQUERY_UI_VERSION).datepicker-fr.js:
 +      @$(download)
 +
 +$(JQUERY_UI_PATHES) htdocs/javascript/jquery.ui.datepicker-fr.js: htdocs/javascript/jquery.ui.%.js: htdocs/javascript/jquery.ui-$(JQUERY_UI_VERSION).%.js
 +      ln -snf $(<F) $@
 +
 +htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js: DOWNLOAD_SRC = https://github.com/jquery/jquery-tmpl/raw/$(JQUERY_TMPL_VERSION)/jquery.tmpl.js --no-check-certificate
 +htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js:
 +      @-rm htdocs/javascript/jquery.tmpl*.js
 +      @$(download)
 +
 +$(JQUERY_TMPL_PATH): htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js
        ln -snf $(<F) $@
  
+ htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js: DOWNLOAD_SRC = https://github.com/jquery/jquery-tmpl/raw/$(JQUERY_TMPL_VERSION)/jquery.tmpl.min.js --no-check-certificate
+ htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js:
+       @-rm htdocs/javascript/jquery.tmpl*.js
+       @$(download)
+ $(JQUERY_TMPL_PATH): htdocs/javascript/jquery.tmpl-$(JQUERY_TMPL_VERSION).js
+       ln -snf $(<F) $@
  $(JSTREE_PATH):
        rm -f htdocs/javascript/jquery.jstree-*.js
        mkdir spool/tmp/jstree
diff --combined classes/group.php
@@@ -37,9 -37,6 +37,9 @@@ class Grou
          }
          $this->id = intval($this->data['id']);
          $this->shortname = $this->data['diminutif'];
 +        if (!is_null($this->axDate)) {
 +            $this->axDate = format_datetime($this->axDate, '%d/%m/%Y');
 +        }
      }
  
      public function __get($name)
          }
          $res = XDB::query('SELECT  a.*, d.nom AS domnom,
                                     FIND_IN_SET(\'wiki_desc\', a.flags) AS wiki_desc,
-                                    FIND_IN_SET(\'notif_unsub\', a.flags) AS notif_unsub
+                                    FIND_IN_SET(\'notif_unsub\', a.flags) AS notif_unsub,
+                                    (nls.id IS NOT NULL) AS has_nl
                               FROM  groups AS a
                          LEFT JOIN  group_dom  AS d ON d.id = a.dom
+                         LEFT JOIN  newsletters AS nls ON (nls.group_id = a.id)
                              WHERE  ' . $where);
          if ($res->numRows() != 1) {
              if ($can_be_shortname && (is_int($id) || ctype_digit($id))) {
              }
              return null;
          }
 -        return new Group($res->fetchOneAssoc());
 +        $data = $res->fetchOneAssoc();
 +        $positions = XDB::fetchAllAssoc('SELECT  position, uid
 +                                           FROM  group_members
 +                                          WHERE  asso_id = {?} AND position IS NOT NULL
 +                                       ORDER BY  position',
 +                                        $data['id']);
 +        return new Group(array_merge($data, array('positions' => $positions)));
      }
  }
  
diff --combined classes/user.php
  
  class User extends PlUser
  {
 -    const PERM_GROUPS            = 'groups';
 -    const PERM_MAIL              = 'mail';
 +    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;
              $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);
       * Clears a user.
       *  *always deletes in: account_lost_passwords, register_marketing,
       *      register_pending, register_subs, watch_nonins, watch, watch_promo
-      *  *always keeps in: account_types, accounts, aliases, axletter_ins, carvas,
+      *  *always keeps in: account_types, accounts, aliases, carvas,
       *      group_members, homonyms, newsletter_ins, register_mstats,
       *  *deletes if $clearAll: account_auth_openid, announce_read, contacts,
       *      email_options, email_send_save, emails, forum_innd, forum_profiles,
diff --combined classes/xnetpage.php
@@@ -41,9 -41,9 +41,9 @@@ class XnetPage extends PlPag
              $this->assign('is_admin', may_update());
              $this->assign('is_member', is_member());
          }
 -        $this->addJsLink('jquery.js');
 +        $this->addJsLink('jquery.xorg.js');
          $this->addJsLink('overlib.js');
 -        $this->addJsLink('wiki.js');
 +        $this->addJsLink('core.js');
          $this->addJsLink('xorg.js');
          $this->setTitle('Les associations polytechniciennes');
      }
                  if ($globals->asso('mail_domain')) {
                      $sub['listes de diffusion'] = "$dim/lists";
                  }
+                 if ($globals->asso('has_nl')) {
+                     $sub['newsletter'] = "$dim/nl";
+                 }
              }
              $sub['événement'] = "$dim/events";
              if ($perms->hasFlag('groupmember')) {
diff --combined classes/xorg.php
@@@ -26,9 -26,9 +26,9 @@@ class Xorg extends Plata
          parent::__construct('auth', 'carnet', 'email', 'events', 'forums',
                              'lists', 'marketing', 'payment', 'platal',
                              'profile', 'register', 'search', 'stats', 'admin',
-                             'newsletter', 'axletter', 'bandeau', 'survey',
+                             'newsletter', 'axletter', 'epletter', 'bandeau', 'survey',
                              'fusionax', 'gadgets', 'googleapps', 'poison',
 -                            'openid', 'reminder');
 +                            'openid', 'reminder', 'api');
      }
  
      public function find_hook()
          header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
          if (S::logged()) {
              $page->changeTpl('core/password_prompt_logged.tpl');
 -            $page->addJsLink('do_challenge_response_logged.js');
          } else {
              $page->changeTpl('core/password_prompt.tpl');
 -            $page->addJsLink('do_challenge_response.js');
          }
          $page->assign_by_ref('platal', $this);
          $page->run();
diff --combined core
--- 1/core
--- 2/core
+++ b/core
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit a21742678e53db58f3d406e355bf0b60fcd46e19
 -Subproject commit 9261ede012d3edda638f614494df31e876e8d758
++Subproject commit 9007d4955f2487e82c0f9f4059e1f6b450bdb528
   *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
   ***************************************************************************/
  
 -var is_IE       = $.browser.msie;
 -
  // {{{ function getNow()
  var days   = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
  var months = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet',
 -              'août', 'septembre', 'octobre', 'novembre', 'décembre']
 +              'août', 'septembre', 'octobre', 'novembre', 'décembre'];
  
  function getNow() {
      var dt = new Date();
      var mh = dt.getMonth();
      var wd = dt.getDate();
      var yr = dt.getYear();
 -    if (yr<1000) yr += 1900;
      var hr = dt.getHours();
      var mi = dt.getMinutes();
 +    var se = dt.getSeconds();
 +
 +    if (yr<1000) {
 +        yr += 1900;
 +    }
      if (mi < 10) {
          mi = '0' + mi;
      }
 -    var se = dt.getSeconds();
      if (se < 10) {
          se = '0' + se;
      }
@@@ -52,8 -51,8 +52,8 @@@
  
  function canAddSearchEngine()
  {
 -    if (((typeof window.sidebar == "object") && $.isFunction(window.sidebar.addSearchEngine))
 -        || ((typeof window.external == "object") && $.isFunction(window.external.AddSearchProvider))) {
 +    if (((typeof window.sidebar === "object") && $.isFunction(window.sidebar.addSearchEngine))
 +        || ((typeof window.external === "object") && $.isFunction(window.external.AddSearchProvider))) {
          return true;
      }
      return false;
@@@ -62,7 -61,7 +62,7 @@@
  function addSearchEngine()
  {
      var searchURI = "http://www.polytechnique.org/xorg.opensearch.xml";
 -    if ((typeof window.sidebar == "object") && $.isFunction(window.sidebar.addSearchEngine)) {
 +    if ((typeof window.sidebar === "object") && $.isFunction(window.sidebar.addSearchEngine)) {
          window.sidebar.addSearchEngine(
              searchURI,
              "http://www.polytechnique.org/images/xorg.png",
  }
  
  // }}}
 -// {{{ dynpost()
 -
 -function dynpost(action, values)
 -{
 -    var form = document.createElement('form');
 -    form.action = action;
 -    form.method = 'post';
 -
 -    $('body').get(0).appendChild(form);
 -
 -    for (var k in values) {
 -        var input = document.createElement('input');
 -        input.type = 'hidden';
 -        input.name = k;
 -        input.value = values[k];
 -        form.appendChild(input);
 -    }
 -
 -    form.submit();
 -}
 -
 -
 -function dynpostkv(action, k, v)
 -{
 -    var dict = {};
 -    dict[k] = v;
 -    dynpost(action, dict);
 -}
 -
 -// }}}
 -// {{{ function RegExp.escape()
 -
 -RegExp.escape = function(text) {
 -  if (!arguments.callee.sRE) {
 -    var specials = [
 -      '/', '.', '*', '+', '?', '|',
 -      '(', ')', '[', ']', '{', '}',
 -      '\\', '^' , '$'
 -    ];
 -    arguments.callee.sRE = new RegExp(
 -      '(\\' + specials.join('|\\') + ')', 'g'
 -    );
 -  }
 -  return text.replace(arguments.callee.sRE, '\\$1');
 -}
 -
 -// }}}
  
  /***************************************************************************
   * POPUP THINGS
  
  // {{{ function goodiesPopup()
  
 -var __goodies_active = true;
 -
 -var __goodies_ical = {
 -    default_title: 'Calendrier iCal',
 -    sites: [
 -        {'url_prefix': '',
 -         'img': 'images/icons/calendar_view_day.gif',
 -         'title': 'Calendrier iCal'},
 -        {'url_prefix': 'http://www.google.com/calendar/render?cid=',
 -         'img': 'images/goodies/add-google-calendar.gif',
 -         'title': 'Ajouter à Google Calendar'},
 -        {'url_prefix': 'https://www.google.com/calendar/hosted/polytechnique.org/render?cid=',
 -         'img': 'images/goodies/add-google-calendar.gif',
 -         'title': 'Ajouter à Google Apps / Calendar'}
 -    ]
 -};
 -
 -var __goodies_rss = {
 -    default_title: 'Fils RSS',
 -    sites: [
 -        {'url_prefix': '',
 -         'img': 'images/icons/feed.gif',
 -         'title': 'Fil rss'},
 -        {'url_prefix': 'http://fusion.google.com/add?feedurl=',
 -         'img': 'images/goodies/add-google.gif',
 -         'alt': 'Add to Google',
 -         'title': 'Ajouter à iGoogle/Google Reader'},
 -        {'url_prefix': 'http://www.netvibes.com/subscribe.php?url=',
 -         'img': 'images/goodies/add-netvibes.gif',
 -         'title': 'Ajouter à Netvibes'},
 -        {'url_prefix': 'http://add.my.yahoo.com/content?.intl=fr&url=',
 -         'img': 'images/goodies/add-yahoo.gif',
 -         'alt': 'Add to My Yahoo!',
 -         'title': 'Ajouter à My Yahoo!'}
 -    ]
 -};
 -
 -function disableGoodiesPopups() {
 -    __goodies_active = false;
 -}
 -
 -function goodiesPopup(node, goodies) {
 -    var text = '<div style="text-align: center; line-height: 2.2">';
 -    for (var site in goodies.sites) {
 -        var entry = goodies.sites[site];
 -        var s_alt   = entry["alt"] ? entry["alt"] : "";
 -        var s_img   = entry["img"];
 -        var s_title = entry["title"] ? entry["title"] : "";
 -        var s_url   = entry["url_prefix"].length > 0 ? entry["url_prefix"] + escape(this.href) : this.href;
 -
 -        text += '<a href="' + s_url + '"><img src="' + s_img + '" title="' + s_title + '" alt="' + s_alt + '"></a><br />';
 -    }
 -    text += '<a href="https://www.polytechnique.org/Xorg/Goodies">Plus de bonus</a> ...</div>';
 +(function($) {
 +    var goodies = {
 +        ical: {
 +            default_title: 'Calendrier iCal',
 +            sites: [
 +                {url_prefix: '',
 +                 img: 'images/icons/calendar_view_day.gif',
 +                 title: 'Calendrier iCal'},
 +                {url_prefix: 'http://www.google.com/calendar/render?cid=',
 +                 img: 'images/goodies/add-google-calendar.gif',
 +                 title: 'Ajouter à Google Calendar'},
 +                {url_prefix: 'https://www.google.com/calendar/hosted/polytechnique.org/render?cid=',
 +                 img: 'images/goodies/add-google-calendar.gif',
 +                 title: 'Ajouter à Google Apps / Calendar'}
 +            ]
 +        },
  
 -    var title = node.title ? node.title : goodies.default_title;
 +        rss: {
 +            default_title: 'Fils RSS',
 +            sites: [
 +                {url_prefix: '',
 +                 img: 'images/icons/feed.gif',
 +                 title: 'Fil rss'},
 +                {url_prefix: 'http://fusion.google.com/add?feedurl=',
 +                 img: 'images/goodies/add-google.gif',
 +                 alt: 'Add to Google',
 +                 title: 'Ajouter à iGoogle/Google Reader'},
 +                {url_prefix: 'http://www.netvibes.com/subscribe.php?url=',
 +                 img: 'images/goodies/add-netvibes.gif',
 +                 title: 'Ajouter à Netvibes'},
 +                {url_prefix: 'http://add.my.yahoo.com/content?.intl=fr&url=',
 +                 img: 'images/goodies/add-yahoo.gif',
 +                 alt: 'Add to My Yahoo!',
 +                 title: 'Ajouter à My Yahoo!'}
 +            ]
 +        }
 +    };
  
 -    $(node)
 -        .mouseover(
 -            function() {
 -                if (__goodies_active) {
 -                    return overlib(text, CAPTION, title, CLOSETEXT, 'Fermer', DELAY, 800, STICKY, WIDTH, 150);
 +    $.fn.extend({
 +        goodiesPopup: function goodiesPopup(type) {
 +            var text = '<div style="text-align: center; line-height: 2.2">';
 +            var site;
 +            var entry;
 +            var s_alt;
 +            var s_img;
 +            var s_title;
 +            var s_url;
 +
 +            for (site in goodies[type].sites) {
 +                entry = goodies[type].sites[site];
 +                s_alt   = entry.alt || "";
 +                s_img   = entry.img;
 +                s_title = entry.title || "";
 +                s_url   = entry.url_prefix.length > 0 ? entry.url_prefix + escape(this.href) : this.href;
 +
 +                text += '<a href="' + s_url + '"><img src="' + s_img + '" title="' + s_title + '" alt="' + s_alt + '"></a><br />';
                  }
 -            }
 -        )
 -        .mouseout(nd);
 -}
 +            text += '<a href="https://www.polytechnique.org/Xorg/Goodies">Plus de bonus</a> ...</div>';
 +
 +            return this.overlib({
 +                text: text,
 +                caption: this.attr('title') || goodies.default_title,
 +                close_text: 'Fermer',
 +                delay: 800,
 +                sticky: true,
 +                width: 150
 +            });
 +        }
 +    });
 +}(jQuery));
  
  // }}}
  // {{{ function auto_links()
  function auto_links() {
      var url  = document.URL;
      var fqdn = url.replace(/^https?:\/\/([^\/]*)\/.*$/,'$1');
 -    var light = (url.indexOf('display=light') > url.indexOf('?'));
 -    var resource_page = (url.indexOf('rss') > -1 || url.indexOf('ical') > -1);
 +    var light = url.indexOf('display=light') > url.indexOf('?');
 +    var resource_page = url.contains('rss') || url.contains('ical');
  
      $("a,link").each(function(i) {
          var node = $(this);
          var href = this.href;
 +        var matches;
 +        var rss;
 +        var ical;
 +
          if(!href || node.hasClass('xdx')
 -           || href.indexOf('mailto:') > -1 || href.indexOf('javascript:') > -1) {
 +           || href.startsWith('mailto:') || href.startsWith('javascript:')) {
              return;
          }
 -        if ((href.indexOf(fqdn) < 0 && this.className.indexOf('popup') < 0) || node.hasClass('popup')) {
 +        if ((!href.contains(fqdn) && !this.className.contains('popup')) || node.hasClass('popup')) {
              node.click(function () {
                  window.open(href);
                  return false;
              });
          }
 -        if (href.indexOf(fqdn) > -1 && light) {
 +        if (href.contains(fqdn) && light) {
              href = href.replace(/([^\#\?]*)\??([^\#]*)(\#.*)?/, "$1?display=light&$2$3");
              this.href = href;
          }
 -        var rss  = href.indexOf('rss') > -1;
 -        var ical = href.indexOf('ical') > -1;
 +        rss  = href.contains('rss');
 +        ical = href.contains('ical');
          if (rss || ical) {
 -            if (href.indexOf('http') < 0) {
 +            if (!href.startsWith('http')) {
                  href = 'http://' + fqdn + '/' + href;
              }
          }
 -        if (this.nodeName.toLowerCase() == 'a' && !resource_page) {
 -            if (rss && href.indexOf('prefs/rss') < 0 &&  (href.indexOf('xml') > -1 || href.indexOf('hash'))) {
 -                goodiesPopup(this, __goodies_rss);
 +        if (this.nodeName.toLowerCase() === 'a' && !resource_page) {
 +            if (rss && !href.contains('prefs/rss') && (href.contains('xml') || href.contains('hash'))) {
 +                node.goodiesPopup('rss');
              } else if (ical) {
 -                goodiesPopup(this, __goodies_ical);
 +                node.goodiesPopup('ical');
              }
          }
 -        if(matches = (/^popup_([0-9]*)x([0-9]*)$/).exec(this.className)) {
 -            var w = matches[1], h = matches[2];
 -            node.popWin(w, h);
 +        matches = /^popup_([0-9]*)x([0-9]*)$/.exec(this.className);
 +        if (matches) {
 +            node.popWin(matches[1], matches[2]);
          }
      });
      $('.popup2').popWin(840, 600);
  
  // {{{ function checkPassword
  
 +/* {{{ SHA1 Implementation */
 +
 +/*
 + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 + * in FIPS PUB 180-1
 + * Version 2.1a Copyright Paul Johnston 2000 - 2002.
 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 + * Distributed under the BSD License
 + * See http://pajhome.org.uk/crypt/md5 for details.
 + */
 +
 +/*
 + * Configurable variables. You may need to tweak these to be compatible with
 + * the server-side, but the defaults work in most cases.
 + */
 +var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
 +var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
 +var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */
 +
 +/*
 + * These are the functions you'll usually want to call
 + * They take string arguments and return either hex or base-64 encoded strings
 + */
 +function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
 +function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
 +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
 +function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
 +function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
 +function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
 +
 +/*
 + * Perform a simple self-test to see if the VM is working
 + */
 +function sha1_vm_test()
 +{
 +  return hex_sha1("abc") === "a9993e364706816aba3e25717850c26c9cd0d89d";
 +}
 +
 +/*
 + * Calculate the SHA-1 of an array of big-endian words, and a bit length
 + */
 +function core_sha1(x, len)
 +{
 +  var w, a, b, c, d, e;
 +  var olda, oldb, oldc, oldd, olde;
 +  var i, j, t;
 +
 +  /* append padding */
 +  x[len >> 5] |= 0x80 << (24 - len % 32);
 +  x[((len + 64 >> 9) << 4) + 15] = len;
 +
 +  w = Array(80);
 +  a =  1732584193;
 +  b = -271733879;
 +  c = -1732584194;
 +  d =  271733878;
 +  e = -1009589776;
 +
 +  for(i = 0; i < x.length; i += 16)
 +  {
 +    olda = a;
 +    oldb = b;
 +    oldc = c;
 +    oldd = d;
 +    olde = e;
 +
 +    for(j = 0; j < 80; j++)
 +    {
 +      if(j < 16) w[j] = x[i + j];
 +      else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
 +      t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
 +                       safe_add(safe_add(e, w[j]), sha1_kt(j)));
 +      e = d;
 +      d = c;
 +      c = rol(b, 30);
 +      b = a;
 +      a = t;
 +    }
 +
 +    a = safe_add(a, olda);
 +    b = safe_add(b, oldb);
 +    c = safe_add(c, oldc);
 +    d = safe_add(d, oldd);
 +    e = safe_add(e, olde);
 +  }
 +  return Array(a, b, c, d, e);
 +
 +}
 +
 +/*
 + * Perform the appropriate triplet combination function for the current
 + * iteration
 + */
 +function sha1_ft(t, b, c, d)
 +{
 +  if(t < 20) return (b & c) | ((~b) & d);
 +  if(t < 40) return b ^ c ^ d;
 +  if(t < 60) return (b & c) | (b & d) | (c & d);
 +  return b ^ c ^ d;
 +}
 +
 +/*
 + * Determine the appropriate additive constant for the current iteration
 + */
 +function sha1_kt(t)
 +{
 +  return (t < 20) ?  1518500249 : (t < 40) ?  1859775393 :
 +         (t < 60) ? -1894007588 : -899497514;
 +}
 +
 +/*
 + * Calculate the HMAC-SHA1 of a key and some data
 + */
 +function core_hmac_sha1(key, data)
 +{
 +  var bkey = str2binb(key);
 +  var i, ipad, opad;
 +  var hash;
 +
 +  if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);
 +
 +  ipad = Array(16);
 +  opad = Array(16);
 +  for(i = 0; i < 16; i++)
 +  {
 +    ipad[i] = bkey[i] ^ 0x36363636;
 +    opad[i] = bkey[i] ^ 0x5C5C5C5C;
 +  }
 +
 +  hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
 +  return core_sha1(opad.concat(hash), 512 + 160);
 +}
 +
 +/*
 + * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 + * to work around bugs in some JS interpreters.
 + */
 +function safe_add(x, y)
 +{
 +  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
 +  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
 +  return (msw << 16) | (lsw & 0xFFFF);
 +}
 +
 +/*
 + * Bitwise rotate a 32-bit number to the left.
 + */
 +function rol(num, cnt)
 +{
 +  return (num << cnt) | (num >>> (32 - cnt));
 +}
 +
 +/*
 + * Convert an 8-bit or 16-bit string to an array of big-endian words
 + * In 8-bit function, characters >255 have their hi-byte silently ignored.
 + */
 +function str2binb(str)
 +{
 +  var bin = Array();
 +  var mask = (1 << chrsz) - 1;
 +  var i;
 +  for(i = 0; i < str.length * chrsz; i += chrsz)
 +    bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
 +  return bin;
 +}
 +
 +/*
 + * Convert an array of big-endian words to a string
 + */
 +function binb2str(bin)
 +{
 +  var str = "";
 +  var mask = (1 << chrsz) - 1;
 +  var i;
 +  for(i = 0; i < bin.length * 32; i += chrsz)
 +    str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
 +  return str;
 +}
 +
 +/*
 + * Convert an array of big-endian words to a hex string.
 + */
 +function binb2hex(binarray)
 +{
 +  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
 +  var str = "";
 +  var i;
 +  for(i = 0; i < binarray.length * 4; i++)
 +  {
 +    str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
 +           hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
 +  }
 +  return str;
 +}
 +
 +/*
 + * Convert an array of big-endian words to a base-64 string
 + */
 +function binb2b64(binarray)
 +{
 +  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 +  var str = "";
 +  var i, j, triplet;
 +  for(i = 0; i < binarray.length * 4; i += 3)
 +  {
 +    triplet = (((binarray[i   >> 2] >> 8 * (3 -  i   %4)) & 0xFF) << 16)
 +                | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
 +                |  ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
 +    for(j = 0; j < 4; j++)
 +    {
 +      if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
 +      else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
 +    }
 +  }
 +  return str;
 +}
 +
 +/* }}} */
 +
 +function hash_encrypt(a) {
 +    return hex_sha1(a);
 +}
 +
 +var hexa_h = "0123456789abcdef";
 +
 +function dechex(a) {
 +    return hexa_h.charAt(a);
 +}
 +
 +function hexdec(a) {
 +    return hexa_h.indexOf(a);
 +}
 +
 +function hash_xor(a, b) {
 +    var c,i,j,k,d;
 +    c = "";
 +    i = a.length;
 +    j = b.length;
 +    if (i < j) {
 +        d = a; a = b; b = d;
 +        k = i; i = j; j = k;
 +    }
 +    for (k = 0; k < j; k++) {
 +        c += dechex(hexdec(a.charAt(k)) ^ hexdec(b.charAt(k)));
 +    }
 +    for (; k < i; k++) {
 +        c += a.charAt(k);
 +    }
 +    return c;
 +}
 +
  function getType(c) {
      if (c >= 'a' && c <= 'z') {
          return 1;
  
  function differentTypes(password) {
      var prev = 0;
 +    var type;
  
      for (i = 0 ; i < password.length ; ++i) {
 -        var type = getType(password.charAt(i));
 -        if (prev != 0 && prev != type) {
 +        type = getType(password.charAt(i));
 +        if (prev !== 0 && prev !== type) {
              return true;
          }
          prev = type;
@@@ -501,16 -286,15 +500,16 @@@ function passwordStrength(password) 
      var prev = 0;
      var firstType = true;
      var types = Array(0, 0, 0, 0, 0);
 +    var type;
  
      for (i = 0 ; i < password.length ; ++i) {
 -        var type = getType(password.charAt(i));
 -        if (prev != 0 && prev != type) {
 +        type = getType(password.charAt(i));
 +        if (prev !== 0 && prev !== type) {
              prop += 5;
              firstType = false;
          }
          prop += i;
 -        if (types[type] == 0 && !firstType) {
 +        if (types[type] === 0 && !firstType) {
              prop += 15;
          }
          types[type]++;
  function checkPassword(box, okLabel) {
      var password = box.value;
      var prop = passwordStrength(password);
 +    var submitButton;
  
      if (prop >= 60) {
          color = "#4f4";
                      }, 750)
             .parent().stop()
                      .animate({ backgroundColor: bgcolor }, 750);
 -    var submitButton = $(":submit[name='" + passwordprompt_submit + "']");
 +    submitButton = $(":submit[name='" + passwordprompt_submit + "']");
      if (ok && password.length >= 6 && differentTypes(password)) {
          submitButton.attr("value", okLabel);
          submitButton.removeAttr("disabled");
      }
  }
  
 +function hashResponse(password1, password2, hasConfirmation) {
 +    var pw1 = $('[name=' + password1 + ']').val();
 +    var pw2;
 +
 +    if (hasConfirmation) {
 +        pw2 = $('[name=' + password2 + ']').val();
 +        if (pw1 !== pw2) {
 +            alert("\nErreur : les deux champs ne sont pas identiques !");
 +            return false;
 +        }
 +        $('[name=' + password2 + ']').val('');
 +    } else if (pw1 === '********') {
 +        return true;
 +    }
 +
 +    if (pw1.length < 6) {
 +        alert("\nErreur : le nouveau mot de passe doit faire au moins 6 caractères !");
 +        return false;
 +    }
 +    if (!differentTypes(pw1)) {
 +        alert ("\nErreur : le nouveau mot de passe doit comporter au moins deux types de caractères parmi les suivants : lettres minuscules, lettres majuscules, chiffres, caractères spéciaux.");
 +        return false;
 +    }
 +
 +    alert("Le mot de passe que tu as rentré va être chiffré avant de nous parvenir par Internet ! Ainsi il ne circulera pas en clair.");
 +    $('[name=' + password1 + ']').val('');
 +    $('[name=pwhash]').val(hash_encrypt(pw1));
 +    return true;
 +}
 +
 +function correctUserName() {
 +    var u = document.forms.login.username;
 +    var mots;
 +
 +    // login with no space
 +    if (!u.value.contains(' ')) {
 +        return true;
 +    }
 +    mots = u.value.split(' ');
 +    // jean paul.du pont -> jean-paul.du-pont
 +    if (u.value.contains('.')) {
 +        u.value = mots.join('-');
 +        return true;
 +    }
 +    // jean dupont  -> jean.dupont
 +    if (mots.length === 2) {
 +        u.value = mots[0] + "." + mots[1];
 +        return true;
 +    }
 +    // jean dupont 2001 -> jean.dupont.2001
 +    if (mots.length === 3 && mots[2] > 1920 && mots[2] < 3000) {
 +        u.value = mots.join('.');
 +        return true;
 +    }
 +    // jean de la vallee -> jean.de-la-vallee
 +    if (mots[1].toUpperCase() === 'DE') {
 +        u.value = mots[0] + "." + mots.join('-').substr(mots[0].length+1);
 +        return true;
 +    }
 +    // jean paul dupont -> jean-paul.dupont
 +    if (mots.length === 3 && mots[0].toUpperCase() === 'JEAN') {
 +        u.value = mots[0] + "-" + mots[1] + "." + mots[2];
 +        return true;
 +    }
 +
 +    alert('Ton email ne doit pas contenir de blanc.\nLe format standard est\n\nprenom.nom.promotion\n\nSi ton nom ou ton prenom est composé,\nsépare les mots par des -');
 +
 +    return false;
 +}
 +
 +function doChallengeResponse() {
 +    var new_pass, old_pass, str;
 +
 +    if (!correctUserName()) {
 +        return false;
 +    }
 +
 +    new_pass = hash_encrypt(document.forms.login.password.value);
 +    old_pass = hash_encrypt(document.forms.login.password.value.substr(0, 10));
 +
 +    str = document.forms.login.username.value + ":" +
 +        new_pass + ":" +
 +        document.forms.loginsub.challenge.value;
 +
 +    document.forms.loginsub.response.value = hash_encrypt(str);
 +    if (new_pass !== old_pass) {
 +        document.forms.loginsub.xorpass.value = hash_xor(new_pass, old_pass);
 +    }
 +    document.forms.loginsub.username.value = document.forms.login.username.value;
 +    document.forms.loginsub.remember.value = document.forms.login.remember.checked;
 +    document.forms.loginsub.domain.value = document.forms.login.domain.value;
 +    document.forms.login.password.value = "";
 +    document.forms.loginsub.submit();
 +}
 +
 +function doChallengeResponseLogged() {
 +    var str = document.forms.loginsub.username.value + ":" +
 +        hash_encrypt(document.forms.login.password.value) + ":" +
 +        document.forms.loginsub.challenge.value;
 +
 +    document.forms.loginsub.response.value = hash_encrypt(str);
 +    document.forms.loginsub.remember.value = document.forms.login.remember.checked;
 +    document.forms.login.password.value = "";
 +    document.forms.loginsub.submit();
 +}
 +
 +// }}}
 +// {{{ send test email
 +
 +function sendTestEmail(token, hruid)
 +{
 +    var url = 'emails/test';
 +    var msg = "Un email a été envoyé avec succès";
 +    if (hruid) {
 +        url += '/' + hruid;
 +        msg += " sur l'adresse de " + hruid + ".";
 +    } else {
 +        msg += " sur ton addresse.";
 +    }
 +    $('#mail_sent').successMessage($url + '?token=' + token, msg);
 +    return false;
 +}
 +
  // }}}
+ // {{{ jQuery object extension
+ (function($) {
+     /* Add new functions to jQuery namesapce */
+     $.extend({
+         /* The goal of the following functions is to provide an AJAX API that
+          * take a different callback in case of HTTP success code (2XX) and in
+          * other cases.
+          */
+         xajax: function(source, method, data, onSuccess, onError, type) {
+             /* Shift argument */
+             if ($.isFunction(data)) {
+                 type = type || onError;
+                 onError = onSuccess;
+                 onSuccess = data;
+                 data = null;
+             }
+             if (onError != null && !$.isFunction(onError)) {
+                 type = type || onError;
+                 onError = null;
+             }
+             function ajaxHandler(data, textStatus, xhr) {
+                 if (textStatus == 'success') {
+                     if (onSuccess) {
+                         onSuccess(data, textStatus, xhr);
+                     }
+                 } else if (textStatus == 'error') {
+                     if (onError) {
+                         onError(data, textStatus, xhr);
+                     } else {
+                         alert("Une error s'est produite lors du traitement de la requête.\n"
+                             + "Ta session a peut-être expiré");
+                     }
+                 }
+             }
+             return $.ajax({
+                 url: source,
+                 type: method,
+                 success: ajaxHandler,
+                 data : data,
+                 dataType: type
+             });
+         },
+         xget: function(source, data, onSuccess, onError, type) {
+             return $.xajax(source, 'GET', data, onSuccess, onError, type);
+         },
+         xgetJSON: function(source, data, onSuccess, onError) {
+             return $.xget(source, data, onSuccess, onError, 'json');
+         },
+         xgetScript: function(source, onSuccess, onError) {
+             return $.xget(source, null, onSuccess, onError, 'script');
+         },
+         xgetText: function(source, data, onSuccess, onError) {
+             return $.xget(source, data, onSuccess, onError, 'text');
+         },
+         xpost: function(source, data, onSuccess, onError, type) {
+             return $.xajax(source, 'POST', data, onSuccess, onError, type);
+         }
+     });
+     /* Add new functions to jQuery objects */
+     $.fn.extend({
+         tmpMessage: function(message, success) {
+             if (success) {
+                 this.html("<img src='images/icons/wand.gif' alt='' /> " + message)
+                     .css('color', 'green');
+             } else {
+                 this.html("<img src='images/icons/error.gif' alt='' /> " + message)
+                     .css('color', 'red');
+             }
+             return this.css('fontWeight', 'bold')
+                        .show()
+                        .delay(1000)
+                        .fadeOut(500);
+         },
+         updateHtml: function(source, callback) {
+             var elements = this;
+             function handler(data) {
+                 elements.html(data);
+                 if (callback) {
+                     callback(data);
+                 }
+             }
+             $.xget(source, handler, 'text');
+             return this;
+         },
+         successMessage: function(source, message) {
+             var elements = this;
+             $.xget(source, function() {
+                 elements.tmpMessage(message, true);
+             });
+             return this;
+         },
+         wiki: function(text, withTitle) {
+             if (text == '') {
+                 return this.html('');
+             }
+             var url = 'wiki_preview';
+             if (!withTitle) {
+                 url += '/notitile';
+             }
+             var $this = this;
+             $.post(url, { text: text },
+                    function (data) {
+                        $this.html(data);
+                    }, 'text');
+             return this;
+         },
+         popWin: function(w, h) {
+             return this.click(function() {
+                 window.open(this.href, '_blank',
+                             'toolbar=0,location=0,directories=0,status=0,'
+                            +'menubar=0,scrollbars=1,resizable=1,'
+                            +'width='+w+',height='+h);
+                 return false;
+             });
+         }
+     });
+ })(jQuery);
+ // }}}
+ // {{{ preview wiki
+ function previewWiki(idFrom, idTo, withTitle, idShow)
+ {
+     $('#' + idTo).wiki($('#' + idFrom).val(), withTitle);
+     if (idShow != null) {
+         $('#' + idShow).show();
+     }
+ }
+ // }}}
+ // {{{ send test email
+ function sendTestEmail(token, hruid)
+ {
+     var url = 'emails/test';
+     var msg = "Un email a été envoyé avec succès";
+     if (hruid != null) {
+         url += '/' + hruid;
+         msg += " sur l'adresse de " + hruid + ".";
+     } else {
+         msg += " sur ton addresse.";
+     }
+     $('#mail_sent').successMessage($url + '?token=' + token, msg);
+     return false;
+ }
+ // }}}
  
  
  /***************************************************************************
 + * Overlib made simple
 + */
 +
 +(function($) {
 +    $.fn.extend({
 +        overlib: function(text, width, height) {
 +            var args = [ ];
 +            var key;
 +
 +            if (typeof text === 'string') {
 +                args.push(text);
 +                if (width) {
 +                    args.push(width);
 +                }
 +                if (height) {
 +                    args.push(height);
 +                }
 +            } else {
 +                for (key in text) {
 +                    switch (key) {
 +                      case 'text':
 +                        args.unshift(text[key]);
 +                        break;
 +                      case 'caption':
 +                        args.push(CAPTION, text[key]);
 +                        break;
 +                      case 'close_text':
 +                        args.push(CLOSETEXT, text[key]);
 +                        break;
 +                      case 'delay':
 +                        args.push(DELAY, text[key]);
 +                        break;
 +                      case 'sticky':
 +                        if (text[key]) {
 +                            args.push(STICKY);
 +                        }
 +                        break;
 +                      case 'width':
 +                        args.push(WIDTH, text[key]);
 +                        break;
 +                      case 'height':
 +                        args.push(HEIGHT, text[key]);
 +                        break;
 +                    }
 +                }
 +            }
 +            return this
 +                .mouseover(function () {
 +                    return overlib.apply(null, args);
 +                })
 +                .mouseout(nd);
 +        }
 +    });
 +}(jQuery));
 +
 +
 +/***************************************************************************
   * The real OnLoad
   */
  
 -$(document).ready(function() {
 +$(function() {
      auto_links();
      getNow();
      setInterval(getNow, 1000);
   *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
   ***************************************************************************/
  
+ // {{{ class StoredUserFilterBuilder
+ class StoredUserFilterBuilder
+ {
+     // Possible stored types (currently only 'ufb' exists)
+     const TYPE_UFB = 'ufb';
+     protected $ufb;
+     protected $env;
+     protected $ufc;
+     public function __construct(UserFilterBuilder &$ufb, PlFilterCondition &$ufc = null, array $env = array())
+     {
+         $this->ufb = $ufb;
+         $this->ufc = $ufc;
+         $this->env = $env;
+     }
+     public function export()
+     {
+         $export = new PlDict();
+         $export->set('type', self::TYPE_UFB);
+         $export->set('condition', $this->ufc->export());
+         $export->set('env', $this->env);
+         return $export;
+     }
+     public function getEnv()
+     {
+         return $this->env;
+     }
+     public function fillFromExport($export)
+     {
+         $export = new PlDict($export);
+         if (!$export->has('type')) {
+             throw new Exception("Missing 'type' field in export.");
+         }
+         if ($export->s('type') != self::TYPE_UFB) {
+             throw new Exception("Unknown type '$type' in export.");
+         }
+         $this->ufc = UserFilterCondition::fromExport($export->v('condition'));
+         $this->env = $export->v('env', array());
+     }
+     public function updateFromEnv($env)
+     {
+         $this->ufb->setFakeEnv($env);
+         if ($this->ufb->isValid()) {
+             $this->env = $env;
+             $this->ufc = $this->ufb->getUFC();
+             return true;
+         } else {
+             $this->ufb->clearFakeEnv();
+             return false;
+         }
+     }
+     public function refresh()
+     {
+         if ($this->isValid()) {
+             $this->ufc = $this->ufb->getUFC();
+         }
+     }
+     public function getUFC()
+     {
+         return $this->ufc;
+     }
+     public function isValid()
+     {
+         $this->ufb->setFakeEnv($this->env);
+         return $this->ufb->isValid();
+     }
+     public function isEmpty()
+     {
+         $this->ufb->setFakeEnv($this->env);
+         return $this->ufb->isEmpty();
+     }
+ }
+ // }}}
  // {{{ class UserFilterBuilder
  class UserFilterBuilder
  {
      private $valid = true;
      private $ufc = null;
      private $orders = array();
+     private $fake_env = null;
  
      /** Constructor
       * @param $fields An array of UFB_Field objects
          $this->envprefix   = $envprefix;
      }
  
+     public function setFakeEnv($env)
+     {
+         $this->fake_env = new PlDict($env);
+     }
+     public function clearFakeEnv()
+     {
+         $this->fake_env = null;
+     }
      /** Builds the UFC; returns as soon as a field says it is invalid
       */
      private function buildUFC()
          return $this->orders;
      }
  
+     public function getEnvFieldNames()
+     {
+         $fields = array();
+         foreach ($this->fields as $ufbf) {
+             $fields = array_merge($fields, $ufbf->getEnvFieldNames());
+         }
+         return array_unique($fields);
+     }
+     public function getEnv()
+     {
+         $values = array();
+         foreach ($this->getEnvFieldNames() as $field) {
+             if ($this->has($field)) {
+                 $values[$field] = $this->v($field);
+             }
+         }
+         return $values;
+     }
+     public function setEnv($values)
+     {
+         foreach ($this->getEnvFieldNames() as $field) {
+             if (array_key_exists($field, $values)) {
+                 Env::set($this->envprefix . $field, $values[$field]);
+             }
+         }
+     }
      /** Wrappers around Env::i/s/..., to add envprefix
       */
      public function s($key, $def = '')
      {
-         return Env::s($this->envprefix . $key, $def);
+         if ($this->fake_env) {
+             return $this->fake_env->s($key, $def);
+         } else {
+             return Env::s($this->envprefix . $key, $def);
+         }
      }
  
      public function t($key, $def = '')
      {
-         return Env::t($this->envprefix . $key, $def);
+         if ($this->fake_env) {
+             return $this->fake_env->t($key, $def);
+         } else {
+             return Env::t($this->envprefix . $key, $def);
+         }
      }
  
      public function i($key, $def = 0)
      {
-         return Env::i($this->envprefix . $key, $def);
+         if ($this->fake_env) {
+             return $this->fake_env->i($key, $def);
+         } else {
+             return Env::i($this->envprefix . $key, $def);
+         }
      }
  
      public function v($key, $def = null)
      {
-         return Env::v($this->envprefix . $key, $def);
+         if ($this->fake_env) {
+             return $this->fake_env->v($key, $def);
+         } else {
+             return Env::v($this->envprefix . $key, $def);
+         }
      }
  
      public function b($key, $def = false)
      {
-         return Env::b($this->envprefix . $key, $def);
+         if ($this->fake_env) {
+             return $this->fake_env->b($key, $def);
+         } else {
+             return Env::b($this->envprefix . $key, $def);
+         }
      }
  
      public function has($key)
      {
-         return Env::has($this->envprefix . $key);
+         if ($this->fake_env) {
+             return $this->fake_env->has($key);
+         } else {
+             return Env::has($this->envprefix . $key);
+         }
      }
  
      public function blank($key, $strict = false)
      {
-         return Env::blank($key, $strict);
+         if ($this->fake_env) {
+             return $this->fake_env->blank($key, $strict);
+         } else {
+             return Env::blank($key, $strict);
+         }
      }
  
      public function hasAlnum($key)
@@@ -241,6 -392,28 +392,28 @@@ class UFB_MentorSearch extends UserFilt
  }
  // }}}
  
+ // {{{ class UFB_NewsLetter
+ class UFB_NewsLetter extends UserFilterBuilder
+ {
+     const FIELDS_PROMO = 'promo';
+     const FIELDS_AXID = 'axid';
+     const FIELDS_GEO = 'geo';
+     public function __construct($flags, $envprefix = '')
+     {
+         $fields = array();
+         if ($flags->hasFlag(self::FIELDS_PROMO)) {
+             $fields[] = new UFBF_Promo('promo1', 'Promotion', 'egal1');
+             $fields[] = new UFBF_Promo('promo2', 'Promotion', 'egal2');
+         }
+         if ($flags->hasFlag(self::FIELDS_AXID)) {
+             $fields[] = new UFBF_SchoolIds('axid', 'Matricule AX', UFC_SchoolId::AX);
+         }
+         parent::__construct($fields, $envprefix);
+     }
+ }
+ // }}}
  // {{{ class UFB_Field
  abstract class UFB_Field
  {
       * @return boolean Whether the input is valid
       */
      abstract protected function check(UserFilterBuilder &$ufb);
+     // Simple form interface
+     /** Retrieve a list of env field names used by that field
+      * their values will be recorded when saving the 'search' and used to prefill the form
+      * when needed.
+      */
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield);
+     }
  }
  // }}}
  
@@@ -484,6 -668,11 +668,11 @@@ abstract class UFBF_Mixed extends UFB_F
          }
          return true;
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfieldindex, $this->envfield);
+     }
  }
  // }}}
  
@@@ -580,11 -769,17 +769,17 @@@ class UFBF_SchoolIds extends UFB_Fiel
  {
      // One of UFC_SchoolId types
      protected $type;
+     protected $reversed_envfield;
+     protected $reversed = false;
  
-     public function __construct($envfield, $formtext, $type = UFC_SchoolId::AX)
+     public function __construct($envfield, $formtext, $type = UFC_SchoolId::AX, $reversed_envfield = '')
      {
          parent::__construct($envfield, $formtext);
          $this->type = $type;
+         if ($reversed_envfield == '') {
+             $reversed_envfield = $envfield . '_reversed';
+         }
+         $this->reversed_envfield = $reversed_envfield;
      }
  
      protected function check(UserFilterBuilder &$ufb)
              return $this->raise("Le champ %s ne contient aucune valeur valide.");
          }
  
+         $this->reversed = $ufb->b($this->reversed_envfield);
          $this->val = $ids;
          return true;
      }
  
      protected function buildUFC(UserFilterBuilder &$ufb)
      {
-         return new UFC_SchoolId($this->type, $this->val);
+         $ufc = new UFC_SchoolId($this->type, $this->val);
+         if ($this->reversed) {
+             return new PFC_Not($ufc);
+         } else {
+             return $ufc;
+         }
      }
  }
  // }}}
@@@ -647,9 -848,9 +848,9 @@@ class UFBF_Promo extends UFB_Fiel
      private $comp;
      private $envfieldcomp;
  
-     public function __construct($envfield, $fromtext = '', $envfieldcomp)
+     public function __construct($envfield, $formtext = '', $envfieldcomp)
      {
-         parent::__construct($envfield, $fromtext);
+         parent::__construct($envfield, $formtext);
          $this->envfieldcomp = $envfieldcomp;
      }
  
      protected function buildUFC(UserFilterBuilder &$ufb) {
          return new UFC_Promo($this->comp, UserFilter::GRADE_ING, $this->val);
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->envfieldcomp);
+     }
  }
  // }}}
  
@@@ -808,6 -1014,11 +1014,11 @@@ class UFBF_Town extends UFBF_Tex
              }
          }
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlycurrentfield);
+     }
  }
  // }}}
  
@@@ -833,6 -1044,11 +1044,11 @@@ class UFBF_Country extends UFBF_Mixe
  
          return new UFC_AddressField($this->val, UFC_AddressField::FIELD_COUNTRY, UFC_Address::TYPE_ANY, $flags);
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->envfieldindex, $this->onlycurrentfield);
+     }
  }
  // }}}
  
@@@ -859,35 -1075,45 +1075,71 @@@ class UFBF_AdminArea extends UFBF_Inde
  
          return new UFC_AddressField($this->val, UFC_AddressField::FIELD_ADMAREA, UFC_Address::TYPE_ANY, $flags);
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlycurrentfield);
+     }
+ }
+ // }}}
+ // {{{ class UFBF_SubAdminArea
+ class UFBF_SubAdminArea extends UFBF_Index
+ {
+     protected $direnum = DirEnum::SUBADMINAREAS;
+     protected $onlycurrentfield;
+     public function __construct($envfield, $formtext = '', $onlycurrentfield = 'only_current')
+     {
+         parent::__construct($envfield, $formtext);
+         $this->onlycurrentfield = $onlycurrentfield;
+     }
+     protected function buildUFC(UserFilterBuilder &$ufb)
+     {
+         if ($ufb->isOn($this->onlycurrentfield)) {
+             $flags = UFC_Address::FLAG_CURRENT;
+         } else {
+             $flags = UFC_Address::FLAG_ANY;
+         }
+         return new UFC_AddressField($this->val, UFC_AddressField::FIELD_SUBADMAREA, UFC_Address::TYPE_ANY, $flags);
+     }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlycurrentfield);
+     }
  }
  // }}}
  
 +// {{{ class UFBF_SubAdminArea
 +class UFBF_SubAdminArea extends UFBF_Index
 +{
 +    protected $direnum = DirEnum::SUBADMINAREAS;
 +    protected $onlycurrentfield;
 +
 +    public function __construct($envfield, $formtext = '', $onlycurrentfield = 'only_current')
 +    {
 +        parent::__construct($envfield, $formtext);
 +        $this->onlycurrentfield = $onlycurrentfield;
 +    }
 +
 +
 +    protected function buildUFC(UserFilterBuilder &$ufb)
 +    {
 +        if ($ufb->isOn($this->onlycurrentfield)) {
 +            $flags = UFC_Address::FLAG_CURRENT;
 +        } else {
 +            $flags = UFC_Address::FLAG_ANY;
 +        }
 +
 +        return new UFC_AddressField($this->val, UFC_AddressField::FIELD_SUBADMAREA, UFC_Address::TYPE_ANY, $flags);
 +    }
 +}
 +// }}}
 +
  // {{{ class UFBF_JobCompany
  class UFBF_JobCompany extends UFBF_Text
  {
      {
          return new UFC_Job_Company(UFC_Job_Company::JOBNAME, $this->val);
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlymentorfield);
+     }
  }
  // }}}
  
@@@ -947,6 -1178,11 +1204,11 @@@ class UFBF_JobDescription extends UFBF_
              return new UFC_Job_Description($this->val, UserFilter::JOB_USERDEFINED);
          }
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlymentorfield);
+     }
  }
  // }}}
  
@@@ -969,6 -1205,11 +1231,11 @@@ class UFBF_JobCv extends UFBF_Tex
              return new UFC_Job_Description($this->val, UserFilter::JOB_CV);
          }
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->onlymentorfield);
+     }
  }
  // }}}
  
@@@ -1115,6 -1356,11 +1382,11 @@@ class UFBF_Networking extends UFBF_Tex
      {
          return new UFC_Networking($this->nwtype, $this->val);
      }
+     public function getEnvFieldNames()
+     {
+         return array($this->envfield, $this->networktypefield);
+     }
  }
  // }}}
  
diff --combined modules/admin.php
@@@ -47,7 -47,6 +47,7 @@@ class AdminModule extends PLModul
              'admin/wiki'                   => $this->make_hook('wiki',                   AUTH_MDP, 'admin'),
              'admin/ipwatch'                => $this->make_hook('ipwatch',                AUTH_MDP, 'admin'),
              'admin/icons'                  => $this->make_hook('icons',                  AUTH_MDP, 'admin'),
 +            'admin/geocoding'              => $this->make_hook('geocoding',              AUTH_MDP, 'admin'),
              'admin/accounts'               => $this->make_hook('accounts',               AUTH_MDP, 'admin'),
              'admin/account/watch'          => $this->make_hook('account_watch',          AUTH_MDP, 'admin'),
              'admin/account/types'          => $this->make_hook('account_types',          AUTH_MDP, 'admin'),
          $page->assign_by_ref('mails', $sql);
      }
  
 -    function handler_postfix_regexpsbounces(&$page, $new = null) {
 -        $page->changeTpl('admin/emails_bounces_re.tpl');
 -        $page->setTitle('Administration - Postfix : Regexps Bounces');
 -        $page->assign('new', $new);
 -
 -        if (Post::has('submit')) {
 -            foreach (Env::v('lvl') as $id=>$val) {
 -                XDB::query(
 -                        "REPLACE INTO emails_bounces_re (id,pos,lvl,re,text) VALUES ({?}, {?}, {?}, {?}, {?})",
 -                        $id, $_POST['pos'][$id], $_POST['lvl'][$id], $_POST['re'][$id], $_POST['text'][$id]
 -                );
 -            }
 -        }
 -
 -        $page->assign('bre', XDB::iterator("SELECT * FROM emails_bounces_re ORDER BY pos"));
 -    }
 -
      // {{{ logger view
  
      /** Retrieves the available days for a given year and month.
          // }}}
  
  
 -        $page->addJsLink('jquery.ui.core.js');
 -        $page->addJsLink('jquery.ui.widget.js');
 -        $page->addJsLink('jquery.ui.tabs.js');
 -        $page->addJsLink('password.js');
 +        $page->addJsLink('jquery.ui.xorg.js');
  
          // Displays last login and last host information.
          $res = XDB::query("SELECT  start, host
      {
          $page->changeTpl('admin/validation.tpl');
          $page->setTitle('Administration - Valider une demande');
-         $page->addCssLink('nl.css');
+         $page->addCssLink('nl.Polytechnique.org.css');
  
          if ($action == 'edit' && !is_null($id)) {
              $page->assign('preview_id', $id);
          $table_editor->apply($page, $action, $id);
      }
  
 +    private static function isCountryIncomplete(array &$item)
 +    {
 +        $warning = false;
 +        foreach (array('worldRegion', 'country', 'capital', 'phonePrefix', 'licensePlate', 'countryPlain') as $field) {
 +            if ($item[$field] == '') {
 +                $item[$field . '_warning'] = true;
 +                $warning = true;
 +            }
 +        }
 +        if (is_null($item['belongsTo'])) {
 +            foreach (array('nationality', 'nationalityEn') as $field) {
 +                if ($item[$field] == '') {
 +                    $item[$field . '_warning'] = true;
 +                    $warning = true;
 +                }
 +            }
 +        }
 +        return $warning;
 +    }
 +
 +    private static function updateCountry(array $item)
 +    {
 +        XDB::execute('UPDATE  geoloc_countries
 +                         SET  countryPlain = {?}
 +                       WHERE  iso_3166_1_a2 = {?}',
 +                     mb_strtoupper(replace_accent($item['country'])), $item['iso_3166_1_a2']);
 +    }
 +
 +    private static function isLanguageIncomplete(array &$item)
 +    {
 +        if ($item['language'] == '') {
 +            $item['language_warning'] = true;
 +            return true;
 +        }
 +        return false;
 +    }
 +
 +    private static function updateLanguage(array $item) {}
 +
 +    function handler_geocoding(&$page, $category = null, $action = null, $id = null)
 +    {
 +        // Warning, this handler requires the following packages:
 +        //  * pkg-isocodes
 +        //  * isoquery
 +
 +        static $properties = array(
 +            'country'  => array(
 +                'name'         => 'pays',
 +                'isocode'      => '3166',
 +                'table'        => 'geoloc_countries',
 +                'id'           => 'iso_3166_1_a2',
 +                'main_fields'  => array('iso_3166_1_a3', 'iso_3166_1_num', 'countryEn'),
 +                'other_fields' => array('worldRegion', 'country', 'capital', 'nationality', 'nationalityEn',
 +                                        'phonePrefix', 'phoneFormat', 'licensePlate', 'belongsTo')
 +            ),
 +            'language' => array(
 +                'name'         => 'langages',
 +                'isocode'      => '639',
 +                'table'        => 'profile_langskill_enum',
 +                'id'           => 'iso_639_2b',
 +                'main_fields'  => array('iso_639_2t', 'iso_639_1', 'language_en'),
 +                'other_fields' => array('language')
 +
 +            )
 +        );
 +
 +        if (is_null($category) || !array_key_exists($category, $properties)) {
 +            pl_redirect('admin');
 +        }
 +
 +        $data = $properties[$category];
 +
 +        if ($action == 'edit' || $action == 'add') {
 +            $main_fields = array_merge(array($data['id']), $data['main_fields']);
 +            $all_fields = array_merge($main_fields, $data['other_fields']);
 +
 +            if (is_null($id)) {
 +                if (Post::has('new_id')) {
 +                    $id = Post::v('new_id');
 +                } else {
 +                    pl_redirect('admin/geocoding/' . $category);
 +                }
 +            }
 +
 +            $list = array();
 +            exec('isoquery --iso=' . $data['isocode'] . ' ' . $id, $list);
 +            if (count($list) == 1) {
 +                $array = explode("\t", $list[0]);
 +                foreach ($main_fields as $i => $field) {
 +                    $iso[$field] = $array[$i];
 +                }
 +            } else {
 +                $iso = array();
 +            }
 +
 +            if ($action == 'add') {
 +                if (Post::has('new_id')) {
 +                    S::assert_xsrf_token();
 +                }
 +
 +                if (count($iso)) {
 +                    $item = $iso;
 +                } else {
 +                    $item = array($data['id'] => $id);
 +                }
 +                XDB::execute('INSERT INTO  ' . $data['table'] . '(' . implode(', ', array_keys($item)) . ')
 +                                   VALUES  ' . XDB::formatArray($item));
 +                $page->trigSuccess($id . ' a bien été ajouté à la base.');
 +            } elseif ($action == 'edit') {
 +                if (Post::has('edit')) {
 +                    S::assert_xsrf_token();
 +
 +                    $item = array();
 +                    $set  = array();
 +                    foreach ($all_fields as $field) {
 +                        $item[$field] = Post::t($field);
 +                        $set[] = $field . XDB::format(' = {?}', ($item[$field] ? $item[$field] : null));
 +                    }
 +                    XDB::execute('UPDATE  ' . $data['table'] . '
 +                                     SET  ' . implode(', ', $set) . '
 +                                   WHERE  ' . $data['id'] . ' = {?}',
 +                                 $id);
 +                    call_user_func_array(array('self', 'update' . ucfirst($category)), array($item));
 +                    $page->trigSuccess($id . ' a bien été mis à jour.');
 +                } elseif (Post::has('del')) {
 +                    S::assert_xsrf_token();
 +
 +                    XDB::execute('DELETE FROM  ' . $data['table'] . '
 +                                        WHERE  ' . $data['id'] . ' = {?}',
 +                                 $id);
 +                    $page->trigSuccessRedirect($id . ' a bien été supprimé.', 'admin/geocoding/' . $category);
 +                } else {
 +                    $item = XDB::fetchOneAssoc('SELECT  *
 +                                                  FROM  ' . $data['table'] . '
 +                                                 WHERE  ' . $data['id'] . ' = {?}',
 +                                               $id);
 +                }
 +            }
 +
 +            $page->changeTpl('admin/geocoding_edit.tpl');
 +            $page->setTitle('Administration - ' . ucfirst($data['name']));
 +            $page->assign('category', $category);
 +            $page->assign('name', $data['name']);
 +            $page->assign('all_fields', $all_fields);
 +            $page->assign('id', $id);
 +            $page->assign('iso', $iso);
 +            $page->assign('item', $item);
 +            return;
 +        }
 +
 +        $page->changeTpl('admin/geocoding.tpl');
 +        $page->setTitle('Administration - ' . ucfirst($data['name']));
 +        $page->assign('category', $category);
 +        $page->assign('name', $data['name']);
 +        $page->assign('id', $data['id']);
 +        $page->assign('main_fields', $data['main_fields']);
 +        $page->assign('all_fields', array_merge($data['main_fields'], $data['other_fields']));
 +
 +        // First build the list provided by the iso codes.
 +        $list = array();
 +        exec('isoquery --iso=' . $data['isocode'], $list);
 +
 +        foreach ($list as $key => $item) {
 +            $array = explode("\t", $item);
 +            unset($list[$key]);
 +            $list[$array[0]] = array();
 +            foreach ($data['main_fields'] as $i => $field) {
 +                $list[$array[0]][$field] = $array[$i + 1];
 +            }
 +        }
 +        ksort($list);
 +
 +        // Retrieve all data from the database.
 +        $db_list = XDB::rawFetchAllAssoc('SELECT  *
 +                                            FROM  ' . $data['table'] . '
 +                                        ORDER BY  ' . $data['id'],
 +                                         $data['id']);
 +
 +        // Sort both iso and database data into 5 categories:
 +        //  $missing: data from the iso list not in the database,
 +        //  $non_existing: data from the database not in the iso list,
 +        //  $erroneous: data that differ on main fields,
 +        //  $incomplete: data with empty fields in the data base,
 +        //  $remaining: remaining correct and complete data from the database.
 +
 +        $missing = $non_existing = $erroneous = $incomplete = $remaining = array();
 +        foreach (array_keys($list) as $id) {
 +            if (!array_key_exists($id, $db_list)) {
 +                $missing[$id] = $list[$id];
 +            }
 +        }
 +
 +        foreach ($db_list as $id => $item) {
 +            if (!array_key_exists($id, $list)) {
 +                $non_existing[$id] = $item;
 +            } else {
 +                $error = false;
 +                foreach ($data['main_fields'] as $field) {
 +                    if ($item[$field] != $list[$id][$field]) {
 +                        $item[$field . '_error'] = true;
 +                        $error = true;
 +                    }
 +                }
 +                if ($error == true) {
 +                    $erroneous[$id] = $item;
 +                } elseif (call_user_func_array(array('self', 'is' . ucfirst($category) . 'Incomplete'), array(&$item))) {
 +                    $incomplete[$id] = $item;
 +                } else {
 +                    $remaining[$id] = $item;
 +                }
 +            }
 +        }
 +
 +        $page->assign('lists', array(
 +                'manquant'  => $missing,
 +                'disparu'   => $non_existing,
 +                'erroné'    => $erroneous,
 +                'incomplet' => $incomplete,
 +                'restant'   => $remaining
 +        ));
 +    }
 +
      function handler_accounts(PlPage $page)
      {
          $page->changeTpl('admin/accounts.tpl');
          $page->setTitle('Administration - Comptes');
 -        $page->addJsLink('password.js');
  
          if (Post::has('create_account')) {
              S::assert_xsrf_token();
diff --combined modules/profile.php
@@@ -30,7 -30,7 +30,7 @@@ class ProfileModule extends PLModul
              'fiche.php'                  => $this->make_hook('fiche',                      AUTH_PUBLIC),
              'profile'                    => $this->make_hook('profile',                    AUTH_PUBLIC),
              'profile/private'            => $this->make_hook('profile',                    AUTH_COOKIE),
-             'profile/ax'                 => $this->make_hook('ax',                         AUTH_COOKIE, 'admin'),
+             'profile/ax'                 => $this->make_hook('ax',                         AUTH_COOKIE, 'admin,edit_directory'),
              'profile/edit'               => $this->make_hook('p_edit',                     AUTH_MDP),
              'profile/ajax/address'       => $this->make_hook('ajax_address',               AUTH_COOKIE, 'user', NO_AUTH),
              'profile/ajax/tel'           => $this->make_hook('ajax_tel',                   AUTH_COOKIE, 'user', NO_AUTH),
          $page->assign('view', $view);
          $page->assign('logged', S::logged());
  
 -        $page->addJsLink('close_on_esc.js');
          header('Last-Modified: ' . date('r', strtotime($profile->last_change)));
      }
  
          $page->addJsLink('education.js', false); /* dynamic content */
          $page->addJsLink('grades.js', false);    /* dynamic content */
          $page->addJsLink('profile.js');
 -        $page->addJsLink('jquery.autocomplete.js');
          $wiz = new PlWizard('Profil', PlPage::getCoreTpl('plwizard.tpl'), true, true, false);
          $wiz->addUserData('profile', $profile);
          $wiz->addUserData('owner', $profile->owner());
                LEFT JOIN  geoloc_countries       AS gc ON (m.country = gc.iso_3166_1_a2)
                    WHERE  pid = {?}", $pf->id());
          $page->assign('pays', $res->fetchColumn());
 -
 -        $page->addJsLink('close_on_esc.js');
      }
  
      function handler_ref_country(&$page)
@@@ -92,7 -92,7 +92,7 @@@ class ProfileSettingJob implements Prof
          }
          $it = Phone::iterate(array($page->pid()), array(Phone::LINK_JOB));
          while ($phone = $it->next()) {
 -            $jobs[$phone->linkId()]['w_phone'][$phone->id()] = $phone->toFormArray();
 +            $jobs[$phone->link_id]['w_phone'][$phone->id] = $phone->toFormArray();
          }
          $res = XDB::iterator("SELECT  e.jtid, e.full_name, j.jid
                                  FROM  profile_job_term_enum AS e
              }
  
              if (isset($job['removed']) && $job['removed']) {
-                 if ($job['name'] == '' && $entreprise) {
+                 if ($job['name'] == '' && $entreprise && isset($entreprise[$entr_val - 1])) {
                      $entreprise[$entr_val - 1]->clean();
                  }
                  unset($value[$key]);
                                    VALUES  ' . implode(', ', $terms_values) . '
                   ON DUPLICATE KEY UPDATE  computed = VALUES(computed)');
          }
+         if (S::user()->isMe($page->owner) && count($value) > 1) {
+             Platal::page()->trigWarning('Attention, tu as plusieurs emplois sur ton profil. Pense à supprimer ceux qui sont obsolètes.');
+         }
      }
  
      public function getText($value)
diff --combined modules/register.php
@@@ -52,7 -52,7 +52,7 @@@ class RegisterModule extends PLModul
              $nameTypes = DirEnum::getOptions(DirEnum::NAMETYPES);
              $nameTypes = array_flip($nameTypes);
              $res = XDB::query("SELECT  a.uid, pd.promo, pnl.name AS lastname, pnf.name AS firstname, p.xorg_id AS xorgid,
 -                                       p.birthdate_ref AS birthdateRef, FIND_IN_SET('watch', a.flags) AS watch, m.hash, a.type as eduType
 +                                       p.birthdate_ref AS birthdateRef, FIND_IN_SET('watch', a.flags) AS watch, m.hash
                                   FROM  register_marketing AS m
                             INNER JOIN  accounts           AS a   ON (m.uid = a.uid)
                             INNER JOIN  account_profiles   AS ap  ON (a.uid = ap.uid AND FIND_IN_SET('owner', ap.perms))
                      }
  
                      // Register the optional services requested by the user.
 -                    if ($subState->v('eduType') == 'x') {
 -                        $proposedServices = array('ax_letter', 'imap', 'ml_promo', 'nl');
 -                    } else {
 -                        $proposedServices = array('ax_letter', 'nl');
 -                    }
                      $services = array();
 -                    foreach ($proposedServices as $service) {
 +                    foreach (array('ax_letter', 'imap', 'ml_promo', 'nl') as $service) {
                          if (Post::b($service)) {
                              $services[] = $service;
                          }
          }
  
          $page->changeTpl('register/step' . $subState->i('step') . '.tpl');
 -        $page->addJsLink('password.js');
          if (isset($error)) {
              $page->trigError($error);
          }
  
          // Prepare the template for display.
          $page->changeTpl('register/end.tpl');
 -        $page->addJsLink('do_challenge_response_logged.js');
          $page->assign('forlife', $forlife);
          $page->assign('firstname', $firstname);
  
  
          // Add the registration email address as first and only redirection.
          require_once 'emails.inc.php';
 -        $user = User::getSilentWithUID($uid);
 -        if ($isX) {
 -            $redirect = new Redirect($user);
 -            $redirect->add_email($email);
 -        } else {
 -            XDB::execute('UPDATE  accounts
 -                             SET  email = {?}
 -                           WHERE  uid = {?}', $email, $uid);
 -        }
 +        $redirect = new Redirect($user);
 +        $redirect->add_email($email);
  
          // Try to start a session (so the user don't have to log in); we will use
          // the password available in Post:: to authenticate the user.
  
          // Subscribe the user to the services she did request at registration time.
          foreach (explode(',', $services) as $service) {
+             require_once 'newsletter.inc.php';
              switch ($service) {
                  case 'ax_letter':
-                     Platal::load('axletter', 'axletter.inc.php');
-                     AXLetter::subscribe($uid);
+                     NewsLetter::forGroup(NewsLetter::GROUP_AX)->subscribe($user);
+                     break;
+                 case 'nl':
+                     NewsLetter::forGroup(NewsLetter::GROUP_XORG)->subscribe($user);
                      break;
                  case 'imap':
                      $storage = new EmailStorage($user, 'imap');
                          }
                      }
                      break;
-                 case 'nl':
-                     require_once 'newsletter.inc.php';
-                     NewsLetter::subscribe($uid);
-                     break;
              }
          }
  
  
          // Congratulate our newly registered user by email.
          $mymail = new PlMailer('register/success.mail.tpl');
 +        $mymail->addTo("\"{$user->fullName()}\" <{$user->forlifeEmail()}>");
          if ($isX) {
 -            $mymail->addTo("\"{$user->fullName()}\" <{$user->forlifeEmail()}>");
              $mymail->setSubject('Bienvenue parmi les X sur le web !');
          } else {
 -            $mymail->addTo($email);
              $mymail->setSubject('Bienvenue sur Polytechnique.org !');
          }
          $mymail->assign('forlife', $forlife);
@@@ -32,6 -32,8 +32,6 @@@
        <a href="admin/postfix/whitelist">Whitelist</a>
        &nbsp;&nbsp;|&nbsp;&nbsp;
        <a href="admin/postfix/delayed">Retardés</a>
 -      &nbsp;&nbsp;|&nbsp;&nbsp;
 -      <a href="admin/postfix/regexp_bounces">Regexps Bounces</a>
      </td>
    </tr>
    <tr class="pair">
  
    <tr><th colspan="2">{icon name=user_gray} Champs</th></tr>
    <tr class="impair">
 +    <td class="titre">Pays / Langues</td>
 +    <td>
 +      <a href="admin/geocoding/country">Pays</a>
 +      &nbsp;&nbsp;|&nbsp;&nbsp;
 +      <a href="admin/geocoding/language">Langues</a>
 +    </td>
 +  </tr>
 +  <tr class="impair">
      <td class="titre">Formation</td>
      <td>
        <a href="admin/education">Formations</a>
      </td>
    </tr>
    <tr class="pair">
-     <td class="titre">Newsletter</td>
+     <td class="titre">Newsletters</td>
      <td>
-       <a href="admin/newsletter">Liste</a>
+       <a href="admin/nls">Liste des NLs groupes</a>
        &nbsp;&nbsp;|&nbsp;&nbsp;
-       <a href="admin/newsletter/categories">Catégories</a>
+       <a href="admin/newsletter/">NL de X.org</a>
      </td>
    </tr>
    <tr class="impair">
-     <td class="titre">AX-Letter</td>
-     <td>
-       <a href="ax/edit">Édition</a>
-       &nbsp;&nbsp;|&nbsp;&nbsp;
-       <a href="admin/axletter">Inscriptions et permissions</a>
-     </td>
-   </tr>
-   <tr class="pair">
      <td class="titre">Wiki</td>
      <td>
        <a href="admin/wiki">Pages et permissions</a>
      </td>
    </tr>
-   <tr class="impair">
+   <tr class="pair">
      <td class="titre">Sondages</td>
      <td>
        <a href="survey/admin">Gestion des sondages</a>
      </td>
    </tr>
-   <tr class="pair">
+   <tr class="impair">
      <td class="titre">Validations</td>
      <td>
        <a href="admin/validate/answers">Réponses automatiques</a>