Delete master education.
[platal.git] / modules / platal.php
index 8146055..1772921 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /***************************************************************************
- *  Copyright (C) 2003-2008 Polytechnique.org                              *
+ *  Copyright (C) 2003-2014 Polytechnique.org                              *
  *  http://opensource.polytechnique.org/                                   *
  *                                                                         *
  *  This program is free software; you can redistribute it and/or modify   *
@@ -21,7 +21,7 @@
 
 function bugize($list)
 {
-    $list = split(',', $list);
+    $list = preg_split('/,/', $list, -1, PREG_SPLIT_NO_EMPTY);
     $ans  = array();
 
     foreach ($list as $bug) {
@@ -38,33 +38,37 @@ class PlatalModule extends PLModule
     function handlers()
     {
         return array(
-            'index'       => $this->make_hook('index',     AUTH_PUBLIC),
-            'cacert.pem'  => $this->make_hook('cacert',    AUTH_PUBLIC),
-            'changelog'   => $this->make_hook('changelog', AUTH_PUBLIC),
+            'index'             => $this->make_hook('index',        AUTH_PUBLIC),
+            'cacert.pem'        => $this->make_hook('cacert',       AUTH_PUBLIC),
+            'changelog'         => $this->make_hook('changelog',    AUTH_PUBLIC),
 
             // Preferences thingies
-            'prefs'       => $this->make_hook('prefs',     AUTH_COOKIE),
-            'prefs/rss'   => $this->make_hook('prefs_rss', AUTH_COOKIE),
-            'prefs/webredirect'
-                          => $this->make_hook('webredir',  AUTH_MDP),
-            'prefs/skin'  => $this->make_hook('skin',      AUTH_COOKIE),
+            'prefs'             => $this->make_hook('prefs',        AUTH_COOKIE, 'user,groups'),
+            'prefs/rss'         => $this->make_hook('prefs_rss',    AUTH_COOKIE, 'user'),
+            'prefs/webredirect' => $this->make_hook('webredir',     AUTH_PASSWD, 'mail'),
+            'prefs/skin'        => $this->make_hook('skin',         AUTH_COOKIE, 'user'),
+            'prefs/email'       => $this->make_hook('prefs_email',  AUTH_COOKIE, 'mail'),
 
             // password related thingies
-            'password'      => $this->make_hook('password',  AUTH_MDP),
-            'tmpPWD'        => $this->make_hook('tmpPWD',    AUTH_PUBLIC),
-            'password/smtp' => $this->make_hook('smtppass',  AUTH_MDP),
-            'recovery'      => $this->make_hook('recovery',  AUTH_PUBLIC),
-            'exit'          => $this->make_hook('exit', AUTH_PUBLIC),
-            'review'        => $this->make_hook('review', AUTH_PUBLIC),
-            'deconnexion.php' => $this->make_hook('exit', AUTH_PUBLIC),
+            'password'          => $this->make_hook('password',     AUTH_PASSWD, 'user,groups'),
+            'password/smtp'     => $this->make_hook('smtppass',     AUTH_PASSWD, 'mail'),
+            'tmpPWD'            => $this->make_hook('tmpPWD',       AUTH_PUBLIC),
+            'recovery'          => $this->make_hook('recovery',     AUTH_PUBLIC),
+            'recovery/ext'      => $this->make_hook('recovery_ext', AUTH_PUBLIC),
+            'register/ext'      => $this->make_hook('register_ext', AUTH_PUBLIC),
+            'exit'              => $this->make_hook('exit',         AUTH_PUBLIC),
+            'review'            => $this->make_hook('review',       AUTH_PUBLIC),
+            'deconnexion.php'   => $this->make_hook('exit',         AUTH_PUBLIC),
+
+            'error'             => $this->make_hook('test_error',   AUTH_COOKIE),
         );
     }
 
-    function handler_index(&$page)
+    function handler_index($page)
     {
         // Include X-XRDS-Location response-header for Yadis discovery
         global $globals;
-        header('X-XRDS-Location: ' . $globals->baseurl . '/openid/idp_xrds');
+        header('X-XRDS-Location: ' . $globals->baseurl . '/openid/xrds');
 
         // Redirect to the suitable page
         if (S::logged()) {
@@ -74,20 +78,14 @@ class PlatalModule extends PLModule
         }
     }
 
-    function handler_cacert(&$page)
+    function handler_cacert($page)
     {
-        $data = file_get_contents("/etc/ssl/xorgCA/cacert.pem","r");
-        header("Pragma:");
-        header("Set-Cookie:");
-        header("Cache-Control:");
-        header("Expires:");
-        header("Content-Type: application/x-x509-ca-cert");
-        header("Content-Length: ".strlen($data));
-        echo $data;
+        pl_cached_content_headers("application/x-x509-ca-cert");
+        readfile("/etc/ssl/xorgCA/cacert.pem");
         exit;
     }
 
-    function handler_changelog(&$page, $core = null)
+    function handler_changelog($page, $core = null)
     {
         $page->changeTpl('platal/changeLog.tpl');
 
@@ -120,46 +118,40 @@ class PlatalModule extends PLModule
     function __set_rss_state($state)
     {
         if ($state) {
-            S::set('token', rand_url_id(16));
-            XDB::execute('UPDATE  accounts
-                             SET  token = {?}
-                           WHERE  uid = {?}', S::s('token'), S::i('uid'));
+            if (!S::user()->token) {
+                S::user()->token = rand_url_id(16);
+                S::set('token', S::user()->token);
+                XDB::execute('UPDATE  accounts
+                                 SET  token = {?}
+                               WHERE  uid = {?}', S::user()->token, S::i('uid'));
+            }
         } else {
             S::kill('token');
+            S::user()->token = null;
             XDB::execute('UPDATE  accounts
                              SET  token = NULL
                            WHERE  uid = {?}', S::i('uid'));
         }
     }
 
-    function handler_prefs(&$page)
+    function handler_prefs($page)
     {
         $page->changeTpl('platal/preferences.tpl');
         $page->setTitle('Mes préférences');
 
         if (Post::has('email_format')) {
+            S::assert_xsrf_token();
             $fmt = Post::s('email_format');
-            XDB::execute("UPDATE accounts
-                             SET email_format = {?}
-                           WHERE uid = {?}",
-                         $fmt, S::v('uid'));
-            S::set('email_format', $fmt);
+            S::user()->setEmailFormat($fmt);
         }
 
         if (Post::has('rss')) {
-            $this->__set_rss_state(Post::b('rss'));
+            S::assert_xsrf_token();
+            $this->__set_rss_state(Post::s('rss') == 'on');
         }
-
-        # FIXME: this code is not multi-domain compatible. We should decide how
-        # carva will extend to users not in the main domain.
-        $res = XDB::query("SELECT  alias
-                             FROM  aliases
-                            WHERE  id = {?} AND FIND_IN_SET('bestalias', flags)",
-                          S::user()->id());
-        $page->assign('bestalias', $res->fetchOneCell());
     }
 
-    function handler_webredir(&$page)
+    function handler_webredir($page)
     {
         $page->changeTpl('platal/webredirect.tpl');
         $page->setTitle('Redirection de page WEB');
@@ -169,8 +161,9 @@ class PlatalModule extends PLModule
                 $page->trigError('URL invalide');
             } else {
                 $url = Env::t('url');
-                XDB::execute('REPLACE INTO  carvas (uid, url)
-                                    VALUES  ({?}, {?})',
+                XDB::execute('INSERT INTO  carvas (uid, url)
+                                   VALUES  ({?}, {?})
+                  ON DUPLICATE KEY UPDATE  url = VALUES(url)',
                              S::i('uid'), $url);
                 S::logger()->log('carva_add', 'http://' . $url);
                 $page->trigSuccess("Redirection activée vers <a href='http://$url'>$url</a>");
@@ -190,14 +183,14 @@ class PlatalModule extends PLModule
 
         # FIXME: this code is not multi-domain compatible. We should decide how
         # carva will extend to users not in the main domain.
-        $res = XDB::query("SELECT  alias
-                             FROM  aliases
-                            WHERE  id = {?} AND FIND_IN_SET('bestalias', flags)",
-                          S::user()->id());
-        $page->assign('bestalias', $res->fetchOneCell());
+        $best = XDB::fetchOneCell('SELECT  email
+                                     FROM  email_source_account
+                                    WHERE  uid = {?} AND FIND_IN_SET(\'bestalias\', flags)',
+                                  S::user()->id());
+        $page->assign('bestalias', $best);
     }
 
-    function handler_prefs_rss(&$page)
+    function handler_prefs_rss($page)
     {
         $page->changeTpl('platal/filrss.tpl');
 
@@ -209,14 +202,55 @@ class PlatalModule extends PLModule
         }
     }
 
-    function handler_password(&$page)
+    function handler_prefs_email($page)
+    {
+        $page->changeTpl('platal/email_preferences.tpl');
+
+        if (Post::has('submit')) {
+            S::assert_xsrf_token();
+
+            $from_email = Post::t('from_email');
+            $from_format = Post::v('from_format');
+
+            // Checks email.
+            $email_regex = '/^[a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+$/i';
+            if (!preg_match($email_regex, $from_email)) {
+                $full_regex = '/^[^<]*<[a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+>$/i';
+                if (!preg_match($full_regex, $from_email)) {
+                    $page->trigError("L'adresse email est erronée.");
+                    $error = true;
+                    $page->assign('from_email', $from_email);
+                    $page->assign('from_format', $from_format);
+                    $page->assign('error', true);
+                    return;
+                }
+            }
+
+            // Saves data.
+            XDB::execute('UPDATE  accounts
+                             SET  from_email = {?}, from_format = {?}
+                           WHERE  uid = {?}',
+                         $from_email, ($from_format == 'html' ? 'html' : 'text'), S::user()->id());
+            $page->trigSuccess('Données enregistrées.');
+        }
+
+        $data = XDB::fetchOneAssoc('SELECT  from_email, from_format
+                                      FROM  accounts
+                                     WHERE  uid = {?}',
+                                   S::user()->id());
+        $page->assign('from_email', $data['from_email']);
+        $page->assign('from_format', $data['from_format']);
+        $page->assign('error', false);
+    }
+
+    function handler_password($page)
     {
         global $globals;
 
-        if (Post::has('response2'))  {
+        if (Post::has('pwhash') && Post::t('pwhash'))  {
             S::assert_xsrf_token();
 
-            S::set('password', $password = Post::v('response2'));
+            S::set('password', $password = Post::t('pwhash'));
             XDB::execute('UPDATE  accounts
                              SET  password = {?}
                            WHERE  uid={?}', $password,
@@ -235,16 +269,16 @@ class PlatalModule extends PLModule
             S::logger()->log('passwd');
             Platal::session()->setAccessCookie(true);
 
-            $page->changeTpl('platal/motdepasse.success.tpl');
+            $page->changeTpl('platal/password.success.tpl');
             $page->run();
         }
 
-        $page->changeTpl('platal/motdepasse.tpl');
-        $page->addJsLink('motdepasse.js');
+        $page->changeTpl('platal/password.tpl');
         $page->setTitle('Mon mot de passe');
+        $page->assign('do_auth', 0);
     }
 
-    function handler_smtppass(&$page)
+    function handler_smtppass($page)
     {
         $page->changeTpl('platal/acces_smtp.tpl');
         $page->setTitle('Acces SMTP/NNTP');
@@ -278,7 +312,7 @@ class PlatalModule extends PLModule
         $page->assign('actif', $res->fetchOneCell());
     }
 
-    function handler_recovery(&$page)
+    function handler_recovery($page)
     {
         global $globals;
 
@@ -288,7 +322,7 @@ class PlatalModule extends PLModule
             return;
         }
 
-        if (!ereg('[0-3][0-9][0-1][0-9][1][9]([0-9]{2})', Env::v('birth'))) {
+        if (!preg_match('/^[0-3][0-9][0-1][0-9][1][9]([0-9]{2})$/', Env::v('birth'))) {
             $page->trigError('Date de naissance incorrecte ou incohérente');
             return;
         }
@@ -300,90 +334,122 @@ class PlatalModule extends PLModule
 
         $mailorg = strtok(Env::v('login'), '@');
 
-        // XXX: recovery requires usage of profile data.
-        $res = XDB::query(
-                "SELECT  user_id, naissance
-                   FROM  auth_user_md5 AS u
-             INNER JOIN  aliases       AS a ON (u.user_id=a.id AND type != 'homonyme')
-                  WHERE  a.alias={?} AND u.perms IN ('admin','user') AND u.deces=0", $mailorg);
-        list($uid, $naissance) = $res->fetchOneRow();
-
-        if ($naissance == $birth) {
-            $res = XDB::query("SELECT  COUNT(*)
-                                 FROM  emails
-                                WHERE  uid = {?} AND flags != 'panne' AND flags != 'filter'", $uid);
-            $count = intval($res->fetchOneCell());
-            if ($count == 0) {
-                $page->assign('no_addr', true);
-                return;
-            }
+        $profile = Profile::get(Env::t('login'));
+        if (is_null($profile) || $profile->birthdate != $birth) {
+            $page->trigError('Les informations que tu as rentrées ne permettent pas de récupérer ton mot de passe.<br />'.
+                        'Si tu as un homonyme, utilise prenom.nom.promo comme login');
+            return;
+        }
 
-            $page->assign('ok', true);
-
-            $url   = rand_url_id();
-            XDB::execute('INSERT INTO  perte_pass (certificat,uid,created)
-                               VALUES  ({?},{?},NOW())', $url, $uid);
-            $res   = XDB::query('SELECT  email
-                                   FROM  emails
-                                  WHERE  uid = {?} AND email = {?}',
-                                $uid, Post::v('email'));
-            if ($res->numRows()) {
-                $mails = $res->fetchOneCell();
-            } else {
-                $res   = XDB::query('SELECT  email
-                                       FROM  emails
-                                      WHERE  uid = {?} AND NOT FIND_IN_SET("filter", flags)', $uid);
-                $mails = implode(', ', $res->fetchColumn());
+        $user = $profile->owner();
+        if ($user->state != 'active') {
+            $page->trigError('Ton compte n\'est pas activé.');
+            return;
+        }
+
+        if ($user->lost) {
+            $page->assign('no_addr', true);
+            return;
+        }
+
+        $page->assign('ok', true);
+
+        $url = rand_url_id();
+        XDB::execute('INSERT INTO  account_lost_passwords (certificat,uid,created)
+                           VALUES  ({?},{?},NOW())', $url, $user->id());
+        $to = XDB::fetchOneCell('SELECT  redirect
+                                   FROM  email_redirect_account
+                                  WHERE  uid = {?} AND redirect = {?}',
+                                $user->id(), Post::t('email'));
+        if (is_null($to)) {
+            $emails = XDB::fetchColumn('SELECT  redirect
+                                          FROM  email_redirect_account
+                                         WHERE  uid = {?} AND flags = \'inactive\' AND type = \'smtp\'',
+                                       $user->id());
+            $inactives_to = implode(', ', $emails);
+        }
+        $mymail = new PlMailer();
+        $mymail->setFrom('"Gestion des mots de passe" <support+password@' . $globals->mail->domain . '>');
+        if (is_null($to)) {
+            $mymail->addTo($user);
+            $log_to = $user->bestEmail();
+            if (!is_null($inactives_to)) {
+                $log_to = $inactives_to . ', ' . $log_to;
+                $mymail->addTo($inactives_to);
             }
-            $mymail = new PlMailer();
-            $mymail->setFrom('"Gestion des mots de passe" <support+password@' . $globals->mail->domain . '>');
-            $mymail->addTo($mails);
-            $mymail->setSubject('Ton certificat d\'authentification');
-            $mymail->setTxtBody("Visite la page suivante qui expire dans six heures :
+        } else {
+            $mymail->addTo($to);
+            $log_to = $to;
+        }
+        $mymail->setSubject("Ton certificat d'authentification");
+        $mymail->setTxtBody("Visite la page suivante qui expire dans six heures :
 {$globals->baseurl}/tmpPWD/$url
 
 Si en cliquant dessus tu n'y arrives pas, copie intégralement l'adresse dans la barre de ton navigateur. Si tu n'as pas utilisé ce lien dans six heures, tu peux tout simplement recommencer cette procédure.
 
 --
 Polytechnique.org
-\"Le portail des élèves & anciens élèves de l'Ecole polytechnique\"
+\"Le portail des élèves & anciens élèves de l'École polytechnique\"
 
-Email envoyé à ".Env::v('login') . (Post::has('email') ? "
-Adresse de secours : " . Post::v('email') : ""));
-            $mymail->send();
+Email envoyé à ".Env::v('login') . (is_null($to) ? '' : '
+Adresse de secours : ' . $to));
+        $mymail->send();
 
-            // on cree un objet logger et on log l'evenement
-            S::logger(uid)->log('recovery', $mails);
-        } else {
-            $page->trigError('Les informations que tu as rentrées ne permettent pas de récupérer ton mot de passe.<br />'.
-                        'Si tu as un homonyme, utilise prenom.nom.promo comme login');
+        S::logger($user->id())->log('recovery', $log_to);
+    }
+
+    function handler_recovery_ext($page)
+    {
+        $page->changeTpl('xnet/recovery.tpl');
+
+        if (!Post::has('login')) {
+            return;
         }
+
+        $user = User::getSilent(Post::t('login'));
+        if (is_null($user)) {
+            $page->trigError('Le compte n\'existe pas.');
+            return;
+        }
+        if ($user->state != 'active') {
+            $page->trigError('Ton compte n\'est pas activé.');
+            return;
+        }
+
+        $page->assign('ok', true);
+
+        $hash = rand_url_id();
+        XDB::execute('INSERT INTO  account_lost_passwords (uid, created, certificat)
+                           VALUES  ({?}, NOW(), {?})',
+                     $user->id(), $hash);
+
+        $mymail = new PlMailer('platal/password_recovery_xnet.mail.tpl');
+        $mymail->setTo($user);
+        $mymail->assign('hash', $hash);
+        $mymail->assign('email', Post::t('login'));
+        $mymail->send();
+
+        S::logger($user->id())->log('recovery', $user->bestEmail());
     }
 
-    function handler_tmpPWD(&$page, $certif = null)
+    function handler_tmpPWD($page, $certif = null)
     {
         global $globals;
-        // XXX: recovery requires data from the profile
-        XDB::execute('DELETE FROM  perte_pass
+        XDB::execute('DELETE FROM  account_lost_passwords
                             WHERE  DATE_SUB(NOW(), INTERVAL 380 MINUTE) > created');
 
-        $res = XDB::query('SELECT  uid
-                             FROM  perte_pass WHERE certificat={?}', $certif);
-        $ligne = $res->fetchOneAssoc();
-        if (!$ligne) {
-            $page->changeTpl('platal/index.tpl');
-            $page->kill("Cette adresse n'existe pas ou n'existe plus sur le serveur.");
-        }
-
-        $uid = $ligne["uid"];
-        if (Post::has('response2')) {
-            $password = Post::v('response2');
+        if (Post::has('pwhash') && Post::t('pwhash')) {
+            $uid = XDB::fetchOneCell('SELECT  uid
+                                        FROM  accounts
+                                       WHERE  hruid = {?}',
+                                     Post::t('username'));
+            $password = Post::t('pwhash');
             XDB::query('UPDATE  accounts
-                           SET  password={?}
+                           SET  password = {?}
                          WHERE  uid = {?} AND state = \'active\'',
                        $password, $uid);
-            XDB::query('DELETE FROM  perte_pass
-                              WHERE  certificat={?}', $certif);
+            XDB::query('DELETE FROM  account_lost_passwords
+                              WHERE  certificat = {?}', $certif);
 
             // If GoogleApps is enabled, and the user did choose to use synchronized passwords,
             // updates the Google Apps password as well.
@@ -396,14 +462,74 @@ Adresse de secours : " . Post::v('email') : ""));
             }
 
             S::logger($uid)->log("passwd", "");
+
+            // 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.
+            Platal::session()->start(AUTH_PASSWD);
+
             $page->changeTpl('platal/tmpPWD.success.tpl');
         } else {
-            $page->changeTpl('platal/motdepasse.tpl');
-            $page->addJsLink('motdepasse.js');
+            $res = XDB::query('SELECT  uid
+                                 FROM  account_lost_passwords
+                                WHERE  certificat = {?}', $certif);
+            $ligne = $res->fetchOneAssoc();
+            if (!$ligne) {
+                $page->changeTpl('platal/index.tpl');
+                $page->kill("Cette adresse n'existe pas ou n'existe plus sur le serveur.");
+            }
+
+            $hruid = XDB::fetchOneCell('SELECT  hruid
+                                          FROM  accounts
+                                         WHERE  uid = {?}',
+                                       $ligne['uid']);
+            $page->changeTpl('platal/password.tpl');
+            $page->assign('hruid', $hruid);
+            $page->assign('do_auth', 1);
+        }
+    }
+
+    function handler_register_ext($page, $hash = null)
+    {
+        XDB::execute('DELETE FROM  register_pending_xnet
+                            WHERE  DATE_SUB(NOW(), INTERVAL 1 MONTH) > date');
+        $res = XDB::fetchOneAssoc('SELECT  uid, hruid, email
+                                     FROM  register_pending_xnet
+                                    WHERE  hash = {?}',
+                                  $hash);
+
+        if (is_null($hash) || is_null($res)) {
+            $page->trigErrorRedirect('Cette adresse n\'existe pas ou n\'existe plus sur le serveur.', '');
+        }
+
+        if (Post::has('pwhash') && Post::t('pwhash')) {
+            XDB::startTransaction();
+            XDB::query('UPDATE  accounts
+                           SET  password = {?}, state = \'active\', registration_date = NOW()
+                         WHERE  uid = {?} AND state = \'pending\' AND type = \'xnet\'',
+                       Post::t('pwhash'), $res['uid']);
+            XDB::query('DELETE FROM  register_pending_xnet
+                              WHERE  uid = {?}',
+                              $res['uid']);
+            XDB::commit();
+
+            S::logger($res['uid'])->log('passwd', '');
+
+            // 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.
+            Post::kill('wait');
+            Platal::session()->startAvailableAuth();
+
+            $page->changeTpl('xnet/register.success.tpl');
+            $page->assign('email', $res['email']);
+        } else {
+            $page->changeTpl('platal/password.tpl');
+            $page->assign('xnet', true);
+            $page->assign('hruid', $res['hruid']);
+            $page->assign('do_auth', 1);
         }
     }
 
-    function handler_skin(&$page)
+    function handler_skin($page)
     {
         global $globals;
 
@@ -432,12 +558,18 @@ Adresse de secours : " . Post::v('email') : ""));
         $page->assign('skins', XDB::iterator($sql));
     }
 
-    function handler_exit(&$page, $level = null)
+    function handler_exit($page, $level = null)
     {
         if (S::suid()) {
-            S::logger()->log('suid_stop', S::user()->login() . " by " . S::suid('hruid'));
+            $old = S::user()->login();
+            S::logger()->log('suid_stop', $old . " by " . S::suid('hruid'));
             Platal::session()->stopSUID();
-            pl_redirect('admin/user/' . S::user()->login());
+            $target = S::s('suid_startpage');
+            S::kill('suid_startpage');
+            if (!empty($target)) {
+                http_redirect($target);
+            }
+            pl_redirect('admin/user/' . $old);
         }
 
         if ($level == 'forget' || $level == 'forgetall') {
@@ -448,8 +580,10 @@ Adresse de secours : " . Post::v('email') : ""));
             Platal::session()->killLoginFormCookies();
         }
 
-        S::logger()->log('deconnexion', @$_SERVER['HTTP_REFERER']);
-        Platal::session()->destroy();
+        if (S::logged()) {
+            S::logger()->log('deconnexion', @$_SERVER['HTTP_REFERER']);
+            Platal::session()->destroy();
+        }
 
         if (Get::has('redirect')) {
             http_redirect(rawurldecode(Get::v('redirect')));
@@ -458,11 +592,11 @@ Adresse de secours : " . Post::v('email') : ""));
         }
     }
 
-    function handler_review(&$page, $action = null, $mode = null) 
+    function handler_review($page, $action = null, $mode = null)
     {
         // Include X-XRDS-Location response-header for Yadis discovery
         global $globals;
-        header('X-XRDS-Location: ' . $globals->baseurl . '/openid/idp_xrds');
+        header('X-XRDS-Location: ' . $globals->baseurl . '/openid/xrds');
 
         $this->load('review.inc.php');
         $dom = 'Review';
@@ -478,7 +612,16 @@ Adresse de secours : " . Post::v('email') : ""));
         }
         $wiz->apply($page, 'review', $action, $mode);
     }
+
+    function handler_test_error($page, $mode = null)
+    {
+        if ($mode == 'js') {
+            $page->changeTpl('platal/error.tpl');
+        } else {
+            throw new Exception("Blih");
+        }
+    }
 }
 
-// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
+// vim:set et sw=4 sts=4 sws=4 foldmethod=marker fenc=utf-8:
 ?>