Commit 5f947b02 authored by Karamel's avatar Karamel
Browse files

Add details in APIResult when requesting an invalid API.

parent 47ecf4f9
<?php
// Pastèque API
//
// Copyright (C) 2012-2015 Scil (http://scil.coop)
// Cédric Houbart, Philippe Pary
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pastèque is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server\Exception;
/**
* The requested API was not found.
*/
class APINotFoundException extends \Exception implements PastequeException
{
private $api;
private $action;
private $minArgc;
private $maxArgc;
private $givenArgc;
private function constructAPI($api) {
$this->api = $api;
$msg = sprintf("API %s doesn't exist.", $this->getJsonableApi());
parent::__construct($msg);
}
private function constructAction($api, $action) {
$this->api = $api;
$this->action = $action;
$msg = sprintf("API %s->%s doesn't exist.",
$this->getJsonableApi(), $this->getJsonableAction());
parent::__construct($msg);
}
private function constructArgs($api, $action,
$minArgc, $maxArgc, $givenArgc) {
$this->api = $api;
$this->action = $action;
$this->minArgc = $minArgc;
$this->maxArgc = $maxArgc;
$this->givenArgc = $givenArgc;
$msg = '';
if ($minArgc == $maxArgc) {
$msg = sprintf('API %s->%s expects %d arguments (%d given).',
$this->getJsonableAPI(), $this->getJsonableAction(),
$this->minArgc, $this->givenArgc);
} else {
$msg = sprintf('API %s->%s expects between %d and %s arguments (%d given).',
$this->getJsonableAPI(), $this->getJsonableAction(),
$this->minArgc, $this->maxArgc, $this->givenArgc);
}
parent::__construct($msg);
}
private function constructArgsInvalid($api, $action) {
$this->api = $api;
$this->action = $action;
$msg = sprintf('API %s->%s was called with an invalid number of arguments.',
$this->getJsonableAPI(), $this->getJsonableAction());
parent::__construct($msg);
}
public function __construct($api, $action = null,
$minArgc = null, $maxArgc = null, $givenArgc = null) {
if ($action === null) {
$this->constructAPI($api);
} elseif ($action !== null && ($minArgc === null
|| $maxArgc === null || $givenArgc === null)) {
$this->constructAction($api, $action);
} elseif ($minArgc !== null && $maxArgc !== null
&& $givenArgc !== null) {
$this->constructArgs($api, $action,
$minArgc, $maxArgc, $givenArgc);
} else {
$this->constructArgsInvalid($api, $action);
}
}
public function getAPI() {
return $this->api;
}
public function getAction() {
return $this->action;
}
public function getMinArgc() {
return $this->minArgc;
}
public function getMaxArgc() {
return $this->maxArgc;
}
public function getGivenArgc() {
return $this->givenArgc;
}
public function getJsonableAPI() {
if (gettype($this->api) == 'resource') {
return '<resource>';
} else {
return $this->api;
}
}
public function getJsonableAction() {
if (gettype($this->action) == 'resource') {
return '<resource>';
} else {
return $this->action;
}
}
public function toStruct() {
return [
'api' => $this->getJsonableAPI(),
'action' => $this->getJsonableAction(),
'minArgc' => $this->minArgc,
'maxArgc' => $this->maxArgc,
'givenArgc' => $this->givenArgc,
'msg' => $this->message,
];
}
}
<?php
// Pastèque API
//
// Copyright (C) 2012-2015 Scil (http://scil.coop)
// Cédric Houbart, Philippe Pary
//
// This file is part of Pastèque.
//
// Pastèque is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pastèque is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Pastèque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server\Exception;
/**
* Business exceptions. Exceptions implementing it are high-level exception
* that can be passed to the client as a result of an API call.
*/
interface PastequeException
{
/** Transform the exception into a dictionary. */
public function toStruct();
}
......@@ -20,6 +20,7 @@
namespace Pasteque\Server\System\API;
use \Pasteque\Server\Exception\APINotFoundException;
use \Pasteque\Server\System\Login;
/** Utility class to call API methods. */
......@@ -32,10 +33,12 @@ class APICaller {
$name = substr($name, 0, -3);
}
$name = ucfirst($name);
return $name;
return sprintf('%sAPI', $name);
}
/** Run an API method and wrap the response in an APIResult. */
/**
* Run an API method and wrap the response in an APIResult.
*/
public static function run($app, $apiName, $methodName, $args = array()) {
if ($args === null) { $args = array(); }
if (!is_array($args)) { $args = array($args); }
......@@ -43,12 +46,12 @@ class APICaller {
$apiName = APICaller::formatAPIName($apiName);
$apiPkg = $app->isFiscalMirror() && strtolower($apiName) !== 'login' ?
'FiscalMirrorAPI' : 'API'; // TODO: ugly exception
$className = sprintf('\Pasteque\Server\%s\%sAPI', $apiPkg, $apiName);
$className = sprintf('\Pasteque\Server\%s\%s', $apiPkg, $apiName);
if (class_exists($className, true)) {
// Class found, get requested method
$apiClass = new \ReflectionClass($className);
if (!$apiClass->implementsInterface('\Pasteque\Server\API\API')) {
return APIResult::badRequest();
if (!$apiClass->implementsInterface('\Pasteque\Server\API\API')) {
return APIResult::reject(new APINotFoundException($apiName));
}
$constructor = $apiClass->getMethod('fromApp');
$api = $constructor->invoke(null, $app);
......@@ -57,7 +60,11 @@ class APICaller {
$method = new \ReflectionMethod($api, $methodName);
$methodParser = new APIMethodParser($api, $method);
if (!$methodParser->checkArgc($args)) {
return APIResult::badRequest();
return APIResult::reject(new APINotFoundException($apiName,
$methodName,
$methodParser->getMinArgc(),
$methodParser->getMaxArgc(),
count($args)));
}
try {
$realArgs = $methodParser->buildArgsArray($args);
......@@ -75,9 +82,12 @@ class APICaller {
array('exception' => $e));
return APIResult::error($e->__toString());
}
} else {
return APIResult::reject(new APINotFoundException($apiName,
$methodName));
}
}
return APIResult::badRequest();
} // end class exists
return APIResult::reject(new APINotFoundException($apiName));
}
/** Check if the user is authenticated and has permission.
......@@ -88,7 +98,7 @@ class APICaller {
$apiName = APICaller::formatAPIName($apiName);
if ($userId == null) {
// Accept only Login->login for unauthenticated users
return ($apiName == 'Login'
return ($apiName == 'LoginAPI'
&& ($method == 'getToken' || $method == 'login'));
} else {
// This is where permission checking will be done
......
......@@ -84,4 +84,10 @@ class APIMethodParser {
public function getAPI() { return $this->api; }
public function getMethod() { return $this->method; }
}
\ No newline at end of file
public function getMinArgc() {
return $this->method->getNumberOfRequiredParameters();
}
public function getMaxArgc() {
return $this->method->getNumberOfParameters();
}
}
......@@ -21,6 +21,7 @@
// along with Pasteque. If not, see <http://www.gnu.org/licenses/>.
namespace Pasteque\Server;
use \Pasteque\Server\Exception\APINotFoundException;
use \Pasteque\Server\System\Login;
use \Pasteque\Server\System\API\APICaller;
use \Pasteque\Server\System\API\APIResult;
......@@ -43,12 +44,12 @@ class APICallerTest extends TestCase {
}
public function testFormatAPIName() {
$this->assertEquals('Login', APICaller::formatAPIName('login'), 'ucfirst failed');
$this->assertEquals('/login', APICaller::formatAPIName('../login'), '.. escape failed');
$this->assertEquals('Login', APICaller::formatAPIName('loginAPI'), 'API suffix failed');
$this->assertEquals('Login', APICaller::formatAPIName('loginapi'), 'api suffix failed');
$this->assertEquals('Login', APICaller::formatAPIName('LOGIN'), 'strtolower failed');
$this->assertEquals('Login', APICaller::formatAPIName('LogIn'), 'Multicap failed');
$this->assertEquals('LoginAPI', APICaller::formatAPIName('login'), 'ucfirst failed');
$this->assertEquals('/loginAPI', APICaller::formatAPIName('../login'), '.. escape failed');
$this->assertEquals('LoginAPI', APICaller::formatAPIName('loginAPI'), 'API suffix failed');
$this->assertEquals('LoginAPI', APICaller::formatAPIName('loginapi'), 'api suffix failed');
$this->assertEquals('LoginAPI', APICaller::formatAPIName('LOGIN'), 'strtolower failed');
$this->assertEquals('LoginAPI', APICaller::formatAPIName('LogIn'), 'Multicap failed');
}
/** @depends testFormatAPIName */
......@@ -98,6 +99,13 @@ class APICallerTest extends TestCase {
public function testRunClassNotFound() {
$result = APICaller::run(static::$app, 'someAPI', 'method', array());
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
$error = $result->getContent();
$this->assertNotNull($error);
$this->assertEquals(APINotFoundException::class, get_class($error));
$this->assertEquals('SomeAPI', $error->getAPI());
$this->assertNull($error->getAction());
$this->assertEquals("API SomeAPI doesn't exist.",
$error->getMessage());
}
/** @depends testFormatAPIName
......@@ -105,19 +113,51 @@ class APICallerTest extends TestCase {
public function testRunMethodNotFound() {
$result = APICaller::run(static::$app, 'test', 'method', array());
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
$error = $result->getContent();
$this->assertNotNull($error);
$this->assertEquals(APINotFoundException::class, get_class($error));
$this->assertEquals('TestAPI', $error->getAPI());
$this->assertEquals('method', $error->getAction());
$this->assertNull($error->getMinArgc());
$this->assertEquals("API TestAPI->method doesn't exist.",
$error->getMessage());
}
/** @depends testRunMethodNotFound */
public function testRunWrongArgs() {
$result = APICaller::run(static::$app, 'test', 'reqParams', array(1));
// Give too much arguments
$result = APICaller::run(static::$app, 'test', 'noParam', [1]);
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
$error = $result->getContent();
$this->assertNotNull($error);
$this->assertEquals(APINotFoundException::class, get_class($error));
$this->assertEquals('TestAPI', $error->getAPI());
$this->assertEquals('noParam', $error->getAction());
$this->assertEquals(0, $error->getMinArgc());
$this->assertEquals(0, $error->getMaxArgc());
$this->assertEquals(1, $error->getGivenArgc());
$this->assertEquals("API TestAPI->noParam expects 0 arguments (1 given).",
$error->getMessage());
// Give not enough arguments
$result = APICaller::run(static::$app, 'test', 'params', [1]);
$this->assertEquals(APIResult::STATUS_CALL_REJECTED, $result->getStatus());
$error = $result->getContent();
$this->assertNotNull($error);
$this->assertEquals(APINotFoundException::class, get_class($error));
$this->assertEquals('TestAPI', $error->getAPI());
$this->assertEquals('params', $error->getAction());
$this->assertEquals(2, $error->getMinArgc());
$this->assertEquals(2, $error->getMaxArgc());
$this->assertEquals(1, $error->getGivenArgc());
$this->assertEquals("API TestAPI->params expects 2 arguments (1 given).",
$error->getMessage());
}
/** @depends testFormatAPIName */
public function testRunNoArgsOK() {
$result = APICaller::run(static::$app, 'test', 'noParam', array());
$this->assertEquals(APIResult::STATUS_CALL_OK, $result->getStatus(),
sprintf('Call rejected: %s', json_encode($result->getContent())));
sprintf('Call rejected: %', json_encode($result->getContent())));
$this->assertEquals(\Pasteque\Server\API\TestAPI::NO_PARAM_RESULT, $result->getContent());
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment