================================================================================
- 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
* 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
* 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:
- 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
##
## 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)
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:
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
# 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:
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
}
$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)));
}
}
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,
$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')) {
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();
- 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;
}
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;
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;
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)
}
// }}}
+ // {{{ 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);
+ }
}
// }}}
}
return true;
}
+
+ public function getEnvFieldNames()
+ {
+ return array($this->envfieldindex, $this->envfield);
+ }
}
// }}}
{
// 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;
+ }
}
}
// }}}
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);
+ }
}
// }}}
}
}
}
+
+ public function getEnvFieldNames()
+ {
+ return array($this->envfield, $this->onlycurrentfield);
+ }
}
// }}}
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);
+ }
}
// }}}
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);
+ }
}
// }}}
return new UFC_Job_Description($this->val, UserFilter::JOB_USERDEFINED);
}
}
+
+ public function getEnvFieldNames()
+ {
+ return array($this->envfield, $this->onlymentorfield);
+ }
}
// }}}
return new UFC_Job_Description($this->val, UserFilter::JOB_CV);
}
}
+
+ public function getEnvFieldNames()
+ {
+ return array($this->envfield, $this->onlymentorfield);
+ }
}
// }}}
{
return new UFC_Networking($this->nwtype, $this->val);
}
+
+ public function getEnvFieldNames()
+ {
+ return array($this->envfield, $this->networktypefield);
+ }
}
// }}}
'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();
'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)
}
$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)
$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);
<a href="admin/postfix/whitelist">Whitelist</a>
|
<a href="admin/postfix/delayed">Retardés</a>
- |
- <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>
+ |
+ <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>
|
- <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>
- |
- <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>