Skip to content

Instantly share code, notes, and snippets.

@mishak87
Last active December 19, 2015 19:29
Show Gist options
  • Save mishak87/6006876 to your computer and use it in GitHub Desktop.
Save mishak87/6006876 to your computer and use it in GitHub Desktop.
<?php
namespace ApiModule;
use Api\Authenticator,
Model\Rest,
Model\Action,
Mishak\Application\Response\JsonResponse,
Nette,
Nette\Application\UI\Presenter,
Nette\Application\AbortException,
Nette\Application\BadRequestException,
Nette\Application\ForbiddenRequestException;
abstract class ApiPresenter extends Presenter
{
protected $model;
protected $id;
protected $user;
// TODO add ACL
// TODO add privacy levels for data
// TODO add display wrappers
// TODO add data filters
public $autoCanonicalize = FALSE;
protected $data = array();
protected $format = 'json';
protected $parameters;
public function actionDefault()
{
try {
$request = $this->getRequest();
if (!$request->hasFlag('secured')) {
$this->sendError("API is available only via secured connection");
}
$parameters = $request->getParameters();
$username = NULL;
$token = NULL;
if (isset($parameters['username']) && isset($parameters['token'])) {
$this->authenticate($parameters['username'], $parameters['token']);
unset($parameters['username']);
unset($parameters['token']);
}
if ($auth = $this->getHttpRequest()->getHeader('Authorization')) {
if (substr($auth, 0, 6) !== 'Basic ') {
throw new BadRequestException("Unsupported authorization method");
}
$auth = base64_decode(substr($auth, 6));
if ($auth !== NULL && strpos($auth, ':') !== FALSE) {
$credentials = explode(':', $auth);
if (count($credentials) == 2) {
list($username, $token) = $credentials;
$this->authenticate($username, $token);
}
} else {
throw new BadRequestException("Unable to read authorization data");
}
}
if (isset($parameters['format'])) {
$this->format = $parameters['format'];
unset($parameters['format']);
}
$this->parameters = $parameters;
$this->callActionFromRequest();
$this->sendData();
} catch (AbortException $e) {
throw $e;
} catch (BadRequestException $e) {
$response = $this->getHttpResponse();
$response->setCode($e->getCode());
if ($e->getCode() == 401) {
$response->setHeader('WWW-Authenticate', 'Basic realm="' . $e->getMessage() . '. (Password is your API TOKEN)"');
}
$this->sendError($e->getMessage());
} catch (\Exception $e) {
$this->sendError($e->getMessage());
}
}
private function sendError($message)
{
$this->data = array(
'status' => 'error',
'error' => $message,
);
$this->sendData();
}
private function authenticate($username, $token)
{
try {
$user = $this->context->user;
$user->getStorage()->setNamespace('api');
$user->setAuthenticator(new Authenticator($this->model('user')));
$user->login($username, $token);
} catch (Nette\Security\AuthenticationException $e) {
throw new BadRequestException("Invalid username or token", 401, $e);
}
}
private function sendData()
{
$this->sendResponse(new JsonResponse($this->data));
}
private function callActionFromRequest()
{
$request = $this->getRequest();
$method = $request->getMethod();
$this->id = $id = $this->getParam('id');
$action = Rest::getActionFromHttpMethod($method);
switch ($action) {
case Action::DISPLAY:
case Action::REPLACE:
case Action::DELETE:
if ($id === NULL) {
$action .= 'Collection';
}
break;
case Action::CREATE:
if ($id !== NULL) {
throw new BadRequestException("For updating or replacing item use put or partial request.", 405);
}
break;
case Action::UPDATE:
case Action::REPLACE:
if ($id === NULL) {
throw new BadRequestException(ucfirst($action) . " is not supported for whole collection.", 405);
}
break;
default:
throw new BadRequestException("Unsupported request method: '$method'.", 405);
}
$this->$action();
}
/**
* REST Actions
*/
public function show()
{
$this->output();
}
public function showCollection()
{
$this->output();
}
public function display()
{
$action = Action::DISPLAY;
$resource = $this->resource();
$model = $this->getModel();
$model->isSupported($action, $resource);
$this->hasPermission($action, $resource);
$this->output($resource);
}
public function create()
{
$action = Action::CREATE;
$data = $this->data();
$model = $this->getModel();
$model->isSupported($action);
$this->hasPermission($action);
$data = $model->filter($data) + $model->defaults($action);
$model->validate($action, $data);
$data = $model->process($data);
$resource = $model->create($data);
$this->output($resource);
}
public function replace()
{
$action = Action::REPLACE;
$resource = $this->resource();
$data = $this->data();
$model = $this->getModel();
$model->isSupported($action, $resource);
$this->hasPermission($action, $resource);
$data = $model->filter($data) + $model->defaults($action);
$model->validate($action, $data);
$data = $model->process($data);
$resource = $model->replace($resource, $data);
$this->output($resource);
}
public function update()
{
$action = Action::UPDATE;
$resource = $this->resource();
$data = $this->data();
$model = $this->getModel();
$model->isSupported($action, $resource);
$this->hasPermission($action, $resource);
$data = $model->filter($data) + $model->defaults($action);
$model->validate($action, $data);
$data = $model->process($data);
$model->update($resource, $data);
$this->output($resource);
}
public function delete()
{
$action = Action::DELETE;
$resource = $this->resource();
$model = $this->getModel();
$model->isSupported($action, $resource);
$this->hasPermission($action, $resource);
$model->delete($resource);
$this->output($resource);
}
protected $input;
protected function data()
{
return $this->input ?: $this->parameters;
}
/**
* Authentication and stuff
*/
protected function authRequired($resource, $action)
{
$user = $this->getApiUser();
if (!$user) {
throw new \Exception("Authentication required");
}
if (!$this->hasPermission($resource, $action)) {
throw new \Exception("You have no permission to perform '$action' on resource");
}
}
protected function hasPermission($resource, $action)
{
return FALSE;
}
/**
* Utility functions
*/
public function resource()
{
if ($this->model === NULL) {
throw new \Exception("Model is not specified.");
}
$model = $this->getModel();
$table = $this->table($this->model);
// TODO limit scope and "total" access rights
if ($this->id !== NULL) {
$resource = $table->get($this->id);
$resource = $this->process($resource);
if (!$resource) {
throw new BadRequestException("Resource #{$this->id} not found");
}
} else {
$resource = $this->process($table);
}
return $resource;
}
/**
* Add additional filters and stuff
* @resource object|Nette\Database\Table\Selection
*/
protected function process($resource)
{
return $resource;
}
protected function output($resource = NULL)
{
$resource = $resource ?: $this->resource();
$model = $this->model($this->model);
if ($resource instanceof Nette\Database\Table\ActiveRow) {
$output = $model->output($resource);
} else {
$output = array();
foreach ($resource as $resource) {
$output[] = $model->output($resource);
}
}
$this->data = $output;
}
/**
* helpers
*/
protected function getModel()
{
return $this->model($this->model);
}
public function model($name)
{
return $this->context->modelManager->model($name);
}
public function table($name)
{
return $this->context->modelManager->table($name);
}
}
<?php
namespace ApiModule;
use Nette,
Nette\Application\Request;
class Router extends Nette\Object implements Nette\Application\IRouter
{
const FORMAT_KEY = 'format';
const METHOD_KEY = 'method';
const FORMAT_JSON = 'json';
private $defaults = array(
self::FORMAT_KEY => self::FORMAT_JSON,
'id' => NULL,
);
function getFormats()
{
return array(
self::FORMAT_JSON
);
}
function getMethods()
{
return array(
'GET',
'PUT',
'POST',
'PARTIAL',
'DELETE',
);
}
/**
* /api/(<namespace>/)*resource(/<id>)?(.<format json|xml...>)
*/
function match(Nette\Http\IRequest $httpRequest)
{
$presenter = NULL;
$method = NULL;
$id = NULL;
$match = preg_match('~^api/(?P<resource>[a-z]+(/[a-z]+)*)(/(?P<id>[1-9][0-9]*))?(.(?P<format>json))?$~i', $httpRequest->getUrl()->getPathInfo(), $matches);
if (!$match) {
return NULL;
}
$matches += array(
'format' => self::FORMAT_JSON,
'user' => '',
'id' => '',
);
$params = array();
if ($matches['id'] !== '') {
$params['id'] = $matches['id'];
}
if ($matches['user'] !== '') {
$params['user'] = $matches['user'];
}
$slugs = explode('/', $matches['resource'], 2);
$first = array_shift($slugs);
if (!$slugs) {
array_push($slugs, 'default');
} else {
$last = array_pop($slugs);
$last = strtr(ucwords($last), '/', '');
array_push($slugs, $last);
}
array_unshift($slugs, 'api');
array_unshift($slugs, $first);
$presenter = ucwords(implode(':', $slugs));
unset($params['resource']);
$method = $httpRequest->getMethod();
if (in_array($method, array('POST', 'PARTIAL', 'PUT'))) {
if ($httpRequest->getHeader('Content-Type') === 'application/json') {
$recieved = (array) json_decode(file_get_contents('php://input'), TRUE);
} else {
$recieved = $httpRequest->getPost();
}
} else {
$recieved = array();
}
$params = $params + $httpRequest->getQuery() + $recieved + $this->defaults;
if (isset($params[self::METHOD_KEY])) {
$method = strtoupper($params[self::METHOD_KEY]);
$this->validateMethod($method);
unset($params[self::METHOD_KEY]);
}
$params[self::FORMAT_KEY] = strtolower($params[self::FORMAT_KEY]);
$this->validateFormat($params[self::FORMAT_KEY]);
return new Request(
$presenter,
$method,
$params,
$httpRequest->getPost(),
$httpRequest->getFiles(),
array(Request::SECURED => $httpRequest->isSecured())
);
}
private function validateFormat($format)
{
if (!in_array($format, $this->getFormats(), TRUE)) {
throw new Nette\InvalidStateException("Invalid format '$format' supported formats: " . implode(', ', $this->getFormats()) . ".");
}
}
private function validateMethod($method)
{
if (!in_array($method, $this->getMethods(), TRUE)) {
throw new Nette\InvalidStateException("Invalid method '$method' supported methods: " . implode(', ', $this->getMethods()) . ".");
}
}
function constructUrl(Request $appRequest, Nette\Http\Url $refUrl)
{
return NULL;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment