+ protected $auth;
+ protected $perms;
+ protected $type;
+
+ protected function __construct($auth = AUTH_PUBLIC, $perms = 'user', $type = DO_AUTH)
+ {
+ $this->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 /<matched path>/<user>/<token>/....). 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;