diff --git a/App.php b/App.php index 9069232..fd31679 100644 --- a/App.php +++ b/App.php @@ -5,6 +5,7 @@ include_once __DIR__ . '/Slim/Slim.php'; include_once __DIR__ . '/Extension.php'; include_once __DIR__ . '/ErrorResponse.php'; +include_once __DIR__ . '/DefaultAppErrors.php'; use \Slim\Slim; use Slim\Exception\Stop; @@ -47,6 +48,18 @@ public function getResponse() { */ protected static $singletonApp = NULL; + /** + * @var ErrorResponseDictionary + */ + protected $errorResponseDictionary = NULL; + + /** + * @return \MABI\ErrorResponseDictionary + */ + public function getErrorResponseDictionary() { + return $this->errorResponseDictionary; + } + /** * todo: docs */ @@ -70,6 +83,7 @@ public function __construct() { array_push($this->middlewareDirectories, __DIR__ . '/middleware'); } $this->slim = new Slim(); + $this->errorResponseDictionary = new DefaultAppErrors(); parent::__construct($this); } @@ -77,35 +91,34 @@ public function __construct() { * Returns a JSON array displaying the error to the client and stops execution * * Example Error Message Definition: - * define('ERRORDEF_NO_ACCESS', array('message' => 'No Access', 'code' => 1007, 'httpcode' => 402)); + * array('ERRORDEF_NO_ACCESS' => array('message' => 'No Access', 'code' => 1007, 'httpcode' => 402)); * - * @param $message string|array|ErrorResponse - * @param $httpStatusCodeOrReplacementArray int|null|array - * @param $applicationErrorCode int|null + * @param $errorKeyOrDefinition string|array + * @param $replacementArray array * * @throws \Slim\Exception\Stop - * @throws \Exception */ - public function returnError($message, $httpStatusCodeOrReplacementArray = NULL, $applicationErrorCode = NULL) { - $replacementArray = $httpStatusCodeOrReplacementArray; - if (is_array($message)) { - $message = ErrorResponse::FromArray($message); + public function returnError($errorKeyOrDefinition, $replacementArray = array()) { + if (is_string($errorKeyOrDefinition)) { + $errorKey = $errorKeyOrDefinition; } - elseif(is_string($message)) { - $message = new ErrorResponse($message, $httpStatusCodeOrReplacementArray, $applicationErrorCode); - $replacementArray = null; + else { + $errorKeys = array_keys($errorKeyOrDefinition); + $errorKey = $errorKeys[0]; } - elseif (!is_subclass_of($message, 'MABI\\ErrorResponse')) { - throw new \Exception('Invalid type ' . get_class($message) . ' instead of a MABI\ErrorResponse'); + + $errorResponse = $this->errorResponseDictionary->getErrorResponse($errorKey); + if (empty($errorResponse)) { + $errorResponse = ErrorResponse::FromArray($errorKeyOrDefinition[$errorKey]); } - $appCode = $message->getCode(); + $appCode = $errorResponse->getCode(); echo json_encode(array( - 'error' => empty($appCode) ? array('message' => $message->getFormattedMessage($replacementArray)) : - array('code' => $appCode, 'message' => $message->getFormattedMessage($replacementArray)) + 'error' => empty($appCode) ? array('message' => $errorResponse->getFormattedMessage($replacementArray)) : + array('code' => $appCode, 'message' => $errorResponse->getFormattedMessage($replacementArray)) )); - $this->getResponse()->status($message->getHttpcode()); - throw new Stop($message->getFormattedMessage($replacementArray)); + $this->getResponse()->status($errorResponse->getHttpcode()); + throw new Stop($errorResponse->getFormattedMessage($replacementArray)); } public function errorHandler($e) { diff --git a/Controller.php b/Controller.php index eb62e1e..fc1aa96 100644 --- a/Controller.php +++ b/Controller.php @@ -158,6 +158,7 @@ public function loadRoutes($slim) { $rClass = new \ReflectionClass($this); $rMethods = $rClass->getMethods(\ReflectionMethod::IS_PUBLIC); + $baseMethods = array(); foreach ($rMethods as $rMethod) { // If there is a '@endpoint ignore' property, the function is not served as an endpoint if (in_array('ignore', ReflectionHelper::getDocDirective($rMethod->getDocComment(), 'endpoint'))) { @@ -183,20 +184,34 @@ public function loadRoutes($slim) { $action = strtolower(substr($methodName, 6)); $httpMethod = \Slim\Http\Request::METHOD_DELETE; } - + if (!empty($action)) { - $slim->map("/{$this->base}/{$action}", + $slim->map("/{$this->base}/{$action}(/?)", array($this, 'preMiddleware'), array($this, '_runControllerMiddlewares'), array($this, 'preCallable'), array($this, $methodName))->via($httpMethod); - $slim->map("/{$this->base}/{$action}(/:param+)", + $slim->map("/{$this->base}/{$action}(/:param+)(/?)", array($this, 'preMiddleware'), array($this, '_runControllerMiddlewares'), array($this, 'preCallable'), array($this, $methodName))->via($httpMethod); + } + elseif(!empty($httpMethod)) { + array_push($baseMethods, array( + 'name' => $methodName, + 'method' => $httpMethod + )); } } + + foreach ($baseMethods as $httpMethod) { + $slim->map("/{$this->base}(/?)", + array($this, 'preMiddleware'), + array($this, '_runControllerMiddlewares'), + array($this, 'preCallable'), + array($this, $httpMethod['name']))->via($httpMethod['method']); + } } /** @@ -272,10 +287,14 @@ public function getDocJSON(Parser $parser) { continue; } $action = strtolower($methodDoc['MethodName']); - $methodDoc['URI'] = "/{$this->base}/{$action}"; + $methodDoc['URI'] = "/{$this->base}" . (empty($action) ? '' : "/{$action}"); $methodDoc['Synopsis'] = $parser->parse(ReflectionHelper::getDocText($rMethod->getDocComment())); $methodDoc['parameters'] = $this->getDocParameters($rMethod); + if (empty($methodDoc['MethodName'])) { + $methodDoc['MethodName'] = ucwords($this->base); + } + // Allow controller middlewares to modify the documentation for this method if (!empty($this->middlewares)) { $middleware = reset($this->middlewares); diff --git a/DefaultAppErrors.php b/DefaultAppErrors.php new file mode 100644 index 0000000..ca11af7 --- /dev/null +++ b/DefaultAppErrors.php @@ -0,0 +1,31 @@ + array( + 'message' => 'Not properly authenticated for this route', + 'httpcode' => 401, + 'code' => 1007 + ) + ); + + public static $ENTRY_EXISTS = array( + 'ENTRY_EXISTS' => array( + 'message' => 'An entry with the id !modelid already exists.', + 'httpcode' => 409, + 'code' => 1008 + ) + ); + + public static $INVALID_JSON = array( + 'INVALID_JSON' => array( + 'message' => 'Could not load model from json: !message', + 'httpcode' => 400, + 'code' => 1009 + ) + ); +} \ No newline at end of file diff --git a/ErrorResponse.php b/ErrorResponse.php index 68ba07f..5aa2ec3 100644 --- a/ErrorResponse.php +++ b/ErrorResponse.php @@ -32,10 +32,10 @@ class ErrorResponse /** * @param string $message - * @param int|null $httpcode + * @param int $httpcode * @param int|null $code */ - function __construct($message, $httpcode = null, $code = null) + function __construct($message, $httpcode, $code = null) { $this->message = $message; $this->httpcode = $httpcode; @@ -46,18 +46,10 @@ public static function FromArray($array) { if (!isset($array['message'])) throw new \Exception('Invalid ErrorResponse Array. Must contain a message'); return $newErrorResponse = new ErrorResponse($array['message'], - isset($array['httpcode']) ? $array['httpcode'] : null, + $array['httpcode'], isset($array['code']) ? $array['code'] : null); } - /** - * @param int $code - */ - public function setCode($code) - { - $this->code = $code; - } - /** * @return int */ @@ -66,14 +58,6 @@ public function getCode() return $this->code; } - /** - * @param int $httpcode - */ - public function setHttpcode($httpcode) - { - $this->httpcode = $httpcode; - } - /** * @return int */ @@ -82,14 +66,6 @@ public function getHttpcode() return $this->httpcode; } - /** - * @param string $message - */ - public function setMessage($message) - { - $this->message = $message; - } - /** * @return string */ @@ -98,11 +74,11 @@ public function getMessage() return $this->message; } - public function getFormattedMessage($replacementArray = null) + public function getFormattedMessage($replacementArray = array()) { if (!empty($replacementArray)) { - return str_replace(array_keys($replacementArray), array_values($replacementArray), $this->message); + return str_replace(array_keys($replacementArray), array_values($replacementArray), $this->getMessage()); } - return str_replace($this->replacementTokens, $this->replacementValues, $this->message); + return str_replace($this->replacementTokens, $this->replacementValues, $this->getMessage()); } } \ No newline at end of file diff --git a/ErrorResponseDictionary.php b/ErrorResponseDictionary.php new file mode 100644 index 0000000..57bfcf8 --- /dev/null +++ b/ErrorResponseDictionary.php @@ -0,0 +1,70 @@ + 'sample error message', + * 'httpcode' => '401', + * 'code' => 1); // optional + */ + function __construct() { + $rClass = new \ReflectionClass(get_called_class()); + $rProperties = $rClass->getProperties(\ReflectionProperty::IS_STATIC); + $defaultProps = $rClass->getDefaultProperties(); + foreach ($rProperties as $rProperty) { + $ignoreAnnotation = ReflectionHelper::getDocDirective($rProperty->getDocComment(), 'ignore'); + if (!empty($ignoreAnnotation) || !is_array($defaultProps[$rProperty->getName()]) || + empty($defaultProps[$rProperty->getName()]) + ) { + continue; + } + + $propertyKeys = array_keys($defaultProps[$rProperty->getName()]); + $key = $propertyKeys[0]; + $errorResponseArray = $defaultProps[$rProperty->getName()][$key]; + if (empty($errorResponseArray['message']) || empty($errorResponseArray['httpcode'])) { + continue; + } + + $this->errorResponses[$key] = ErrorResponse::FromArray($errorResponseArray); + } + } + + /** + * Creates or overrides error responses with an initialization array in mass + * + * @param $initArray + */ + public function overrideErrorResponses(ErrorResponseDictionary $overridingDictionary) { + foreach ($overridingDictionary->errorResponses as $key => $errorResponse) { + $this->errorResponses[$key] = $errorResponse; + } + } + + public function overrideErrorResponse($key, ErrorResponse $errorResponse) { + $this->errorResponses[$key] = $errorResponse; + } + + /** + * todo: docs + * + * @param $key + * + * @return ErrorResponse|null + */ + public function getErrorResponse($key) { + if (empty($this->errorResponses[$key])) { + return NULL; + } + + return $this->errorResponses[$key]; + } +} \ No newline at end of file diff --git a/Model.php b/Model.php index 46ce66c..87bcb02 100644 --- a/Model.php +++ b/Model.php @@ -250,7 +250,7 @@ public function loadFromExternalSource($source) { try { $this->load($source, TRUE); } catch (InvalidJSONException $ex) { - $this->app->returnError($ex->getMessage(), 400, 1009); + $this->app->returnError(DefaultAppErrors::$INVALID_JSON, array('!message' => $ex->getMessage())); } } diff --git a/RESTModelController.php b/RESTModelController.php index aa2797e..0e209e7 100644 --- a/RESTModelController.php +++ b/RESTModelController.php @@ -21,7 +21,7 @@ public function __construct($extension) { } } - public function _restGetCollection() { + public function get() { $outputArr = array(); foreach ($this->model->findAll() as $foundModel) { $outputArr[] = $foundModel->getPropertyArray(TRUE); @@ -29,22 +29,22 @@ public function _restGetCollection() { echo json_encode($outputArr); } - public function _restPostCollection() { + public function post() { $this->model->loadFromExternalSource($this->getApp()->getRequest()->getBody()); if ($this->model->findById($this->model->getId())) { - $this->getApp()->returnError('An entry with the id ' . $this->model->getId() . ' already exists.', 409, 1008); + $this->getApp()->returnError(DefaultAppErrors::$ENTRY_EXISTS, array('!modelid' => $this->model->getId())); } $this->model->insert(); echo $this->model->outputJSON(); } - public function _restPutCollection() { + public function put() { // todo: implement } - public function _restDeleteCollection() { + public function delete() { // todo: implement } @@ -80,29 +80,21 @@ public function _readModel($route) { $this->model->findById($route->getParam('id')); } - protected function addStandardRestRoute(\Slim\Slim $slim, $httpMethod, $isObjectLevel = FALSE) { - $methodName = '_rest' . ucwords(strtolower($httpMethod)) . ($isObjectLevel ? 'Resource' : 'Collection'); + protected function addStandardRestRoute(\Slim\Slim $slim, $httpMethod) { + $methodName = '_rest' . ucwords(strtolower($httpMethod)) . 'Resource'; $rMethod = new \ReflectionMethod(get_called_class(), $methodName); // If there is a '@endpoint ignore' property, the function is not served as an endpoint if (in_array('ignore', ReflectionHelper::getDocDirective($rMethod->getDocComment(), 'endpoint'))) { return; } - if (!$isObjectLevel) { - $slim->map("/{$this->base}", - array($this, 'preMiddleware'), - array($this, '_runControllerMiddlewares'), - array($this, 'preCallable'), - array($this, $methodName))->via($httpMethod); - } - else { - $slim->map("/{$this->base}/:id", - array($this, 'preMiddleware'), - array($this, '_readModel'), - array($this, '_runControllerMiddlewares'), - array($this, 'preCallable'), - array($this, $methodName))->via($httpMethod); - } + + $slim->map("/{$this->base}/:id(/?)", + array($this, 'preMiddleware'), + array($this, '_readModel'), + array($this, '_runControllerMiddlewares'), + array($this, 'preCallable'), + array($this, $methodName))->via($httpMethod); } /** @@ -125,12 +117,8 @@ public function loadRoutes($slim) { // todo: add API versioning $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_GET); - $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_POST); $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_PUT); $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_DELETE); - $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_GET, TRUE); - $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_PUT, TRUE); - $this->addStandardRestRoute($slim, \Slim\Http\Request::METHOD_DELETE, TRUE); /** * Gets other automatically generated routes following the pattern: @@ -166,13 +154,13 @@ public function loadRoutes($slim) { } if (!empty($action)) { - $slim->map("/{$this->base}/:id/{$action}", + $slim->map("/{$this->base}/:id/{$action}(/?)", array($this, 'preMiddleware'), array($this, '_readModel'), array($this, '_runControllerMiddlewares'), array($this, 'preCallable'), array($this, $methodName))->via($httpMethod); - $slim->map("/{$this->base}/:id/{$action}(/:param)", + $slim->map("/{$this->base}/:id/{$action}(/:param)(/?)", array($this, 'preMiddleware'), array($this, '_readModel'), array($this, '_runControllerMiddlewares'), @@ -182,8 +170,7 @@ public function loadRoutes($slim) { } } - private function getRestMethodDocJSON(Parser $parser, $methodName, $httpMethod, $url, $rClass, - $method, $includesId = FALSE) { + private function getRestMethodDocJSON(Parser $parser, $methodName, $httpMethod, $url, $rClass, $method) { $methodDoc = array(); $rMethod = new \ReflectionMethod(get_called_class(), $method); @@ -199,15 +186,13 @@ private function getRestMethodDocJSON(Parser $parser, $methodName, $httpMethod, $methodDoc['URI'] = $url; $methodDoc['Synopsis'] = $parser->parse(ReflectionHelper::getDocText($docComment)); $methodDoc['parameters'] = $this->getDocParameters($rMethod); - if ($includesId) { - $methodDoc['parameters'][] = array( - 'Name' => 'id', - 'Required' => 'Y', - 'Type' => 'string', - 'Location' => 'url', - 'Description' => 'The id of the resource' - ); - } + $methodDoc['parameters'][] = array( + 'Name' => 'id', + 'Required' => 'Y', + 'Type' => 'string', + 'Location' => 'url', + 'Description' => 'The id of the resource' + ); // Allow controller middlewares to modify the documentation for this method if (!empty($this->middlewares)) { @@ -231,38 +216,33 @@ public function getDocJSON(Parser $parser) { $rClass = new \ReflectionClass(get_called_class()); - $methodDoc = $this->getRestMethodDocJSON($parser, 'Get Full Collection', - 'GET', "/{$this->base}", $rClass, '_restGetCollection'); - if (!empty($methodDoc)) { - $doc['methods'][] = $methodDoc; - } - $methodDoc = $this->getRestMethodDocJSON($parser, 'Add to Collection', - 'POST', "/{$this->base}", $rClass, '_restPostCollection'); - if (!empty($methodDoc)) { - $doc['methods'][] = $methodDoc; - } - $methodDoc = $this->getRestMethodDocJSON($parser, 'Replace Full Collection', - 'PUT', "/{$this->base}", $rClass, '_restPutCollection'); - if (!empty($methodDoc)) { - $doc['methods'][] = $methodDoc; - } - $methodDoc = $this->getRestMethodDocJSON($parser, 'Delete Full Collection', - 'DELETE', "/{$this->base}", $rClass, '_restDeleteCollection'); - if (!empty($methodDoc)) { - $doc['methods'][] = $methodDoc; + foreach($doc['methods'] as $k => $methodDoc) { + if($methodDoc['InternalMethodName'] == 'get') { + $doc['methods'][$k]['MethodName'] = 'Get Full Collection'; + } + elseif($methodDoc['InternalMethodName'] == 'post') { + $doc['methods'][$k]['MethodName'] = 'Add to Collection'; + } + elseif($methodDoc['InternalMethodName'] == 'put') { + $doc['methods'][$k]['MethodName'] = 'Replace Full Collection'; + } + elseif($methodDoc['InternalMethodName'] == 'delete') { + $doc['methods'][$k]['MethodName'] = 'Delete Full Collection'; + } } + $methodDoc = $this->getRestMethodDocJSON($parser, 'Get Resource', - 'GET', "/{$this->base}/:id", $rClass, '_restGetResource', TRUE); + 'GET', "/{$this->base}/:id", $rClass, '_restGetResource'); if (!empty($methodDoc)) { $doc['methods'][] = $methodDoc; } $methodDoc = $this->getRestMethodDocJSON($parser, 'Update Resource', - 'PUT', "/{$this->base}/:id", $rClass, '_restPutResource', TRUE); + 'PUT', "/{$this->base}/:id", $rClass, '_restPutResource'); if (!empty($methodDoc)) { $doc['methods'][] = $methodDoc; } $methodDoc = $this->getRestMethodDocJSON($parser, 'Delete Resource', - 'DELETE', "/{$this->base}/:id", $rClass, '_restDeleteResource', TRUE); + 'DELETE', "/{$this->base}/:id", $rClass, '_restDeleteResource'); if (!empty($methodDoc)) { $doc['methods'][] = $methodDoc; } diff --git a/autodocs/iodocs/app.js b/autodocs/iodocs/app.js index 4de96c3..535ebe8 100755 --- a/autodocs/iodocs/app.js +++ b/autodocs/iodocs/app.js @@ -243,6 +243,7 @@ function processRequest(req, res, next) { }; var reqQuery = req.body, + reqFiles = req.files, customHeaders = {}, params = reqQuery.params || {}, locations = reqQuery.locations || {}, @@ -306,7 +307,17 @@ function processRequest(req, res, next) { }; if (['POST','DELETE','PUT'].indexOf(httpMethod) !== -1) { - if(config.jsonRequests) { + if(reqFiles) { + var files2Post = {}; + for (var file in reqFiles.params) { + if (reqFiles.params.hasOwnProperty(file)) + files2Post[file] = reqFiles.params[file]; + } + var headersAndParams = getFormDataForPost(params, files2Post); + for (var key in headersAndParams.headers) options.headers[key] = headersAndParams.headers[key]; + requestBody = headersAndParams.postdata; + } + else if(config.jsonRequests) { // Extract body param if it's a json request, and just use it as the post body for( var param in params ) { @@ -607,7 +618,11 @@ function processRequest(req, res, next) { }; }); - if (requestBody) { + if (requestBody instanceof Array) { + for (var i = 0; i < requestBody.length; i++)apiCall.write(requestBody[i]); + apiCall.end(); + } + else if (requestBody) { apiCall.end(requestBody, 'utf-8'); } else { @@ -712,3 +727,57 @@ if (!module.parent) { console.log("Express server listening on port %d", app.address().port); }); } + +/** + Converts a list of parameters to forum data + - `fields` - a property map of key value pairs + - `files` - a list of property maps of content + - `type` - the type of file data + - `keyname` - the name of the key corresponding to the file + - `valuename` - the name of the value corresponding to the file + - `data` - the data of the file + */ +function getFormDataForPost(fields, files) { + function encodeFieldPart(boundary,name,value) { + var return_part = "--" + boundary + "\r\n"; + return_part += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"; + return_part += value + "\r\n"; + return return_part; + } + function encodeFilePart(boundary,type,name,filename) { + var return_part = "--" + boundary + "\r\n"; + return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n"; + return_part += "Content-Type: " + type + "\r\n\r\n"; + return return_part; + } + var boundary = '----------------------------' + (Math.ceil(Math.random() * 17592186044416 + 263882790666239).toString(16)); + var post_data = []; + + if (fields) { + for (var key in fields) { + var value = fields[key]; + post_data.push(new Buffer(encodeFieldPart(boundary, key, value), 'ascii')); + } + } + if (files) { + for (var key in files) { + var value = files[key]; + post_data.push(new Buffer(encodeFilePart(boundary, value.type, key, value.name), 'ascii')); + post_data.push(fs.readFileSync(value.path)); + } + } + post_data.push(new Buffer("\r\n--" + boundary + "--"), 'ascii'); + var length = 0; + + for(var i = 0; i < post_data.length; i++) { + length += post_data[i].length; + } + var params = { + postdata : post_data, + headers : { + 'Content-Type': 'multipart/form-data; boundary=' + boundary, + 'Content-Length': length + } + }; + return params; +} \ No newline at end of file diff --git a/autodocs/iodocs/public/javascripts/docs.js b/autodocs/iodocs/public/javascripts/docs.js index ead4ca0..5ab5cbe 100644 --- a/autodocs/iodocs/public/javascripts/docs.js +++ b/autodocs/iodocs/public/javascripts/docs.js @@ -180,6 +180,14 @@ apiSecret = { name: 'apiSecret', value: $('input[name=secret]').val() }, apiName = { name: 'apiName', value: $('input[name=apiName]').val() }; + var inputFiles = false; + if ($(this).find('.file2upload').length > 0) { + inputFiles = new FormData(); + $(this).find('.file2upload').each(function(i, el) { + inputFiles.append($(el)[0].name, $(el)[0].files[0]); + }); + } + params.push(apiKey, apiSecret, apiName); // Setup results container @@ -238,9 +246,25 @@ .addClass('response prettyprint')); } - console.log(params); + var ajaxOptions = { + url: basePath + 'processReq', + type: 'POST' + }; + + if (inputFiles) { + params.forEach(function(param) { + inputFiles.append(param['name'], param['value']) + }); + ajaxOptions['processData'] = false; + ajaxOptions['contentType'] = false; + ajaxOptions['data'] = inputFiles; + } else { + ajaxOptions['data'] = params; + } + - $.post(basePath + 'processReq', params, function(result, text) { + $.ajax(ajaxOptions) + .success(function(result, text) { // If we get passed a signin property, open a window to allow the user to signin/link their account if (result.signin) { window.open(result.signin,"_blank","height=900,width=800,menubar=0,resizable=1,scrollbars=1,status=0,titlebar=0,toolbar=0"); @@ -254,7 +278,6 @@ .toggleClass('error', false) .text(formatJSON(JSON.parse(response))); } - }) // Complete, runs on error and success .complete(function(result, text) { diff --git a/autodocs/iodocs/views/api.jade b/autodocs/iodocs/views/api.jade index bb789ae..7e30357 100644 --- a/autodocs/iodocs/views/api.jade +++ b/autodocs/iodocs/views/api.jade @@ -139,6 +139,8 @@ ul option(value=choice, selected=true) #{choice} - else option(value=choice) #{choice} + - else if (parameter.Type =='file') + input(class='file2upload', type='file', name='params[' + parameter.Name + ']', multiple='multiple') - else - if (parameter.Location == 'body' && config.jsonRequests) textarea(name='params[' + parameter.Name + ']', value=parameter.Default, placeholder=className, rows=8, columns=6) diff --git a/extensions/FacebookIdentity/Errors.php b/extensions/FacebookIdentity/Errors.php new file mode 100644 index 0000000..b3fe061 --- /dev/null +++ b/extensions/FacebookIdentity/Errors.php @@ -0,0 +1,26 @@ + array( + 'message' => 'An authorization token is required to create a session', + 'httpcode' => 400, + 'code' => 1000 + ) + ); + + public static $FB_ONLY = array( + 'FB_IDENTITY_ONLY' => array( + 'message' => 'Facebook Connect is the only method allowed to create users.', + 'httpcode' => 401, + 'code' => 1001 + ) + ); + +} \ No newline at end of file diff --git a/extensions/FacebookIdentity/FacebookIdentity.php b/extensions/FacebookIdentity/FacebookIdentity.php index 8dfbfa8..f7e85c3 100644 --- a/extensions/FacebookIdentity/FacebookIdentity.php +++ b/extensions/FacebookIdentity/FacebookIdentity.php @@ -5,6 +5,7 @@ include_once __DIR__ . '/../../DirectoryModelLoader.php'; include_once __DIR__ . '/../../DirectoryControllerLoader.php'; include_once __DIR__ . '/../Identity/Identity.php'; +include_once __DIR__ . '/Errors.php'; use MABI\DirectoryControllerLoader; use MABI\DirectoryModelLoader; @@ -31,5 +32,7 @@ public function __construct(\MABI\App $app, \MABI\Identity\Identity $identityExt foreach($this->getControllers() as $controller) { $controller->setFacebookOnly($facebookOnly); } + + $this->getApp()->getErrorResponseDictionary()->overrideErrorResponses(new Errors()); } } diff --git a/extensions/FacebookIdentity/controllers/SessionController.php b/extensions/FacebookIdentity/controllers/SessionController.php index b3c81c3..38a6ea0 100644 --- a/extensions/FacebookIdentity/controllers/SessionController.php +++ b/extensions/FacebookIdentity/controllers/SessionController.php @@ -104,7 +104,7 @@ protected function insertFBUser($fbData) { $userModel = call_user_func($this->userModelClass . '::init', $this->getApp()); if ($userModel->findByField('email', $fbData->email)) { - $this->getApp()->returnError('An account with this email already exists', 409, 1006); + $this->getApp()->returnError(\MABI\Identity\Errors::$EMAIL_EXISTS); } $userModel->firstName = $fbData->first_name; @@ -156,15 +156,15 @@ protected function insertFBUser($fbData) { * * @throws \Slim\Exception\Stop */ - function _restPostCollection() { + function post() { $this->model->loadFromExternalSource($this->getApp()->getRequest()->getBody()); if (empty($this->model->accessToken)) { if ($this->getFacebookOnly()) { - $this->getApp()->returnError('An authorization token is required to create a session', 400, 1000); + $this->getApp()->returnError(Errors::$TOKEN_REQUIRED); } - parent::_restPostCollection(); + parent::post(); } else { // get facebook info and login or create a user diff --git a/extensions/FacebookIdentity/controllers/UserController.php b/extensions/FacebookIdentity/controllers/UserController.php index 16a3b45..73139f5 100644 --- a/extensions/FacebookIdentity/controllers/UserController.php +++ b/extensions/FacebookIdentity/controllers/UserController.php @@ -54,18 +54,18 @@ public function setFacebookOnly($facebookOnly) { * * @throws \Slim\Exception\Stop */ - public function _restPostCollection() { + public function post() { if ($this->getFacebookOnly()) { - $this->getApp()->returnError('Facebook Connect is the only method allowed to create users', 401, 1001); + $this->getApp()->returnError(Errors::$FB_ONLY); } else { - parent::_restPostCollection(); + parent::post(); } } public function postForgotPassword() { if ($this->getFacebookOnly()) { - $this->getApp()->returnError('Facebook Connect is the only method allowed to create users', 401, 1001); + $this->getApp()->returnError(Errors::$FB_ONLY); } else { parent::postForgotPassword(); @@ -82,10 +82,10 @@ public function postForgotPassword() { public function getDocJSON(Parser $parser) { $doc = parent::getDocJSON($parser); - // Removes documentation for _restPostCollection and postForgotPassword if facebookOnly is set + // Removes documentation for post and postForgotPassword if facebookOnly is set if ($this->getFacebookOnly()) { foreach ($doc['methods'] as $k => $method) { - if ($method['InternalMethodName'] == '_restPostCollection' || + if ($method['InternalMethodName'] == 'post' || $method['InternalMethodName'] == 'postForgotPassword' ) { unset($doc['methods'][$k]); diff --git a/extensions/Identity/Errors.php b/extensions/Identity/Errors.php new file mode 100644 index 0000000..fcb82f3 --- /dev/null +++ b/extensions/Identity/Errors.php @@ -0,0 +1,95 @@ + array( + 'message' => 'Email is required to create a session', + 'httpcode' => 400, + 'code' => 1002 + ) + ); + + public static $PASSWORD_INVALID = array( + 'IDENTITY_PASSWORD_INVALID' => array( + 'message' => 'Password is invalid', + 'httpcode' => 400, + 'code' => 1003 + ) + ); + + public static $TOKEN_INVALID = array( + 'IDENTITY_TOKEN_INVALID' => array( + 'message' => 'AuthToken is invalid', + 'httpcode' => 400, + 'code' => 1003 + ) + ); + + public static $SESSION_PASSWORD_TOKEN_REQUIRED = array( + 'IDENTITY_SESSION_PASSWORD_TOKEN_REQUIRED' => array( + 'message' => 'A Password or an authToken are required to create a session', + 'httpcode' => 400, + 'code' => 1002 + ) + ); + + public static $SHORT_PASSWORD = array( + 'IDENTITY_SHORT_PASSWORD' => array( + 'message' => 'Password must be at least 6 characters', + 'httpcode' => 400, + 'code' => 1004 + ) + ); + + public static $EMAIL_REQUIRED = array( + 'IDENTITY_EMAIL_REQUIRED' => array( + 'message' => 'Email is required', + 'httpcode' => 400, + 'code' => 1005 + ) + ); + + public static $EMAIL_EXISTS = array( + 'IDENTITY_EMAIL_EXISTS' => array( + 'message' => 'An account with this email already exists', + 'httpcode' => 409, + 'code' => 1006 + ) + ); + + public static $PASSWORD_NO_USER_EMAIL = array( + 'FORGET_PASSWORD_NO_USER_EMAIL' => array( + 'message' => 'There is no user associated with this email', + 'httpcode' => 400, + 'code' => 1011 + ) + ); + + public static $PASSWORD_EMAIL_REQUIRED = array( + 'FORGET_PASSWORD_EMAIL_REQUIRED' => array( + 'message' => 'Email is required to reset password', + 'httpcode' => 400, + 'code' => 1010 + ) + ); + + public static $PASSWORD_EMAIL_TEMPLATE = array( + 'FORGET_PASSWORD_EMAIL_TEMPLATE' => array( + 'message' => 'forgotEmailTemplate must be set', + 'httpcode' => 500 + ) + ); + + public static $PASSWORD_EMAIL_PROVIDER = array( + 'FORGET_PASSWORD_EMAIL_PROVIDER' => array( + 'message' => 'EmailProvider is not properly implemented. PHPCore and Mandrill can be used as defaults.', + 'httpcode' => 500 + ) + ); +} \ No newline at end of file diff --git a/extensions/Identity/Identity.php b/extensions/Identity/Identity.php index a0b3703..3fd87cb 100644 --- a/extensions/Identity/Identity.php +++ b/extensions/Identity/Identity.php @@ -6,6 +6,7 @@ include_once __DIR__ . '/../../Extension.php'; include_once __DIR__ . '/../../DirectoryModelLoader.php'; include_once __DIR__ . '/../../DirectoryControllerLoader.php'; +include_once __DIR__ . '/Errors.php'; use MABI\App; use MABI\DirectoryControllerLoader; @@ -26,6 +27,8 @@ public function __construct(App $app, RESTAccess $restAccessExtension) { new DirectoryControllerLoader(__DIR__ . '/controllers', $this, 'MABI\Identity') )); $this->getExtensionModelClasses(); + + $this->getApp()->getErrorResponseDictionary()->overrideErrorResponses(new Errors()); } public static function passHash($password, $salt) { diff --git a/extensions/Identity/controllers/SessionController.php b/extensions/Identity/controllers/SessionController.php index 8c8d536..2594730 100644 --- a/extensions/Identity/controllers/SessionController.php +++ b/extensions/Identity/controllers/SessionController.php @@ -55,10 +55,10 @@ class SessionController extends RESTModelController { * * @throws \Slim\Exception\Stop */ - function _restPostCollection() { + function post() { $this->model->loadFromExternalSource($this->getApp()->getRequest()->getBody()); if (empty($this->model->email)) { - $this->getApp()->returnError('Email is required to create a session', 400, 1002); + $this->getApp()->returnError(Errors::$SESSION_EMAIL_REQUIRED); } else { /** @@ -69,16 +69,16 @@ function _restPostCollection() { if (!empty($this->model->password)) { if ($user->passHash != Identity::passHash($this->model->password, $user->salt)) { - $this->getApp()->returnError('Password is invalid', 400, 1003); + $this->getApp()->returnError(Errors::$PASSWORD_INVALID); } } elseif (!empty($this->model->authToken)) { if ($this->model->authToken != Identity::passHash($user->passHash, $user->lastAccessed->getTimestamp())) { - $this->getApp()->returnError('AuthToken is invalid', 400, 1003); + $this->getApp()->returnError(Errors::$TOKEN_INVALID); } } else { - $this->getApp()->returnError('A Password or an authToken are required to create a session', 400, 1002); + $this->getApp()->returnError(Errors::$SESSION_PASSWORD_TOKEN_REQUIRED); } $user->lastAccessed = new \DateTime('now'); $user->save(); diff --git a/extensions/Identity/controllers/UserController.php b/extensions/Identity/controllers/UserController.php index 3d55275..e222bcf 100644 --- a/extensions/Identity/controllers/UserController.php +++ b/extensions/Identity/controllers/UserController.php @@ -78,19 +78,19 @@ public function setEmailProvider($emailProvider) { * * @throws \Slim\Exception\Stop */ - public function _restPostCollection() { + public function post() { $this->model->loadFromExternalSource($this->getApp()->getRequest()->getBody()); if (empty($this->model->password) || strlen($this->model->password) < 6) { - $this->getApp()->returnError('Password must be at least 6 characters', 400, 1004); + $this->getApp()->returnError(Errors::$SHORT_PASSWORD); } if (empty($this->model->email)) { - $this->getApp()->returnError('Email is required', 400, 1005); + $this->getApp()->returnError(Errors::$EMAIL_REQUIRED); } if ($this->model->findByField('email', $this->model->email)) { - $this->getApp()->returnError('An account with this email already exists', 409, 1006); + $this->getApp()->returnError(Errors::$EMAIL_EXISTS); } $this->model->insert(); @@ -116,17 +116,17 @@ public function _restPostCollection() { * @param $id string The id of the user you are trying to update */ public function _restPutResource($id) { - $updatedUser = call_user_func($this->modelClass . '::init', $this->getApp()); - $updatedUser->loadFromExternalSource($this->getApp()->getRequest()->getBody()); - $updatedUser->setId($id); - if (!empty($updatedUser->password)) { - if (strlen($updatedUser->password) < 6) { - $this->getApp()->returnError('Password must be at least 6 characters', 400, 1004); + $oldUser = $this->model; + $this->model->loadFromExternalSource($this->getApp()->getRequest()->getBody()); + + if (!empty($this->model->password)) { + if (strlen($this->model->password) < 6) { + $this->getApp()->returnError(Errors::$SHORT_PASSWORD); } - $updatedUser->passHash = Identity::passHash($updatedUser->password, $this->model->salt); - $updatedUser->password = NULL; + $this->model->passHash = Identity::passHash($this->model->password, $oldUser->salt); + $this->model->password = NULL; /** * Deletes all sessions except for the current one for the user whose password changed @@ -143,38 +143,29 @@ public function _restPutResource($id) { $session->delete(); } } - else { - $updatedUser->passHash = $this->model->passHash; - } - if (empty($updatedUser->email)) { - $this->getApp()->returnError('Email is required', 400, 1005); + if (empty($this->model->email)) { + $this->getApp()->returnError(Errors::$EMAIL_REQUIRED); } - if ($updatedUser->email != $this->model->email && $updatedUser->findByField('email', $updatedUser->email)) { - $this->getApp()->returnError('An account with this email already exists', 409, 1006); + if ($this->model->email != $oldUser->email && $this->model->findByField('email', $this->model->email)) { + $this->getApp()->returnError(Errors::$EMAIL_EXISTS); } - $updatedUser->created = $this->model->created; - $updatedUser->salt = $this->model->salt; - - $updatedUser->save(); - echo $updatedUser->outputJSON(); + $this->model->created = $oldUser->created; + $this->model->salt = $oldUser->salt; + $this->model->lastAccessed = $oldUser->lastAccessed; + $this->model->save(); + echo $this->model->outputJSON(); } public function postForgotPassword() { if ($this->getEmailProvider() == null) { - $this->getApp()->returnError(array( - 'message' => 'EmailProvider is not properly implemented. PHPCore and Mandrill can be used as defaults.', - 'httpcode' => 500 - )); + $this->getApp()->returnError(Errors::$PASSWORD_EMAIL_PROVIDER); } if ($this->forgotEmailTemplate == null) { - $this->getApp()->returnError(array( - 'message' => 'forgotEmailTemplate must be set.', - 'httpcode' => 500 - )); + $this->getApp()->returnError(Errors::$PASSWORD_EMAIL_TEMPLATE); } /** @@ -184,17 +175,11 @@ public function postForgotPassword() { try { $email = $data->email; } catch (\Exception $e) { - $this->getApp()->returnError(array( - 'message' => 'Email is required to reset password.', - 'httpcode' => 400 - )); + $this->getApp()->returnError(Errors::$PASSWORD_EMAIL_REQUIRED); } $user = User::init($this->getApp()); if (!$user->findByField('email', $email)) { - $this->getApp()->returnError(array( - 'message' => 'There is no user with this email.', - 'httpcode' => 400 - )); + $this->getApp()->returnError(Errors::$PASSWORD_NO_USER_EMAIL); } $user->lastAccessed = new \DateTime('now'); diff --git a/extensions/Identity/middleware/RESTOwnerOnlyAccess.php b/extensions/Identity/middleware/RESTOwnerOnlyAccess.php index d0a9e7b..ad9110d 100755 --- a/extensions/Identity/middleware/RESTOwnerOnlyAccess.php +++ b/extensions/Identity/middleware/RESTOwnerOnlyAccess.php @@ -2,6 +2,7 @@ namespace MABI\Identity\Middleware; +use MABI\DefaultAppErrors; use MABI\Middleware; use MABI\Identity\Session; use MABI\ReflectionHelper; @@ -11,22 +12,14 @@ class RESTOwnerOnlyAccess extends Middleware { protected function isCollectionCallable($methodName) { - switch ($methodName) { - case '_restGetCollection': - case '_restPostCollection': - case '_restPutCollection': - case '_restDeleteCollection': - return TRUE; - default: - if (strpos($methodName, 'get', 0) === 0 || - strpos($methodName, 'put', 0) === 0 || - strpos($methodName, 'post', 0) === 0 || - strpos($methodName, 'delete', 0) === 0 - ) { - return TRUE; - } - return FALSE; + if (strpos($methodName, 'get', 0) === 0 || + strpos($methodName, 'put', 0) === 0 || + strpos($methodName, 'post', 0) === 0 || + strpos($methodName, 'delete', 0) === 0 + ) { + return TRUE; } + return FALSE; } /** @@ -49,7 +42,7 @@ public function call() { // A session is required to access these objects if (!isset($this->getApp()->getRequest()->session)) { - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } /** @@ -74,7 +67,7 @@ public function call() { $session->userId != $restController->getModel()->{$ownerProperty} ) { // Don't give access to endpoint if the sessions don't match - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } if (!empty($this->next)) { diff --git a/extensions/Identity/middleware/SessionHeader.php b/extensions/Identity/middleware/SessionHeader.php index e250d7f..9af3663 100755 --- a/extensions/Identity/middleware/SessionHeader.php +++ b/extensions/Identity/middleware/SessionHeader.php @@ -13,6 +13,8 @@ class SessionHeader extends Middleware { * @var \MABI\Identity\Session */ public $session = NULL; + + protected $sessionModelClass = '\MABI\Identity\Session'; /** * Call @@ -24,8 +26,8 @@ class SessionHeader extends Middleware { */ public function call() { $sessionId = $this->getApp()->getRequest()->headers('SESSION'); - - $foundSession = Session::init($this->getApp()); + + $foundSession = call_user_func($this->sessionModelClass . '::init', $this->getApp()); if($foundSession->findById($sessionId)) { $this->session = $foundSession; $this->getApp()->getRequest()->session = $this->session; @@ -36,6 +38,8 @@ public function call() { $user->findById($this->session->userId); $user->lastAccessed = $now; $user->save(); + + $this->session->user = $user; } if (!empty($this->next)) { diff --git a/extensions/Identity/middleware/SessionOnlyAccess.php b/extensions/Identity/middleware/SessionOnlyAccess.php index f4f1849..acaae1f 100755 --- a/extensions/Identity/middleware/SessionOnlyAccess.php +++ b/extensions/Identity/middleware/SessionOnlyAccess.php @@ -2,6 +2,7 @@ namespace MABI\Identity\Middleware; +use MABI\DefaultAppErrors; use MABI\Middleware; include_once __DIR__ . '/../../../Middleware.php'; @@ -19,7 +20,7 @@ class SessionOnlyAccess extends Middleware { public function call() { // A session is required to access this call if (empty($this->getApp()->getRequest()->session)) { - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } if (!empty($this->next)) { diff --git a/extensions/Identity/models/Session.php b/extensions/Identity/models/Session.php index bf4f443..6bca695 100644 --- a/extensions/Identity/models/Session.php +++ b/extensions/Identity/models/Session.php @@ -47,7 +47,7 @@ class Session extends Model { * so that an extra request does not need to be made to the User controller. This is most likely wasteful for other * requests so only the userId field is filled. * - * @var User + * @var \MABI\Identity\User * @field external */ public $user; diff --git a/extensions/Identity/tests/RESTOwnerOnlyAccessTest.php b/extensions/Identity/tests/RESTOwnerOnlyAccessTest.php index f758247..764c04f 100644 --- a/extensions/Identity/tests/RESTOwnerOnlyAccessTest.php +++ b/extensions/Identity/tests/RESTOwnerOnlyAccessTest.php @@ -8,6 +8,7 @@ use MABI\Identity\Middleware\SessionHeader; use MABI\RESTAccess\RESTAccess; use MABI\Testing\MiddlewareTestCase; +use MABI\Testing\TableDefinition; include_once 'PHPUnit/Autoload.php'; include_once __DIR__ . '/../../../tests/middleware/MiddlewareTestCase.php'; @@ -18,6 +19,42 @@ class RESTOwnerOnlyAccessTest extends MiddlewareTestCase { + protected static $SESSION_111444 = array( + 'created' => '1370663864', + 'userId' => 11 + ); + + protected static $SESSION_111445 = array( + 'created' => '1370663864', + 'userId' => 12 + ); + + protected static $USER_11 = array( + 'id' => 11, + 'created' => 1372375580, + 'firstName' => 'Photis', + 'lastName' => 'Patriotis', + 'email' => 'ppatriotis@gmail.com', + 'passHash' => '604cefb585491865043db59f5f200c08af016dc636bcb37c858199e20f082c10', + // result of: hash_hmac('sha256', '123', 'salt4456'); + 'salt' => 'salt4456', + 'lastAccessed' => 1379430989 + ); + + protected static $USER_12 = array( + 'id' => 12, + 'created' => 1372375580, + 'firstName' => 'Photis', + 'lastName' => 'Patriotis', + 'email' => 'ppatriotis@gmail.com', + 'passHash' => '604cefb585491865043db59f5f200c08af016dc636bcb37c858199e20f082c10', + // result of: hash_hmac('sha256', '123', 'salt4456'); + 'salt' => 'salt4456', + 'lastAccessed' => 1379430989 + ); + + protected static $MODELB_1 = array('modelBId' => 1, 'name' => 'test', 'testOwner' => 11); + public function testSuccessfulCall() { $middleware = new RESTOwnerOnlyAccess(); @@ -26,9 +63,18 @@ public function testSuccessfulCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->any()) + $this->dataConnectionMock->expects($this->exactly(3)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); + ->will($this->returnCallback( + function ($field, $value, $table) { + return $this->findOneByFieldCallback( + array( + 'sessions' => new TableDefinition('id', 111444, self::$SESSION_111444), + 'users' => new TableDefinition('id', 11, self::$USER_11), + 'modelbs' => new TableDefinition('id', 4, self::$MODELB_1) + ), $field, $value, $table); + } + )); $this->app->call(); @@ -47,10 +93,6 @@ public function testNoSessionCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->any()) - ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); - $this->app->call(); $this->assertEquals(401, $this->app->getResponse()->status()); @@ -66,9 +108,18 @@ public function testWrongOwnerCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->any()) + $this->dataConnectionMock->expects($this->exactly(3)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); + ->will($this->returnCallback( + function ($field, $value, $table) { + return $this->findOneByFieldCallback( + array( + 'sessions' => new TableDefinition('id', 111445, self::$SESSION_111445), + 'users' => new TableDefinition('id', 12, self::$USER_12), + 'modelbs' => new TableDefinition('id', 4, self::$MODELB_1) + ), $field, $value, $table); + } + )); $this->app->call(); @@ -85,9 +136,17 @@ public function testWrongOwnerCollectionCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->any()) + $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); + ->will($this->returnCallback( + function ($field, $value, $table) { + return $this->findOneByFieldCallback( + array( + 'sessions' => new TableDefinition('id', 111445, self::$SESSION_111445), + 'users' => new TableDefinition('id', 12, self::$USER_12) + ), $field, $value, $table); + } + )); $this->app->call(); @@ -95,33 +154,6 @@ public function testWrongOwnerCollectionCall() { $this->assertNotEmpty($this->app->getResponse()->body()); } - public function myFindOneByFieldCallback($field, $value, $table) { - $this->assertThat($table, $this->logicalOr($this->equalTo('sessions'), $this->equalTo('modelbs'))); - $this->assertEquals('id', $field); - - switch ($table) { - case 'sessions': - $this->assertThat($value, $this->logicalOr($this->equalTo(111444), - $this->equalTo(111445))); - - if ($value == 111444) { - return array( - 'created' => '1370663864', - 'userId' => 11 - ); - } - return array( - 'created' => '1370663865', - 'userId' => 12 - ); - case 'modelbs': - default: - $this->assertEquals('4', $value); - return array('modelBId' => 1, 'name' => 'test', 'testOwner' => 11); - } - } - - public function testDocs() { $middleware = new RESTOwnerOnlyAccess(); $sessHeaderMiddleware = new SessionHeader(); @@ -135,7 +167,7 @@ public function testDocs() { $docArray = array(); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $rRefMock = new \ReflectionMethod('\mabiTesting\ModelBController', '_restPostCollection'); + $rRefMock = new \ReflectionMethod('\mabiTesting\ModelBController', 'post'); $sessHeaderMiddleware->documentMethod($rClassMock, $rRefMock, $docArray); $middleware->documentMethod($rClassMock, $rRefMock, $docArray); diff --git a/extensions/Identity/tests/SessionHeaderTest.php b/extensions/Identity/tests/SessionHeaderTest.php index a40ff0c..997d92e 100644 --- a/extensions/Identity/tests/SessionHeaderTest.php +++ b/extensions/Identity/tests/SessionHeaderTest.php @@ -6,6 +6,7 @@ use MABI\Identity\Middleware\SessionHeader; use MABI\RESTAccess\RESTAccess; use MABI\Testing\MiddlewareTestCase; +use MABI\Testing\TableDefinition; include_once 'PHPUnit/Autoload.php'; include_once __DIR__ . '/../../../tests/middleware/MiddlewareTestCase.php'; @@ -15,6 +16,23 @@ class SessionHeaderTest extends MiddlewareTestCase { + protected static $SESSION_111444 = array( + 'created' => '1370663864', + 'userId' => 11 + ); + + protected static $USER_11 = array( + 'id' => 11, + 'created' => 1372375580, + 'firstName' => 'Photis', + 'lastName' => 'Patriotis', + 'email' => 'ppatriotis@gmail.com', + 'passHash' => '604cefb585491865043db59f5f200c08af016dc636bcb37c858199e20f082c10', + // result of: hash_hmac('sha256', '123', 'salt4456'); + 'salt' => 'salt4456', + 'lastAccessed' => 1379430989 + ); + public function testCall() { $middleware = new SessionHeader(); @@ -22,20 +40,31 @@ public function testCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->once()) + $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->with('id', 111444, 'sessions', array()) - ->will($this->returnValue(array( - 'created' => '1370663864', - 'user' => 'TEST-USER-ID-1' - ))); + ->will($this->returnCallback( + function ($field, $value, $table) { + return $this->findOneByFieldCallback( + array( + 'sessions' => new TableDefinition('id', 111444, self::$SESSION_111444), + 'users' => new TableDefinition('id', 11, self::$USER_11) + ), $field, $value, $table); + } + )); + // Makes sure that lastAccessed was updated on the user + $user_11_mod = self::$USER_11; + $user_11_mod['lastAccessed'] = (new \DateTime('now'))->getTimestamp(); + $this->dataConnectionMock->expects($this->once()) + ->method('save') + ->with('users', $user_11_mod, 'id', 11); + $this->app->call(); $this->assertEquals(200, $this->app->getResponse()->status()); $this->assertNotEmpty($this->app->getRequest()->session); $this->assertInstanceOf('\MABI\Identity\Session', $this->app->getRequest()->session); - $this->assertEquals('TEST-USER-ID-1', $this->app->getRequest()->session->user); + $this->assertEquals(11, $this->app->getRequest()->session->userId); } public function testDocs() { diff --git a/extensions/Identity/tests/SessionOnlyAccessTest.php b/extensions/Identity/tests/SessionOnlyAccessTest.php index c43cba2..94b0bbf 100644 --- a/extensions/Identity/tests/SessionOnlyAccessTest.php +++ b/extensions/Identity/tests/SessionOnlyAccessTest.php @@ -8,6 +8,7 @@ use MABI\Identity\Middleware\SessionOnlyAccess; use MABI\RESTAccess\RESTAccess; use MABI\Testing\MiddlewareTestCase; +use MABI\Testing\TableDefinition; include_once 'PHPUnit/Autoload.php'; include_once __DIR__ . '/../../../tests/middleware/MiddlewareTestCase.php'; @@ -18,6 +19,25 @@ class SessionOnlyAccessTest extends MiddlewareTestCase { + protected static $SESSION_111444 = array( + 'created' => '1370663864', + 'userId' => 11 + ); + + protected static $USER_11 = array( + 'id' => 11, + 'created' => 1372375580, + 'firstName' => 'Photis', + 'lastName' => 'Patriotis', + 'email' => 'ppatriotis@gmail.com', + 'passHash' => '604cefb585491865043db59f5f200c08af016dc636bcb37c858199e20f082c10', + // result of: hash_hmac('sha256', '123', 'salt4456'); + 'salt' => 'salt4456', + 'lastAccessed' => 1379430989 + ); + + protected static $MODELB_1 = array('modelBId' => 1, 'name' => 'test', 'testOwner' => 11); + public function testSuccessfulCall() { $middleware = new SessionOnlyAccess(); @@ -26,9 +46,20 @@ public function testSuccessfulCall() { $identity = new Identity($this->app, new RESTAccess($this->app)); $this->app->addExtension($identity); - $this->dataConnectionMock->expects($this->any()) + $this->dataConnectionMock->expects($this->exactly(3)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); + ->will($this->returnCallback( + function ($field, $value, $table) { + return $this->findOneByFieldCallback( + array( + 'sessions' => new TableDefinition('id', 111444, self::$SESSION_111444), + 'users' => new TableDefinition('id', 11, self::$USER_11), + 'modelbs' => new TableDefinition('id', 4, self::$MODELB_1) + ), $field, $value, $table); + } + )); + + // ->will($this->returnCallback(array($this, 'myFindOneByFieldCallback'))); $this->app->call(); @@ -97,7 +128,7 @@ public function testDocs() { $docArray = array(); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $rRefMock = new \ReflectionMethod('\mabiTesting\ModelBController', '_restPostCollection'); + $rRefMock = new \ReflectionMethod('\mabiTesting\ModelBController', 'post'); $sessHeaderMiddleware->documentMethod($rClassMock, $rRefMock, $docArray); $middleware->documentMethod($rClassMock, $rRefMock, $docArray); diff --git a/extensions/Identity/tests/UserControllerTest.php b/extensions/Identity/tests/UserControllerTest.php index e484ede..d04aef7 100644 --- a/extensions/Identity/tests/UserControllerTest.php +++ b/extensions/Identity/tests/UserControllerTest.php @@ -11,6 +11,7 @@ use MABI\Identity\Identity; use MABI\RESTAccess\RESTAccess; use MABI\Testing\AppTestCase; +use MABI\Testing\TableDefinition; class UserControllerTest extends AppTestCase { /** @@ -18,6 +19,23 @@ class UserControllerTest extends AppTestCase { */ protected $identityExtension; + protected static $USER_122 = array( + 'id' => 122, + 'created' => 1372375580, + 'firstName' => 'Photis', + 'lastName' => 'Patriotis', + 'email' => 'ppatriotis@gmail.com', + 'passHash' => '604cefb585491865043db59f5f200c08af016dc636bcb37c858199e20f082c10', + // result of: hash_hmac('sha256', '123', 'salt4456'); + 'salt' => 'salt4456', + 'lastAccessed' => 1379430989 + ); + + protected static $SESSION_111444 = array( + 'created' => '1370663864', + 'userId' => 122 + ); + public function setUpApp($env = array()) { parent::setUpApp($env); $identityExtension = new Identity($this->app, new RESTAccess($this->app)); @@ -66,7 +84,7 @@ public function testExistingEmailPostCollection() { $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCreateUserCallback'))); + ->will($this->returnCallback(array($this, 'findOneByFieldCreateUserCallback'))); $this->app->call(); $this->assertEquals(409, $this->app->getResponse()->status()); @@ -81,12 +99,12 @@ public function testSuccessfulPostCollection() { $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldCreateUserCallback'))); + ->will($this->returnCallback(array($this, 'findOneByFieldCreateUserCallback'))); // There are two insert calls, one for creating creating the session, and one for creating the user. $this->dataConnectionMock->expects($this->exactly(2)) ->method('insert') - ->will($this->returnCallback(array($this, 'myInsertCallback'))); + ->will($this->returnCallback(array($this, 'insertCallback'))); $this->app->call(); $this->assertEquals(200, $this->app->getResponse()->status()); @@ -97,8 +115,30 @@ public function testSuccessfulPostCollection() { $this->assertEquals('4', $output->newSessionId); } - public function setUpUpdateResourceTest($inputData) { - + public function findOneByFieldUpdateUserCallback($field, $value, $table, $fields, + $testEmailChange = TRUE, $testEmailExists = FALSE) { + switch ($table) { + case 'users': + switch ($field) { + case 'id': + return $this->returnTableValue($field, $value, new TableDefinition('id', 122, self::$USER_122)); + case 'email': + if ($testEmailChange) { + $this->assertEquals($value, 'ppatriotis2@gmail.com'); + return FALSE; + } + elseif ($testEmailExists) { + $this->assertEquals($value, 'ppatriotis+exists@gmail.com'); + return self::$USER_122; + } + } + $this->fail("Invalid user field value: $field (value: $value)"); + return NULL; + case 'sessions': + return $this->returnTableValue($field, $value, new TableDefinition('id', 111444, self::$SESSION_111444)); + } + $this->fail("Table '$table' should not be called"); + return NULL; } public function testSuccessfulUpdateResource() { @@ -109,43 +149,39 @@ public function testSuccessfulUpdateResource() { 'PATH_INFO' => '/users/122' )); - $this->dataConnectionMock->expects($this->exactly(3)) + $this->dataConnectionMock->expects($this->exactly(4)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldUpdateUserCallback'))); + ->will($this->returnCallback(array($this, 'findOneByFieldUpdateUserCallback'))); $this->dataConnectionMock->expects($this->once()) ->method('findAllByField') ->with('userId', 122, 'sessions', array()) - ->will($this->returnValue( - array( - array( - 'id' => 111444, - 'userId' => 122 - ), - array( - 'id' => 111445, - 'userId' => 122 - ), - ) - )); + ->will($this->returnValue(array(self::$SESSION_111444, array('id' => 111445, 'userId' => 122)))); $this->dataConnectionMock->expects($this->once()) ->method('deleteByField') ->with('id', 111445, 'sessions'); - $this->dataConnectionMock->expects($this->once()) + $userDateUpdate = self::$USER_122; + $userDateUpdate['lastAccessed'] = (new \DateTime('now'))->getTimestamp(); + $userOtherUpdate = self::$USER_122; + $userOtherUpdate['firstName'] = 'photis'; + $userOtherUpdate['lastName'] = 'patriotis2'; + $userOtherUpdate['email'] = 'ppatriotis2@gmail.com'; + $userOtherUpdate['passHash'] = 'facf86a33affc4c13a720ebfc8f5030faec8cedf2c1aa8855185e7d8cc0dab0b'; + // result of: hash_hmac('sha256', '777777', 'salt4456'); + + $this->dataConnectionMock->expects($this->exactly(2)) ->method('save') - //$table, $data, $field, $value - ->with('users', array( - 'id' => 122, - 'created' => 1372375580, - 'firstName' => 'photis', - 'lastName' => 'patriotis2', - 'email' => 'ppatriotis2@gmail.com', - 'passHash' => 'facf86a33affc4c13a720ebfc8f5030faec8cedf2c1aa8855185e7d8cc0dab0b', - // result of: hash_hmac('sha256', '777777', 'salt4456'); - 'salt' => 'salt4456' - ), 'id', 122); + ->will($this->returnCallback(function ($table, $data, $field, $value) use ($userDateUpdate, $userOtherUpdate) { + $this->assertEquals($table, 'users'); + $this->assertEquals($field, 'id'); + $this->assertEquals($value, 122); + $this->assertThat($data, $this->logicalOr( + $this->equalTo($userDateUpdate), + $this->equalTo($userOtherUpdate) + )); + })); $this->app->call(); $this->assertEquals(200, $this->app->getResponse()->status()); @@ -159,23 +195,29 @@ public function testSuccessfulUpdateResourceNoPassword() { 'PATH_INFO' => '/users/122' )); - $this->dataConnectionMock->expects($this->exactly(2)) + $this->dataConnectionMock->expects($this->exactly(3)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldUpdateUserCallback'))); + ->will($this->returnCallback(function ($field, $value, $table, $fields) { + return $this->findOneByFieldUpdateUserCallback($field, $value, $table, $fields, FALSE); + })); - $this->dataConnectionMock->expects($this->once()) + $userDateUpdate = self::$USER_122; + $userDateUpdate['lastAccessed'] = (new \DateTime('now'))->getTimestamp(); + $userOtherUpdate = self::$USER_122; + $userOtherUpdate['firstName'] = 'photis'; + $userOtherUpdate['lastName'] = 'patriotis2'; + + $this->dataConnectionMock->expects($this->exactly(2)) ->method('save') - //$table, $data, $field, $value - ->with('users', array( - 'id' => 122, - 'created' => 1372375580, - 'firstName' => 'photis', - 'lastName' => 'patriotis2', - 'email' => 'ppatriotis@gmail.com', - 'passHash' => '433813e38c7f564a06319c74c16d7e30f9cf645c0712a183ed0cbae3d74d24de', - // result of: hash_hmac('sha256', '777777', 'salt4456'); - 'salt' => 'salt4456' - ), 'id', 122); + ->will($this->returnCallback(function ($table, $data, $field, $value) use ($userDateUpdate, $userOtherUpdate) { + $this->assertEquals($table, 'users'); + $this->assertEquals($field, 'id'); + $this->assertEquals($value, 122); + $this->assertThat($data, $this->logicalOr( + $this->equalTo($userDateUpdate), + $this->equalTo($userOtherUpdate) + )); + })); $this->app->call(); $this->assertEquals(200, $this->app->getResponse()->status()); @@ -189,9 +231,11 @@ public function testSuccessfulUpdateResourceExistingEmail() { 'PATH_INFO' => '/users/122' )); - $this->dataConnectionMock->expects($this->exactly(3)) + $this->dataConnectionMock->expects($this->exactly(4)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldUpdateUserCallback'))); + ->will($this->returnCallback(function ($field, $value, $table, $fields) { + return $this->findOneByFieldUpdateUserCallback($field, $value, $table, $fields, FALSE, TRUE); + })); $this->app->call(); $this->assertEquals(409, $this->app->getResponse()->status()); @@ -217,7 +261,7 @@ public function testForgotPasswordNoTemplate() { $controllers = $this->app->getControllers(); - foreach($controllers as $controller) { + foreach ($controllers as $controller) { if (get_class($controller) == "MABI\\Identity\\UserController") { $controller->setEmailProvider(new \MABI\EmailSupport\Mandrill('12445', 'DEFAULT_SENDER', 'DEFAULT_NAME')); } @@ -236,7 +280,7 @@ public function testForgotPasswordNoEmail() { $controllers = $this->app->getControllers(); - foreach($controllers as $controller) { + foreach ($controllers as $controller) { if (get_class($controller) == "MABI\\Identity\\UserController") { $controller->setEmailProvider(new \MABI\EmailSupport\Mandrill('12445', 'DEFAULT_SENDER', 'DEFAULT_NAME')); $controller->setForgotEmailTemplate(new \MABI\EmailSupport\TokenTemplate('TestTemplate', 'Password Reset')); @@ -256,7 +300,7 @@ public function testForgotPasswordBadEmail() { $controllers = $this->app->getControllers(); - foreach($controllers as $controller) { + foreach ($controllers as $controller) { if (get_class($controller) == "MABI\\Identity\\UserController") { $controller->setEmailProvider(new \MABI\EmailSupport\Mandrill('12445', 'DEFAULT_SENDER', 'DEFAULT_NAME')); $controller->setForgotEmailTemplate(new \MABI\EmailSupport\TokenTemplate('TestTemplate', 'Password Reset')); @@ -265,7 +309,7 @@ public function testForgotPasswordBadEmail() { $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldForgotPasswordCallback'))); + ->will($this->returnCallback(array($this, 'findOneByFieldForgotPasswordCallback'))); $this->app->call(); $this->assertEquals(400, $this->app->getResponse()->status()); @@ -280,17 +324,21 @@ public function testForgotPassword() { $controllers = $this->app->getControllers(); - $mandrillMock = $this->getMock('\MABI\EmailSupport\Mandrill', array(), array('12445', 'DEFAULT_SENDER', 'DEFAULT_NAME')); + $mandrillMock = $this->getMock('\MABI\EmailSupport\Mandrill', array(), array( + '12445', + 'DEFAULT_SENDER', + 'DEFAULT_NAME' + )); $mandrillMock->expects($this->once()) ->method('sendEmail') ->will($this->returnValue(json_encode(array( "email" => "conord33@gmail.com", "status" => "sent", "_id" => "4b35185b2ca04ffaacbd1abf2e32dabc", - "reject_reason" => null + "reject_reason" => NULL )))); - foreach($controllers as $controller) { + foreach ($controllers as $controller) { if (get_class($controller) == "MABI\\Identity\\UserController") { $controller->setEmailProvider($mandrillMock); $controller->setForgotEmailTemplate(new \MABI\EmailSupport\TokenTemplate('TestTemplate', 'Password Reset')); @@ -299,72 +347,22 @@ public function testForgotPassword() { $this->dataConnectionMock->expects($this->exactly(2)) ->method('findOneByField') - ->will($this->returnCallback(array($this, 'myFindOneByFieldForgotPasswordCallback'))); + ->will($this->returnCallback(array($this, 'findOneByFieldForgotPasswordCallback'))); $this->app->call(); $this->assertEquals(200, $this->app->getResponse()->status()); } - public function myFindOneByFieldForgotPasswordCallback($field, $value, $table) { - $this->assertThat($field, $this->logicalOr($this->equalTo('id'), $this->equalTo('email'))); - - $userVal = array( - 'id' => 122, - 'created' => 1372375580, - 'firstName' => 'Photis', - 'lastName' => 'Patriotis', - 'email' => 'ppatriotis@gmail.com', - 'passHash' => '433813e38c7f564a06319c74c16d7e30f9cf645c0712a183ed0cbae3d74d24de', - // result of: hash_hmac('sha256', '123456', 'salt4456'); - 'salt' => 'salt4456' - ); - - switch ($field) { - case 'id': - $this->assertThat($table, $this->logicalOr($this->equalTo('sessions'), $this->equalTo('users'))); - if ($table == 'sessions') { - return False; - } - $this->assertEquals(122, $value); - return $userVal; - case 'email': - default: - $this->assertEquals('users', $table); - switch ($value) { - case 'ppatriotis2@gmail.com': - return FALSE; - case 'ppatriotis@gmail.com': - return $userVal; - default: - $this->fail('Invalid value: ' . $value); - return FALSE; - } - } - } - - public function myFindOneByFieldUpdateUserCallback($field, $value, $table) { + public function findOneByFieldForgotPasswordCallback($field, $value, $table) { $this->assertThat($field, $this->logicalOr($this->equalTo('id'), $this->equalTo('email'))); - $userVal = array( - 'id' => 122, - 'created' => 1372375580, - 'firstName' => 'Photis', - 'lastName' => 'Patriotis', - 'email' => 'ppatriotis@gmail.com', - 'passHash' => '433813e38c7f564a06319c74c16d7e30f9cf645c0712a183ed0cbae3d74d24de', - // result of: hash_hmac('sha256', '123456', 'salt4456'); - 'salt' => 'salt4456' - ); + $userVal = self::$USER_122; switch ($field) { case 'id': $this->assertThat($table, $this->logicalOr($this->equalTo('sessions'), $this->equalTo('users'))); if ($table == 'sessions') { - $this->assertEquals(111444, $value); - return array( - 'id' => 111444, - 'userId' => 122 - ); + return FALSE; } $this->assertEquals(122, $value); return $userVal; @@ -376,9 +374,6 @@ public function myFindOneByFieldUpdateUserCallback($field, $value, $table) { return FALSE; case 'ppatriotis@gmail.com': return $userVal; - case 'ppatriotis+exists@gmail.com': - $userVal['email'] = 'ppatriotis+exists@gmail.com'; - return $userVal; default: $this->fail('Invalid value: ' . $value); return FALSE; @@ -386,7 +381,7 @@ public function myFindOneByFieldUpdateUserCallback($field, $value, $table) { } } - public function myFindOneByFieldCreateUserCallback($field, $value, $table) { + public function findOneByFieldCreateUserCallback($field, $value, $table) { $this->assertThat($field, $this->logicalOr($this->equalTo('id'), $this->equalTo('email'))); switch ($field) { case 'id': @@ -400,35 +395,23 @@ public function myFindOneByFieldCreateUserCallback($field, $value, $table) { $this->assertThat($value, $this->logicalOr($this->equalTo('ppatriotis@gmail.com'), $this->equalTo('ppatriotis2@gmail.com'))); if ($value == 'ppatriotis@gmail.com') { - return array( - 'id' => '1', - 'created' => 1372375580, - 'firstName' => 'Photis', - 'lastName' => 'Patriotis', - 'email' => 'ppatriotis@gmail.com', - 'passHash' => '433813e38c7f564a06319c74c16d7e30f9cf645c0712a183ed0cbae3d74d24de', - // result of: hash_hmac('sha256', '123456', 'salt4456'); - 'salt' => 'salt4456' - ); + return self::$USER_122; } return FALSE; } } - public function myInsertCallback($table, $value) { - $this->assertThat($table, $this->logicalOr($this->equalTo('sessions'), $this->equalTo('users'))); + public function insertCallback($table, $value) { switch ($table) { case 'sessions': $this->assertEquals($value['userId'], '2'); return array( 'id' => 4, - 'date_created' => time(), + 'created' => time(), 'lastAccessed' => time(), 'userId' => '2', ); - break; case 'users': - default: return array( 'id' => 2, 'created' => 1372375585, @@ -440,6 +423,8 @@ public function myInsertCallback($table, $value) { 'salt' => 'salt4456' ); } + $this->fail("Table '$table' should not be called"); + return NULL; } } \ No newline at end of file diff --git a/extensions/RESTAccess/ObjectOnly.php b/extensions/RESTAccess/ObjectOnly.php index d04ff3a..2c0df63 100755 --- a/extensions/RESTAccess/ObjectOnly.php +++ b/extensions/RESTAccess/ObjectOnly.php @@ -11,10 +11,10 @@ class ObjectOnly extends RESTAccessMiddleware { protected function doesHaveAccessToMethod($methodName) { switch ($methodName) { - case '_restGetCollection': - case '_restPostCollection': - case '_restPutCollection': - case '_restDeleteCollection': + case 'get': + case 'post': + case 'put': + case 'delete': return FALSE; default: return TRUE; diff --git a/extensions/RESTAccess/PostAndObjectOnly.php b/extensions/RESTAccess/PostAndObjectOnly.php index dacc7b4..7469be1 100755 --- a/extensions/RESTAccess/PostAndObjectOnly.php +++ b/extensions/RESTAccess/PostAndObjectOnly.php @@ -10,9 +10,9 @@ class PostAndObjectOnly extends RESTAccessMiddleware { protected function doesHaveAccessToMethod($methodName) { switch ($methodName) { - case '_restGetCollection': - case '_restPutCollection': - case '_restDeleteCollection': + case 'get': + case 'put': + case 'delete': return FALSE; default: return TRUE; diff --git a/extensions/RESTAccess/PostOnly.php b/extensions/RESTAccess/PostOnly.php index 9d577e6..92b4e6a 100755 --- a/extensions/RESTAccess/PostOnly.php +++ b/extensions/RESTAccess/PostOnly.php @@ -11,9 +11,9 @@ class PostOnly extends RESTAccessMiddleware { protected function doesHaveAccessToMethod($methodName) { switch ($methodName) { - case '_restGetCollection': - case '_restPutCollection': - case '_restDeleteCollection': + case 'get': + case 'put': + case 'delete': case '_restGetResource': case '_restPutResource': case '_restDeleteResource': diff --git a/extensions/RESTAccess/RESTAccessMiddleware.php b/extensions/RESTAccess/RESTAccessMiddleware.php index 072f954..6864959 100755 --- a/extensions/RESTAccess/RESTAccessMiddleware.php +++ b/extensions/RESTAccess/RESTAccessMiddleware.php @@ -2,6 +2,7 @@ namespace MABI\RESTAccess; +use MABI\DefaultAppErrors; use MABI\Middleware; include_once __DIR__ . '/../../Middleware.php'; @@ -19,7 +20,7 @@ protected abstract function doesHaveAccessToMethod($methodName); public function call() { $callable = $this->getRouteCallable(); if (empty($callable) || !$this->doesHaveAccessToMethod($callable[1])) { - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } if (!empty($this->next)) { diff --git a/extensions/RESTAccess/ReadOnly.php b/extensions/RESTAccess/ReadOnly.php index 21fa957..70fb557 100755 --- a/extensions/RESTAccess/ReadOnly.php +++ b/extensions/RESTAccess/ReadOnly.php @@ -11,9 +11,9 @@ class ReadOnly extends RESTAccessMiddleware { protected function doesHaveAccessToMethod($methodName) { switch ($methodName) { - case '_restPostCollection': - case '_restPutCollection': - case '_restDeleteCollection': + case 'post': + case 'put': + case 'delete': case '_restPutResource': case '_restDeleteResource': return FALSE; diff --git a/extensions/RESTAccess/tests/ObjectOnlyTest.php b/extensions/RESTAccess/tests/ObjectOnlyTest.php index 22dd74a..cf164ce 100644 --- a/extensions/RESTAccess/tests/ObjectOnlyTest.php +++ b/extensions/RESTAccess/tests/ObjectOnlyTest.php @@ -48,8 +48,7 @@ public function testSkipDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPostCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'post'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNull($docArray); diff --git a/extensions/RESTAccess/tests/PostAndObjectOnlyTest.php b/extensions/RESTAccess/tests/PostAndObjectOnlyTest.php index 9fc5ada..f5f0567 100644 --- a/extensions/RESTAccess/tests/PostAndObjectOnlyTest.php +++ b/extensions/RESTAccess/tests/PostAndObjectOnlyTest.php @@ -48,8 +48,7 @@ public function testSkipDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPutCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'put'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNull($docArray); @@ -66,8 +65,7 @@ public function testFullDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPostCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'post'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNotEmpty($docArray); diff --git a/extensions/RESTAccess/tests/PostOnlyTest.php b/extensions/RESTAccess/tests/PostOnlyTest.php index d77b95f..4ee1388 100644 --- a/extensions/RESTAccess/tests/PostOnlyTest.php +++ b/extensions/RESTAccess/tests/PostOnlyTest.php @@ -57,8 +57,7 @@ public function testSkipDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPutCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'put'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNull($docArray); @@ -75,8 +74,7 @@ public function testFullDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPostCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'post'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNotEmpty($docArray); diff --git a/extensions/RESTAccess/tests/ReadOnlyTest.php b/extensions/RESTAccess/tests/ReadOnlyTest.php index 36f5031..1ed1f30 100644 --- a/extensions/RESTAccess/tests/ReadOnlyTest.php +++ b/extensions/RESTAccess/tests/ReadOnlyTest.php @@ -48,8 +48,7 @@ public function testSkipDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restPostCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'post'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNull($docArray); @@ -66,8 +65,7 @@ public function testFullDocs() { 'parameters' => array() ); $rClassMock = $this->getMock('\ReflectionClass', array(), array(), '', FALSE); - $reflectionMethod = new \ReflectionMethod(get_class($this->restController), - '_restGetCollection'); + $reflectionMethod = new \ReflectionMethod(get_class($this->restController), 'get'); $middleware->documentMethod($rClassMock, $reflectionMethod, $docArray); $this->assertNotEmpty($docArray); diff --git a/middleware/APIApplicationOnlyAccess.php b/middleware/APIApplicationOnlyAccess.php index 8a0c1ba..b25618a 100755 --- a/middleware/APIApplicationOnlyAccess.php +++ b/middleware/APIApplicationOnlyAccess.php @@ -2,9 +2,12 @@ namespace MABI\Middleware; +use MABI\DefaultAppErrors; +use MABI\Middleware; + include_once __DIR__ . '/../Middleware.php'; -class APIApplicationOnlyAccess extends \MABI\Middleware { +class APIApplicationOnlyAccess extends Middleware { /** * Call * @@ -15,7 +18,7 @@ class APIApplicationOnlyAccess extends \MABI\Middleware { */ public function call() { if (empty($this->getApp()->getRequest()->apiApplication)) { - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } if (!empty($this->next)) { diff --git a/middleware/NoAccess.php b/middleware/NoAccess.php index b0e6039..0174165 100755 --- a/middleware/NoAccess.php +++ b/middleware/NoAccess.php @@ -3,12 +3,13 @@ namespace MABI\Middleware; use MABI\Middleware; +use MABI\DefaultAppErrors; include_once __DIR__ . '/../Middleware.php'; class NoAccess extends Middleware { public function call() { - $this->getApp()->returnError('Not properly authenticated for this route', 401, 1007); + $this->getApp()->returnError(DefaultAppErrors::$NOT_AUTHORIZED); } public function documentMethod(\ReflectionClass $rClass, \ReflectionMethod $rMethod, array &$methodDoc) { diff --git a/tests/AppTestCase.php b/tests/AppTestCase.php index 5468c7c..39fe0f4 100644 --- a/tests/AppTestCase.php +++ b/tests/AppTestCase.php @@ -3,10 +3,56 @@ namespace MABI\Testing; use MABI\App; +use mabiTesting\Errors; include_once 'PHPUnit/Autoload.php'; include_once __DIR__ . '/../App.php'; include_once __DIR__ . '/MockDataConnection.php'; +include_once __DIR__ . '/TestApp/Errors.php'; + +class TableDefinition { + /** + * @var string + */ + protected $queryField; + + /** + * @var string + */ + protected $queryValue; + + /** + * @var array + */ + protected $returnValue; + + function __construct($queryField, $queryValue, $returnValue) { + $this->queryField = $queryField; + $this->queryValue = $queryValue; + $this->returnValue = $returnValue; + } + + /** + * @return string + */ + public function getQueryField() { + return $this->queryField; + } + + /** + * @return string + */ + public function getQueryValue() { + return $this->queryValue; + } + + /** + * @return array + */ + public function getReturnValue() { + return $this->returnValue; + } +} class AppTestCase extends \PHPUnit_Framework_TestCase { @@ -39,6 +85,31 @@ public function setUpApp($env = array()) { ); $this->app->addDataConnection('default', $this->dataConnectionMock); + $this->app->getErrorResponseDictionary()->overrideErrorResponses(new Errors()); + } + + protected function returnTableValue($field, $value, TableDefinition $tableDefinition) { + $this->assertEquals($field, $tableDefinition->getQueryField()); + $this->assertEquals($value, $tableDefinition->getQueryValue()); + return $tableDefinition->getReturnValue(); } + /** + * @param TableDefinition[] $tableDefinitions + * @param $field + * @param $value + * @param $table + * + * @return mixed + */ + protected function findOneByFieldCallback($tableDefinitions, $field, $value, $table) { + foreach ($tableDefinitions as $tdTable => $tableDefinition) { + if ($tdTable == $table) { + return $this->returnTableValue($field, $value, $tableDefinition); + } + } + + $this->fail("Table '$table' should not be called"); + return NULL; + } } \ No newline at end of file diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 0d1c5cd..ae604ad 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -28,6 +28,7 @@ private function setUpControllerApp($env = array()) { $dirControllerLoader = new \MABI\DirectoryControllerLoader('TestApp/TestControllerDir', $this->app, 'mabiTesting'); $this->controllerMock = $this->getMock('\mabiTesting\JustAController', array( + 'post', 'getTestFunc', 'postTestFunc', 'putTestFunc', @@ -47,6 +48,15 @@ private function setUpControllerApp($env = array()) { * test that custom routes were generated properly */ public function testRoutes() { + // Test base post + $this->setUpControllerApp(array('REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/justa')); + $this->controllerMock->expects($this->once()) + ->method('post') + ->will($this->returnValue('test')); + $this->app->call(); + $this->assertEquals(200, $this->app->getResponse()->status()); + $this->assertEquals('', $this->app->getResponse()->body()); + // Test custom get $this->setUpControllerApp(array('PATH_INFO' => '/justa/testfunc')); $this->controllerMock->expects($this->once()) diff --git a/tests/ErrorResponseTest.php b/tests/ErrorResponseTest.php new file mode 100644 index 0000000..97caf11 --- /dev/null +++ b/tests/ErrorResponseTest.php @@ -0,0 +1,65 @@ +setUpApp(array('PATH_INFO' => '/justa/customerror')); + + $this->app->call(); + + $this->assertJson($this->app->getResponse()->body()); + $response = json_decode($this->app->getResponse()->body()); + $this->assertEquals($response->error->code, 1); + $this->assertEquals($response->error->message, "New test error with a replacement string"); + $this->assertEquals(401, $this->app->getResponse()->status()); + } + + public function testCustomError2() { + $this->setUpApp(array('PATH_INFO' => '/justa/customerror2')); + + $this->app->call(); + + $this->assertJson($this->app->getResponse()->body()); + $response = json_decode($this->app->getResponse()->body()); + $this->assertEquals($response->error->code, 1); + $this->assertEquals($response->error->message, "Test error2"); + $this->assertEquals(401, $this->app->getResponse()->status()); + } + + public function testCustomError3() { + $this->setUpApp(array('PATH_INFO' => '/justa/customerror3')); + + $this->app->call(); + + $this->assertJson($this->app->getResponse()->body()); + $response = json_decode($this->app->getResponse()->body()); + $this->assertEquals($response->error->code, 1); + $this->assertEquals($response->error->message, "New test error with a replacement string"); + $this->assertEquals(401, $this->app->getResponse()->status()); + } + + public function testErrorOverride() { + $middleware = new \MABI\Middleware\SharedSecret(); + $middleware2 = new \MABI\Middleware\APIApplicationOnlyAccess(); + $this->setUpApp(array('PATH_INFO' => '/justa/testfunc'), array($middleware, $middleware2)); + + $this->app->call(); + + $this->assertJson($this->app->getResponse()->body()); + $response = json_decode($this->app->getResponse()->body()); + $this->assertEquals($response->error->code, 1007); + $this->assertEquals($response->error->message, "Why don't you just get out of here, ok?"); + $this->assertEquals(401, $this->app->getResponse()->status()); + } +} \ No newline at end of file diff --git a/tests/TestApp/Errors.php b/tests/TestApp/Errors.php new file mode 100644 index 0000000..df0cc50 --- /dev/null +++ b/tests/TestApp/Errors.php @@ -0,0 +1,41 @@ + array( + 'message' => 'New test error with !replacement', + 'httpcode' => 401, + 'code' => 1 + ) + ); + + // Used to test overrides on default errors + public static $NOT_AUTHORIZED = array( + 'NOT_AUTHORIZED' => array( + 'message' => 'Why don\'t you just get out of here, ok?', + 'httpcode' => 401, + 'code' => 1007 + ) + ); + + /** + * @ignore + */ + public static $IGNORED_STATIC = 'blah'; + + public static $INVALID_DEF1 = array( + 'NOT_AUTHORIZED' => array( + 'message' => 'Why don\'t you just get out of here, ok?', + 'code' => 1007 + ) + ); + + public static $INVALID_DEF2 = array(); + + public static $INVALID_DEF3 = 'blah'; +} diff --git a/tests/TestApp/TestControllerDir/JustAController.php b/tests/TestApp/TestControllerDir/JustAController.php index ac8af3f..cc7ab6d 100644 --- a/tests/TestApp/TestControllerDir/JustAController.php +++ b/tests/TestApp/TestControllerDir/JustAController.php @@ -2,6 +2,8 @@ namespace mabiTesting; +use MABI\Controller; + include_once __DIR__ . '/../../../Controller.php'; /** @@ -10,7 +12,11 @@ * @middleware MABI\Middleware\AnonymousIdentifier * @package mabiTesting */ -class JustAController extends \MABI\Controller { +class JustAController extends Controller { + public function post() { + echo 'post called'; + } + public function getTestFunc() { echo 'restGetTestFunc called'; } @@ -26,4 +32,20 @@ public function putTestFunc() { public function deleteTestFunc() { return 'restDeleteTestFunc called'; } + + public function getCustomError() { + $this->getApp()->returnError(Errors::$TEST_NEW_ERROR, array('!replacement' => 'a replacement string')); + } + + public function getCustomError2() { + $this->getApp()->returnError(array('TEST_NEW_ERROR_2' => array( + 'message' => 'Test error2', + 'httpcode' => 401, + 'code' => 1 + ))); + } + + public function getCustomError3() { + $this->getApp()->returnError('TEST_NEW_ERROR', array('!replacement' => 'a replacement string')); + } } \ No newline at end of file diff --git a/tests/middleware/MiddlewareTestCase.php b/tests/middleware/MiddlewareTestCase.php index 0914da2..5dd22dc 100644 --- a/tests/middleware/MiddlewareTestCase.php +++ b/tests/middleware/MiddlewareTestCase.php @@ -4,6 +4,7 @@ include_once __DIR__ . '/../../middleware/AnonymousIdentifier.php'; include_once __DIR__ . '/../../DirectoryControllerLoader.php'; +include_once __DIR__ . '/../../DirectoryModelLoader.php'; include_once __DIR__ . '/../AppTestCase.php'; class MiddlewareTestCase extends AppTestCase { @@ -38,6 +39,8 @@ class MiddlewareTestCase extends AppTestCase { public function setUpApp($env = array(), $middlewares = array()) { parent::setUpApp($env); + $this->app->setModelLoaders(array(new \MABI\DirectoryModelLoader(__DIR__ . '/../TestApp/TestModelDir', 'mabiTesting'))); + $dirControllerLoader = new \MABI\DirectoryControllerLoader(__DIR__ . '/../TestApp/TestControllerDir', $this->app, 'mabiTesting'); foreach ($dirControllerLoader->getControllers() as $controller) {