X-Git-Url: http://git.polytechnique.org/?a=blobdiff_plain;f=classes%2Fplatal.php;h=c442ce1fb00aee4002f64bd5dcfb1a5f44687f7b;hb=refs%2Fheads%2Fcore%2Fmaster;hp=94d9632c708185519d2396e3f96c3e8498569a3c;hpb=30b4d2149c5fa19fccb09a4f6b286b3a52344245;p=platal.git diff --git a/classes/platal.php b/classes/platal.php index 94d9632..c442ce1 100644 --- a/classes/platal.php +++ b/classes/platal.php @@ -1,6 +1,6 @@ auth = $auth; + $this->perms = $perms; + $this->type = $type; + } + + public function checkPerms() + { + // Don't check permissions if there are no permission requirement + // (either no requested group membership, or public auth is allowed). + return !$this->perms || $this->auth == AUTH_PUBLIC || + Platal::session()->checkPerms($this->perms); + } + + public function hasType($type) + { + return ($this->type & $type) == $type; + } + + abstract protected function run(PlPage $page, array $args); + + public function call(PlPage $page, array $args) + { + global $globals, $session, $platal; + if (!$session->checkAuth($this->auth)) { + if ($this->hasType(DO_AUTH)) { + if (!$session->start($this->auth)) { + $platal->force_login($page); + return PL_FORBIDDEN; + } + } else { + return PL_FORBIDDEN; + } + } + if (!$this->checkPerms()) { + if (Platal::notAllowed()) { + return PL_FORBIDDEN; + } + } + return $this->run($page, $args); + } +} + +/** The standard plat/al hook, for interactive requests. + * It optionally does active authentication (DO_AUTH). The handler is invoked + * with the PlPage object, and with each of the remaining path components. + */ +class PlStdHook extends PlHook +{ + private $callback; + + public function __construct($callback, $auth = AUTH_PUBLIC, $perms = 'user', $type = DO_AUTH) + { + parent::__construct($auth, $perms, $type); + $this->callback = $callback; + } + + protected function run(PlPage $page, array $args) + { + global $session, $platal; + + $args[0] = $page; + $val = call_user_func_array($this->callback, $args); + if ($val == PL_DO_AUTH) { + if (!$session->start($session->loggedLevel())) { + $platal->force_login($page); + } + $val = call_user_func_array($this->callback, $args); + } + return $val; + } +} + +/** A specialized hook for API requests. + * It is intended to be used for passive API requests, authenticated either by + * an existing session (with a valid XSRF token), or by an alternative single + * request auth mechanism implemented by PlSession::apiAuth. + * + * This hook is suitable for read-write requests against the website, provided + * $auth is set appropriately. Note that the auth level is only checked for + * session-authenticated users, as "apiAuth" users are assumed to always have + * the requested level (use another hook otherwise). + * + * The callback will be passed as arguments the PlPage, the authenticated + * PlUser, the JSON decoded payload, and the remaining path components, as with + * any other hook. + * + * If the callback intends to JSON-encode its returned value, it is advised to + * use PlPage::jsonAssign, and return PL_JSON to enable automatic encoding. + */ +class PlApiHook extends PlHook +{ + private $actualAuth; + private $callback; + + public function __construct($callback, $auth = AUTH_PUBLIC, $perms = 'user', $type = NO_AUTH) + { + // As mentioned above, $auth is only applied for session-based auth + // (as opposed to token-based). PlHook is initialized to AUTH_PUBLIC to + // avoid it refusing to approve requests; this is important as the user + // is not yet authenticated at that point (see below for the actual + // permissions check). + parent::__construct(AUTH_PUBLIC, $perms, $type); + $this->actualAuth = $auth; + $this->callback = $callback; + } + + private function getEncodedPayload($method) + { + return $method == "GET" ? "" : file_get_contents("php://input"); + } + + private function decodePayload($encodedPayload) + { + return empty($encodedPayload) ? array() : json_decode($encodedPayload, true); + } + + protected function run(PlPage $page, array $args) + { + $method = $_SERVER['REQUEST_METHOD']; + $encodedPayload = $this->getEncodedPayload($method); + $jsonPayload = $this->decodePayload($encodedPayload); + $resource = '/' . implode('/', $args); + + // If the payload wasn't a valid JSON encoded object, bail out early. + if (is_null($jsonPayload)) { + $page->trigError("Could not decode the JSON-encoded payload sent with the request."); + return PL_BAD_REQUEST; + } + + // Authenticate the request. Try first with the existing session (which + // is less expensive to check), by veryfing that the XSRF token is + // valid; otherwise fallbacks to API-type authentication from PlSession. + if (S::logged() && S::has_xsrf_token() && Platal::session()->checkAuth($this->actualAuth)) { + $user = S::user(); + } else { + $user = Platal::session()->apiAuth($method, $resource, $encodedPayload); + } + + // Check the permissions, unless the handler is fully public. + if ($this->actualAuth > AUTH_PUBLIC) { + if (is_null($user) || !$user->checkPerms($this->perms)) { + return PL_FORBIDDEN; + } + } + + // Invoke the callback, whose signature is (PlPage, PlUser, jsonPayload). + array_shift($args); + array_unshift($args, $page, $user, $jsonPayload); + return call_user_func_array($this->callback, $args); + } +} + +/** A specialized hook for token-based requests. + * It is intended for purely passive requests (typically for serving CSV or RSS + * content outside the browser), and can fallback to regular session-based + * authentication when the token is not valid/available. + * + * Note that $auth is only applied for session-backed authentication; it is + * assumed that token-based auth is always enough for the hook (otherwise, just + * use PlStdHook above). + * + * Also, this hook requires that the first two unmatched path components are the + * user and token (for instance ////....). They will + * be popped before being passed to the handler, and replaced by the request's + * PlUser object. + */ +class PlTokenHook extends PlHook +{ + private $actualAuth; + private $callback; + + public function __construct($callback, $auth = AUTH_PUBLIC, $perms = 'user', $type = NO_AUTH) + { + // See PlApiHook::__construct. + parent::__construct(AUTH_PUBLIC, $perms, $type); + $this->actualAuth = $auth; + $this->callback = $callback; + } + + protected function run(PlPage $page, array $args) + { + // Retrieve the user, either from the session (less expensive, as it is + // already there), or from the in-path (user, token) pair. + if (S::logged() && Platal::session()->checkAuth($this->actualAuth)) { + $user = S::user(); + } else { + $user = Platal::session()->tokenAuth(@$args[1], @$args[2]); + } + + // Check the permissions, unless the handler is fully public. + if ($this->actualAuth > AUTH_PUBLIC) { + if (is_null($user) || !$user->checkPerms($this->perms)) { + return PL_FORBIDDEN; + } + } + + // Replace the first three remaining elements of the path with the + // PlPage and PlUser objects. + array_shift($args); + $args[0] = $page; + $args[1] = $user; + return call_user_func_array($this->callback, $args); + } +} + +/** A specialized plat/al hook for serving wiki pages. + */ +class PlWikiHook extends PlHook +{ + public function __construct($auth = AUTH_PUBLIC, $perms = 'user', $type = DO_AUTH) + { + parent::__construct($auth, $perms, $type); + } + + protected function run(PlPage $page, array $args) + { + return PL_WIKI; + } +} + +class PlHookTree +{ + public $hook = null; + public $aliased = null; + public $children = array(); + + public function addChildren(array $hooks) + { + global $platal; + foreach ($hooks as $path=>$hook) { + $path = explode('/', $path); + $element = $this; + foreach ($path as $next) { + $alias = null; + if ($next{0} == '%') { + $alias = $next; + $next = $platal->hook_map(substr($next, 1)); + } + if (!isset($element->children[$next])) { + $child = new PlHookTree(); + $child->aliased = $alias; + $element->children[$next] = $child; + } else { + $child = $element->children[$next]; + } + $element = $child; + } + $element->hook = $hook; + } + } + + public function findChild(array $path) + { + $remain = $path; + $matched = array(); + $aliased = array(); + $element = $this; + while (true) + { + $next = @$remain[0]; + if ($element->aliased) { + $aliased = $matched; + } + if (empty($next) || !isset($element->children[$next])) { + break; + } + $element = $element->children[$next]; + array_shift($remain); + $matched[] = $next; + } + return array($element->hook, $matched, $remain, $aliased); + } + + private function findNearestChildAux(array $remain, array $matched, array $aliased) + { + $next = @$remain[0]; + if ($this->aliased) { + $aliased = $matched; + } + if (!empty($next)) { + $child = @$this->children[$next]; + if (!$child) { + $nearest_lev = 50; + $nearest_sdx = 50; + $match = null; + foreach ($this->children as $path=>$hook) { + if ($path) { + $lev = levenshtein($next, $path); + if ($lev <= $nearest_lev + && ($lev < strlen($next) / 2 || strpos($next, $path) !== false + || strpos($path, $next) !== false)) { + $sdx = levenshtein(soundex($next), soundex($path)); + if ($lev == $nearest_lev || $sdx < $nearest_sdx) { + $child = $hook; + $nearest_lev = $lev; + $nearest_sdx = $sdx; + $match = $path; + } + } + } + } + $next = $match; + } + if ($child) { + array_shift($remain); + $matched[] = $next; + return $child->findNearestChildAux($remain, $matched, $aliased); + } + if (($pos = strpos($next, '.php')) !== false) { + $remain[0] = substr($next, 0, $pos); + return $this->findNearestChildAux($remain, $matched, $aliased); + } + } + return array($this->hook, $matched, $remain, $aliased); + } + + public function findNearestChild(array $path) + { + return $this->findNearestChildAux($path, array(), array()); + } +} abstract class Platal { - private $__mods; - private $__hooks; + private $mods; + private $hooks; protected $https; public $ns; public $path; - public $argv; + public $argv = array(); static private $_page = null; public function __construct() { global $platal, $session, $globals; - $platal =& $this; - $globalclass = PL_GLOBALS_CLASS; - $globals = new $globalclass(); + $platal = $this; + + /* Assign globals first, then call init: init must be used for operations + * that requires access to the content of $globals (e.g. XDB requires + * $globals to be assigned. + */ + $globals = $this->buildGlobals(); $globals->init(); - $sessionclass = PL_SESSION_CLASS; - $session = new $sessionclass(); + + /* Get the current session: assign first, then activate the session. + */ + $session = $this->buildSession(); if (!$session->startAvailableAuth()) { - Platal::page()->trigError('Données d\'authentification invalide.'); + Platal::page()->trigError("Données d'authentification invalides."); } $modules = func_get_args(); - if (is_array($modules[0])) { + if (isset($modules[0]) && is_array($modules[0])) { $modules = $modules[0]; } $this->path = trim(Get::_get('n', null), '/'); - $this->__mods = array(); - $this->__hooks = array(); + $this->mods = array(); + $this->hooks = new PlHookTree(); array_unshift($modules, 'core'); foreach ($modules as $module) { $module = strtolower($module); - $this->__mods[$module] = $m = PLModule::factory($module); - $this->__hooks += $m->handlers(); + $this->mods[$module] = $m = PLModule::factory($module); + $this->hooks->addChildren($m->handlers()); } if ($globals->mode == '') { @@ -84,166 +425,51 @@ abstract class Platal return join('/', array_slice($this->argv, 0, $n)); } - protected function find_hook() + public static function wiki_hook($auth = AUTH_PUBLIC, $perms = 'user', $type = DO_AUTH) { - $p = $this->path; - - while ($p) { - if (array_key_exists($p, $this->__hooks)) - break; - - $p = substr($p, 0, strrpos($p, '/')); - } - - if (empty($this->__hooks[$p])) { - return null; - } - - $hook = $this->__hooks[$p]; - - if (!is_callable($hook['hook'])) { - return null; - } - - $this->https = ($hook['type'] & NO_HTTPS) ? false : true; - $this->argv = explode('/', substr($this->path, strlen($p))); - $this->argv[0] = $p; - - return $hook; + return new PlWikiHook($auth, $perms, $type); } - protected function find_nearest_key($key, array &$array) + public function hook_map($name) { - $keys = array_keys($array); - if (in_array($key, $keys)) { - return $key; - } - - if (($pos = strpos($key, '.php')) !== false) { - $key = substr($key, 0, $pos); - } - - $has_end = in_array("#final#", $keys); - if (strlen($key) > 24 && $has_end) { - return "#final#"; - } - - foreach ($keys as $k) { - if ($k == "#final#") { - continue; - } - $lev = levenshtein($key, $k); - - if ((!isset($val) || $lev < $val) - && ($lev <= strlen($k)/2 || strpos($k, $key) !== false || strpos($key, $k) !== false)) { - $val = $lev; - $best = $k; - } - } - if (!isset($best) && $has_end) { - return "#final#"; - } else if (isset($best)) { - return $best; - } return null; } - public function near_hook() + protected function find_hook() { - $hooks = array(); - $leafs = array(); - foreach ($this->__hooks as $hook=>$handler) { - if (!$this->check_perms($handler['perms'])) { - continue; - } - $parts = split('/', $hook); - $place =& $hooks; - foreach ($parts as $part) { - if (!isset($place[$part])) { - $place[$part] = array(); - } - $place =& $place[$part]; - } - $leaf = $parts[count($parts)-1]; - if (!isset($leafs[$leaf])) { - $leafs[$leaf] = $hook; - } else if (is_array($leafs[$leaf])) { - $leafs[$leaf][] = $hook; - } else { - $leafs[$leaf] = array($hook, $leafs[$leaf]); - } - $place["#final#"] = array(); - } - - // search for the nearest full path - $p = split('/', $this->path); - $place =& $hooks; - $link = ''; - foreach ($p as $k) { - if (!isset($ended)) { - $key = $this->find_nearest_key($k, $place); - } else { - $key = $k; - } - if ($key == "#final#") { - if (!array_key_exists($link, $this->__hooks)) { - $link = ''; - break; - } - $key = $k; - $ended = true; - } - if (!is_null($key)) { - if (!empty($link)) { - $link .= '/'; - } - $link .= $key; - $place =& $place[$key]; - } else { - $link = ''; - break; - } - } - if ($link == $this->path) { - $link = ''; - } - if ($link && levenshtein($link, $this->path) < strlen($link)/3) { - return $link; - } - - // search for missing namespace (the given name is a leaf) - $leaf = array_shift($p); - $args = count($p) ? '/' . implode('/', $p) : ''; - if (isset($leafs[$leaf]) && !is_array($leafs[$leaf]) && $leafs[$leaf] != $this->path) { - return $leafs[$leaf] . $args; + $p = explode('/', $this->path); + list($hook, $matched, $remain, $aliased) = $this->hooks->findChild($p); + if (empty($hook)) { + return null; } - unset($val); - $best = null; - foreach ($leafs as $k=>&$path) { - if (is_array($path)) { - continue; - } - $lev = levenshtein($leaf, $k); - - if ((!isset($val) || $lev < $val) - && ($lev <= strlen($k)/2 || strpos($k, $leaf) !== false || strpos($leaf, $k) !== false)) { - $val = $lev; - $best = $path; - } + $this->argv = $remain; + array_unshift($this->argv, implode('/', $matched)); + if (!empty($aliased)) { + $this->ns = implode('/', $aliased) . '/'; } - return $best == null ? ( $link ? $link : null ) : $best . $args; + $this->https = !$hook->hasType(NO_HTTPS); + return $hook; } - protected function check_perms($perms) + public function near_hook() { - if (!$perms) { // No perms, no check - return true; + $p = explode('/', $this->path); + list($hook, $matched, $remain, $aliased) = $this->hooks->findNearestChild($p); + if (empty($hook)) { + return null; + } + $url = implode('/', $matched); + if (!empty($remain)) { + $url .= '/' . implode('/', $remain); + } + if ($url == $this->path || levenshtein($url, $this->path) > strlen($url) / 3 + || !$hook->checkPerms()) { + return null; } - $s_perms = S::v('perms'); - return $s_perms->hasFlagCombination($perms); + return $url; } - private function call_hook(PlPage &$page) + private function call_hook(PlPage $page) { $hook = $this->find_hook(); if (empty($hook)) { @@ -254,36 +480,17 @@ abstract class Platal http_redirect('https://' . $globals->core->secure_domain . $_SERVER['REQUEST_URI']); } - $args = $this->argv; - $args[0] =& $page; - - if ($hook['auth'] > S::v('auth', AUTH_PUBLIC)) { - if ($hook['type'] & DO_AUTH) { - if (!$session->start($hook['auth'])) { - $this->force_login($page); - } - } else { - return PL_FORBIDDEN; - } - } - if ($hook['auth'] != AUTH_PUBLIC && !$this->check_perms($hook['perms'])) { - return PL_FORBIDDEN; - } - - $val = call_user_func_array($hook['hook'], $args); - if ($val == PL_DO_AUTH) { - // The handler need a better auth with the current args - if (!$session->start($session->loggedLevel())) { - $this->force_login($page); - } - $val = call_user_func_array($hook['hook'], $args); - } - return $val; + return $hook->call($page, $this->argv); } /** Show the authentication form. */ - abstract public function force_login(PlPage& $page); + abstract public function force_login(PlPage $page); + + protected function report_error($error) + { + PlErrorReport::report($error); + } public function run() { @@ -293,26 +500,49 @@ abstract class Platal $this->path = 'index'; } - $page->assign('platal', $this); - switch ($this->call_hook($page)) { - case PL_FORBIDDEN: - $this->__mods['core']->handler_403($page); - break; - - case PL_NOT_FOUND: - $this->__mods['core']->handler_404($page); - break; + try { + $page->assign('platal', $this); + $res = $this->call_hook($page); + switch ($res) { + case PL_BAD_REQUEST: + $this->mods['core']->handler_400($page); + break; + + case PL_FORBIDDEN: + $this->mods['core']->handler_403($page); + break; + + case PL_NOT_FOUND: + $this->mods['core']->handler_404($page); + break; + + case PL_WIKI: + return PL_WIKI; + } + } catch (Exception $e) { + header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error'); + $this->report_error($e); + if (self::globals()->debug) { + $page->kill(pl_entities($e->getMessage()) + . '
' . pl_entities("" . $e) . '
'); + } else { + $page->kill(pl_entities($e->getMessage())); + } } $page->assign('platal', $this); - $page->run(); + if ($res == PL_JSON) { + $page->runJSon(); + } else { + $page->run(); + } } public function error403() { $page =& self::page(); - $this->__mods['core']->handler_403($page); + $this->mods['core']->handler_403($page); $page->assign('platal', $this); $page->run(); } @@ -321,20 +551,30 @@ abstract class Platal { $page =& self::page(); - $this->__mods['core']->handler_404($page); + $this->mods['core']->handler_404($page); $page->assign('platal', $this); $page->run(); } + public static function notAllowed() + { + if (S::admin()) { + self::page()->trigWarning('Tu accèdes à cette page car tu es administrateur du site.'); + return false; + } else { + return true; + } + } + public static function load($modname, $include = null) { global $platal; $modname = strtolower($modname); - if (isset($platal->__mods[$modname])) { + if (isset($platal->mods[$modname])) { if (is_null($include)) { return; } - $platal->__mods[$modname]->load($include); + $platal->mods[$modname]->load($include); } else { if (is_null($include)) { require_once PLModule::path($modname) . '.php'; @@ -344,22 +584,65 @@ abstract class Platal } } + public static function assert($cond, $error, $userfriendly = null) + { + if ($cond === false) { + if ($userfriendly == null) { + $userfriendly = "Une erreur interne s'est produite. + Merci de réessayer la manipulation qui a déclenché l'erreur ; + si cela ne fonctionne toujours pas, merci de nous signaler le problème rencontré."; + } + throw new PlException($userfriendly, $error); + } + } + + public function &buildLogger($uid, $suid = 0) + { + if (defined('PL_LOGGER_CLASS')) { + $class = PL_LOGGER_CLASS; + $logger = new $class($uid, $suid); + return $logger; + } else { + return PlLogger::dummy($uid, $suid); + } + } + + protected function &buildPage() + { + $pageclass = PL_PAGE_CLASS; + $page = new $pageclass(); + return $page; + } + static public function &page() { - global $platal; if (is_null(self::$_page)) { - $pageclass = PL_PAGE_CLASS; - self::$_page = new $pageclass(); + global $platal; + self::$_page = $platal->buildPage(); } return self::$_page; } + protected function &buildSession() + { + $sessionclass = PL_SESSION_CLASS; + $session = new $sessionclass(); + return $session; + } + static public function &session() { global $session; return $session; } + protected function &buildGlobals() + { + $globalclass = PL_GLOBALS_CLASS; + $globals = new $globalclass(); + return $globals; + } + static public function &globals() { global $globals; @@ -367,5 +650,5 @@ abstract class Platal } } -// 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: ?>