}
}
+/** 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
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).
+ // See PlApiHook::__construct.
parent::__construct(AUTH_PUBLIC, $perms, $type);
$this->actualAuth = $auth;
$this->callback = $callback;
return new PlStdHook(array($this, 'handler_' . $fun), $auth, $perms, $type);
}
+ /** Register an API hook.
+ * @param fun name of the handler (the exact name will be handler_$fun); the
+ * handler will be invoked with a PlPage, the authenticated PlUser, the
+ * JSON-decoded payload (if any), and the unmatched path components
+ * @param auth authentification level required, when not API-authenticated
+ * @param perms permission required to run this handler
+ * @param type additionnal flags (only NO_HTTPS is supported at the moment)
+ *
+ * See {@link make_hook} above for details on permissions.
+ *
+ * WARNING: It is expected that the API authentication mechanism will not be
+ * protected against short-term replay of requests (for instance replay of a
+ * given request within 5-10 seconds).
+ *
+ * You are explicitly advised to make any API request idempotent (eg. use
+ * "DELETE /api/email/foo@example.com" instead of "DELETE /api/email/0" to
+ * delete the first email in a list).
+ */
+ public function make_api_hook($fun, $auth, $perms = 'user', $type = NO_AUTH)
+ {
+ return new PlApiHook(array($this, 'handler_' . $fun), $auth, $perms, $type);
+ }
+
/** Register a token-authentified hook (rss, csv, ical, ...)
* @param fun name of the handler (the exact name will be handler_$fun); the
* handler will be invoked with the PlPage object, the PlUser of the
*/
abstract protected function startSessionAs($user, $level);
+ /** Authenticate the request for the given (method, payload) pair.
+ *
+ * Implementations are expected to provide strong authentication. It is
+ * suggested to use an HMAC-based scheme, where the signature validates the
+ * method, url, and payload (to avoid replay of the signature against other
+ * methods), and the timestamp (to avoid replay in time).
+ *
+ * @param method method of the request (GET, POST, PUT, DELETE)
+ * @param resource URL path of the resource (eg. "/api/user")
+ * @param payload binary payload sent with the request (before decoding)
+ * @return a valid PlUser object if authentication is successfull, or null.
+ */
+ public function apiAuth($method, $resource, $payload)
+ {
+ return null; // Default implementation does nothing
+ }
+
/** Check authentication with the given token.
*
* Token authentication is a light-weight authentication based on a user-specific token.