From 96f868dbf592b98452a2ea660dbb9c8d4181d2c7 Mon Sep 17 00:00:00 2001 From: helpfulrobot Date: Sat, 21 Nov 2015 19:21:34 +1300 Subject: [PATCH] Converted to PSR-2 --- code/BasicRestfulAuthenticator.php | 54 +- code/RestfulServer.php | 1190 +++++++++++++++------------- tests/travis/_config.php | 4 +- tests/travis/_ss_environment.php | 30 +- tests/unit/RestfulServerTest.php | 1166 ++++++++++++++------------- 5 files changed, 1279 insertions(+), 1165 deletions(-) diff --git a/code/BasicRestfulAuthenticator.php b/code/BasicRestfulAuthenticator.php index ac9af7f..843a712 100644 --- a/code/BasicRestfulAuthenticator.php +++ b/code/BasicRestfulAuthenticator.php @@ -8,32 +8,34 @@ * application accessing the RestfulServer to store logins in plain text (or in * decrytable form) */ -class BasicRestfulAuthenticator { +class BasicRestfulAuthenticator +{ + /** + * The authenticate function + * + * Takes the basic auth details and attempts to log a user in from the DB + * + * @return Member|false The Member object, or false if no member + */ + public static function authenticate() + { + //if there is no username or password, break + if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { + return false; + } - /** - * The authenticate function - * - * Takes the basic auth details and attempts to log a user in from the DB - * - * @return Member|false The Member object, or false if no member - */ - public static function authenticate() { - //if there is no username or password, break - if(!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) return false; - - //Attempt to authenticate with the default authenticator for the site - $authClass = Authenticator::get_default_authenticator(); - $member = $authClass::authenticate(array( - 'Email' => $_SERVER['PHP_AUTH_USER'], - 'Password' => $_SERVER['PHP_AUTH_PW'], - )); - - //Log the member in and return the member, if they were found - if($member) { - $member->LogIn(false); - return $member; - } - return false; - } + //Attempt to authenticate with the default authenticator for the site + $authClass = Authenticator::get_default_authenticator(); + $member = $authClass::authenticate(array( + 'Email' => $_SERVER['PHP_AUTH_USER'], + 'Password' => $_SERVER['PHP_AUTH_PW'], + )); + //Log the member in and return the member, if they were found + if ($member) { + $member->LogIn(false); + return $member; + } + return false; + } } diff --git a/code/RestfulServer.php b/code/RestfulServer.php index 4ecb8de..2462feb 100644 --- a/code/RestfulServer.php +++ b/code/RestfulServer.php @@ -26,560 +26,629 @@ * @package framework * @subpackage api */ -class RestfulServer extends Controller { - static $url_handlers = array( - '$ClassName/$ID/$Relation' => 'handleAction' - #'$ClassName/#ID' => 'handleItem', - #'$ClassName' => 'handleList', - ); +class RestfulServer extends Controller +{ + public static $url_handlers = array( + '$ClassName/$ID/$Relation' => 'handleAction' + #'$ClassName/#ID' => 'handleItem', + #'$ClassName' => 'handleList', + ); - protected static $api_base = "api/v1/"; + protected static $api_base = "api/v1/"; - protected static $authenticator = 'BasicRestfulAuthenticator'; + protected static $authenticator = 'BasicRestfulAuthenticator'; - /** - * If no extension is given in the request, resolve to this extension - * (and subsequently the {@link self::$default_mimetype}. - * - * @var string - */ - public static $default_extension = "xml"; - - /** - * If no extension is given, resolve the request to this mimetype. - * - * @var string - */ - protected static $default_mimetype = "text/xml"; - - /** - * @uses authenticate() - * @var Member - */ - protected $member; - - public static $allowed_actions = array( - 'index' - ); - - /* - function handleItem($request) { - return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID"))); - } + /** + * If no extension is given in the request, resolve to this extension + * (and subsequently the {@link self::$default_mimetype}. + * + * @var string + */ + public static $default_extension = "xml"; + + /** + * If no extension is given, resolve the request to this mimetype. + * + * @var string + */ + protected static $default_mimetype = "text/xml"; + + /** + * @uses authenticate() + * @var Member + */ + protected $member; + + public static $allowed_actions = array( + 'index' + ); + + /* + function handleItem($request) { + return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID"))); + } - function handleList($request) { - return new RestfulServer_List(DataObject::get($request->param("ClassName"),"")); - } - */ + function handleList($request) { + return new RestfulServer_List(DataObject::get($request->param("ClassName"),"")); + } + */ - function init() { - /* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults - * to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live. - * TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API) - */ - if (class_exists('SiteTree')) singleton('SiteTree')->extend('modelascontrollerInit', $this); - parent::init(); - } + public function init() + { + /* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults + * to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live. + * TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API) + */ + if (class_exists('SiteTree')) { + singleton('SiteTree')->extend('modelascontrollerInit', $this); + } + parent::init(); + } - /** - * This handler acts as the switchboard for the controller. - * Since no $Action url-param is set, all requests are sent here. - */ - function index() { - if(!isset($this->urlParams['ClassName'])) return $this->notFound(); - $className = $this->urlParams['ClassName']; - $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null; - $relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null; - - // Check input formats - if(!class_exists($className)) return $this->notFound(); - if($id && !is_numeric($id)) return $this->notFound(); - if( - $relation - && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation) - ) { - return $this->notFound(); - } - - // if api access is disabled, don't proceed - $apiAccess = singleton($className)->stat('api_access'); - if(!$apiAccess) return $this->permissionFailure(); + /** + * This handler acts as the switchboard for the controller. + * Since no $Action url-param is set, all requests are sent here. + */ + public function index() + { + if (!isset($this->urlParams['ClassName'])) { + return $this->notFound(); + } + $className = $this->urlParams['ClassName']; + $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null; + $relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null; + + // Check input formats + if (!class_exists($className)) { + return $this->notFound(); + } + if ($id && !is_numeric($id)) { + return $this->notFound(); + } + if ( + $relation + && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation) + ) { + return $this->notFound(); + } + + // if api access is disabled, don't proceed + $apiAccess = singleton($className)->stat('api_access'); + if (!$apiAccess) { + return $this->permissionFailure(); + } - // authenticate through HTTP BasicAuth - $this->member = $this->authenticate(); + // authenticate through HTTP BasicAuth + $this->member = $this->authenticate(); - // handle different HTTP verbs - if($this->request->isGET() || $this->request->isHEAD()) { - return $this->getHandler($className, $id, $relation); - } - - if($this->request->isPOST()) { - return $this->postHandler($className, $id, $relation); - } - - if($this->request->isPUT()) { - return $this->putHandler($className, $id, $relation); - } + // handle different HTTP verbs + if ($this->request->isGET() || $this->request->isHEAD()) { + return $this->getHandler($className, $id, $relation); + } + + if ($this->request->isPOST()) { + return $this->postHandler($className, $id, $relation); + } + + if ($this->request->isPUT()) { + return $this->putHandler($className, $id, $relation); + } - if($this->request->isDELETE()) { - return $this->deleteHandler($className, $id, $relation); - } + if ($this->request->isDELETE()) { + return $this->deleteHandler($className, $id, $relation); + } - // if no HTTP verb matches, return error - return $this->methodNotAllowed(); - } - - /** - * Handler for object read. - * - * The data object will be returned in the following format: - * - * - * Value - * ... - * - * ... - * - * - * - * - * ... - * - * - * - * - * - * - * Access is controlled by two variables: - * - * - static $api_access must be set. This enables the API on a class by class basis - * - $obj->canView() must return true. This lets you implement record-level security - * - * @todo Access checking - * - * @param String $className - * @param Int $id - * @param String $relation - * @return String The serialized representation of the requested object(s) - usually XML or JSON. - */ - protected function getHandler($className, $id, $relationName) { - $sort = ''; - - if($this->request->getVar('sort')) { - $dir = $this->request->getVar('dir'); - $sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC')); - } - - $limit = array( - 'start' => $this->request->getVar('start'), - 'limit' => $this->request->getVar('limit') - ); - - $params = $this->request->getVars(); - - $responseFormatter = $this->getResponseDataFormatter($className); - if(!$responseFormatter) return $this->unsupportedMediaType(); - - // $obj can be either a DataObject or a SS_List, - // depending on the request - if($id) { - // Format: /api/v1// - $obj = $this->getObjectQuery($className, $id, $params)->First(); - if(!$obj) return $this->notFound(); - if(!$obj->canView()) return $this->permissionFailure(); + // if no HTTP verb matches, return error + return $this->methodNotAllowed(); + } + + /** + * Handler for object read. + * + * The data object will be returned in the following format: + * + * + * Value + * ... + * + * ... + * + * + * + * + * ... + * + * + * + * + * + * + * Access is controlled by two variables: + * + * - static $api_access must be set. This enables the API on a class by class basis + * - $obj->canView() must return true. This lets you implement record-level security + * + * @todo Access checking + * + * @param String $className + * @param Int $id + * @param String $relation + * @return String The serialized representation of the requested object(s) - usually XML or JSON. + */ + protected function getHandler($className, $id, $relationName) + { + $sort = ''; + + if ($this->request->getVar('sort')) { + $dir = $this->request->getVar('dir'); + $sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC')); + } + + $limit = array( + 'start' => $this->request->getVar('start'), + 'limit' => $this->request->getVar('limit') + ); + + $params = $this->request->getVars(); + + $responseFormatter = $this->getResponseDataFormatter($className); + if (!$responseFormatter) { + return $this->unsupportedMediaType(); + } + + // $obj can be either a DataObject or a SS_List, + // depending on the request + if ($id) { + // Format: /api/v1// + $obj = $this->getObjectQuery($className, $id, $params)->First(); + if (!$obj) { + return $this->notFound(); + } + if (!$obj->canView()) { + return $this->permissionFailure(); + } - // Format: /api/v1/// - if($relationName) { - $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); - if(!$obj) return $this->notFound(); - - // TODO Avoid creating data formatter again for relation class (see above) - $responseFormatter = $this->getResponseDataFormatter($obj->dataClass()); - } - - } else { - // Format: /api/v1/ - $obj = $this->getObjectsQuery($className, $params, $sort, $limit); - } - - $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); - - $rawFields = $this->request->getVar('fields'); - $fields = $rawFields ? explode(',', $rawFields) : null; + // Format: /api/v1/// + if ($relationName) { + $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); + if (!$obj) { + return $this->notFound(); + } + + // TODO Avoid creating data formatter again for relation class (see above) + $responseFormatter = $this->getResponseDataFormatter($obj->dataClass()); + } + } else { + // Format: /api/v1/ + $obj = $this->getObjectsQuery($className, $params, $sort, $limit); + } + + $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); + + $rawFields = $this->request->getVar('fields'); + $fields = $rawFields ? explode(',', $rawFields) : null; - if($obj instanceof SS_List) { - $responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount()); - $objs = new ArrayList($obj->toArray()); - foreach($objs as $obj) if(!$obj->canView()) $objs->remove($obj); - return $responseFormatter->convertDataObjectSet($objs, $fields); - } else if(!$obj) { - $responseFormatter->setTotalSize(0); - return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields); - } else { - return $responseFormatter->convertDataObject($obj, $fields); - } - } - - /** - * Uses the default {@link SearchContext} specified through - * {@link DataObject::getDefaultSearchContext()} to augument - * an existing query object (mostly a component query from {@link DataObject}) - * with search clauses. - * - * @todo Allow specifying of different searchcontext getters on model-by-model basis - * - * @param string $className - * @param array $params - * @return SS_List - */ - protected function getSearchQuery($className, $params = null, $sort = null, - $limit = null, $existingQuery = null - ) { - if(singleton($className)->hasMethod('getRestfulSearchContext')) { - $searchContext = singleton($className)->{'getRestfulSearchContext'}(); - } else { - $searchContext = singleton($className)->getDefaultSearchContext(); - } - return $searchContext->getQuery($params, $sort, $limit, $existingQuery); - } - - /** - * Returns a dataformatter instance based on the request - * extension or mimetype. Falls back to {@link self::$default_extension}. - * - * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers - * @param String Classname of a DataObject - * @return DataFormatter - */ - protected function getDataFormatter($includeAcceptHeader = false, $className = null) { - $extension = $this->request->getExtension(); - $contentTypeWithEncoding = $this->request->getHeader('Content-Type'); - preg_match('/([^;]*)/',$contentTypeWithEncoding, $contentTypeMatches); - $contentType = $contentTypeMatches[0]; - $accept = $this->request->getHeader('Accept'); - $mimetypes = $this->request->getAcceptMimetypes(); - if(!$className) $className = $this->urlParams['ClassName']; + if ($obj instanceof SS_List) { + $responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount()); + $objs = new ArrayList($obj->toArray()); + foreach ($objs as $obj) { + if (!$obj->canView()) { + $objs->remove($obj); + } + } + return $responseFormatter->convertDataObjectSet($objs, $fields); + } elseif (!$obj) { + $responseFormatter->setTotalSize(0); + return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields); + } else { + return $responseFormatter->convertDataObject($obj, $fields); + } + } + + /** + * Uses the default {@link SearchContext} specified through + * {@link DataObject::getDefaultSearchContext()} to augument + * an existing query object (mostly a component query from {@link DataObject}) + * with search clauses. + * + * @todo Allow specifying of different searchcontext getters on model-by-model basis + * + * @param string $className + * @param array $params + * @return SS_List + */ + protected function getSearchQuery($className, $params = null, $sort = null, + $limit = null, $existingQuery = null + ) { + if (singleton($className)->hasMethod('getRestfulSearchContext')) { + $searchContext = singleton($className)->{'getRestfulSearchContext'}(); + } else { + $searchContext = singleton($className)->getDefaultSearchContext(); + } + return $searchContext->getQuery($params, $sort, $limit, $existingQuery); + } + + /** + * Returns a dataformatter instance based on the request + * extension or mimetype. Falls back to {@link self::$default_extension}. + * + * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers + * @param String Classname of a DataObject + * @return DataFormatter + */ + protected function getDataFormatter($includeAcceptHeader = false, $className = null) + { + $extension = $this->request->getExtension(); + $contentTypeWithEncoding = $this->request->getHeader('Content-Type'); + preg_match('/([^;]*)/', $contentTypeWithEncoding, $contentTypeMatches); + $contentType = $contentTypeMatches[0]; + $accept = $this->request->getHeader('Accept'); + $mimetypes = $this->request->getAcceptMimetypes(); + if (!$className) { + $className = $this->urlParams['ClassName']; + } - // get formatter - if(!empty($extension)) { - $formatter = DataFormatter::for_extension($extension); - }elseif($includeAcceptHeader && !empty($accept) && $accept != '*/*') { - $formatter = DataFormatter::for_mimetypes($mimetypes); - if(!$formatter) $formatter = DataFormatter::for_extension(self::$default_extension); - } elseif(!empty($contentType)) { - $formatter = DataFormatter::for_mimetype($contentType); - } else { - $formatter = DataFormatter::for_extension(self::$default_extension); - } + // get formatter + if (!empty($extension)) { + $formatter = DataFormatter::for_extension($extension); + } elseif ($includeAcceptHeader && !empty($accept) && $accept != '*/*') { + $formatter = DataFormatter::for_mimetypes($mimetypes); + if (!$formatter) { + $formatter = DataFormatter::for_extension(self::$default_extension); + } + } elseif (!empty($contentType)) { + $formatter = DataFormatter::for_mimetype($contentType); + } else { + $formatter = DataFormatter::for_extension(self::$default_extension); + } - if(!$formatter) return false; - - // set custom fields - if($customAddFields = $this->request->getVar('add_fields')) { - $formatter->setCustomAddFields(explode(',',$customAddFields)); - } - if($customFields = $this->request->getVar('fields')) { - $formatter->setCustomFields(explode(',',$customFields)); - } - $formatter->setCustomRelations($this->getAllowedRelations($className)); - - $apiAccess = singleton($className)->stat('api_access'); - if(is_array($apiAccess)) { - $formatter->setCustomAddFields( - array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']) - ); - if($formatter->getCustomFields()) { - $formatter->setCustomFields( - array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view']) - ); - } else { - $formatter->setCustomFields((array)$apiAccess['view']); - } - if($formatter->getCustomRelations()) { - $formatter->setCustomRelations( - array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view']) - ); - } else { - $formatter->setCustomRelations((array)$apiAccess['view']); - } - - } + if (!$formatter) { + return false; + } + + // set custom fields + if ($customAddFields = $this->request->getVar('add_fields')) { + $formatter->setCustomAddFields(explode(',', $customAddFields)); + } + if ($customFields = $this->request->getVar('fields')) { + $formatter->setCustomFields(explode(',', $customFields)); + } + $formatter->setCustomRelations($this->getAllowedRelations($className)); + + $apiAccess = singleton($className)->stat('api_access'); + if (is_array($apiAccess)) { + $formatter->setCustomAddFields( + array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']) + ); + if ($formatter->getCustomFields()) { + $formatter->setCustomFields( + array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view']) + ); + } else { + $formatter->setCustomFields((array)$apiAccess['view']); + } + if ($formatter->getCustomRelations()) { + $formatter->setCustomRelations( + array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view']) + ); + } else { + $formatter->setCustomRelations((array)$apiAccess['view']); + } + } - // set relation depth - $relationDepth = $this->request->getVar('relationdepth'); - if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth; - - return $formatter; - } - - /** - * @param String Classname of a DataObject - * @return DataFormatter - */ - protected function getRequestDataFormatter($className = null) { - return $this->getDataFormatter(false, $className); - } - - /** - * @param String Classname of a DataObject - * @return DataFormatter - */ - protected function getResponseDataFormatter($className = null) { - return $this->getDataFormatter(true, $className); - } - - /** - * Handler for object delete - */ - protected function deleteHandler($className, $id) { - $obj = DataObject::get_by_id($className, $id); - if(!$obj) return $this->notFound(); - if(!$obj->canDelete()) return $this->permissionFailure(); - - $obj->delete(); - - $this->getResponse()->setStatusCode(204); // No Content - return true; - } + // set relation depth + $relationDepth = $this->request->getVar('relationdepth'); + if (is_numeric($relationDepth)) { + $formatter->relationDepth = (int)$relationDepth; + } + + return $formatter; + } + + /** + * @param String Classname of a DataObject + * @return DataFormatter + */ + protected function getRequestDataFormatter($className = null) + { + return $this->getDataFormatter(false, $className); + } + + /** + * @param String Classname of a DataObject + * @return DataFormatter + */ + protected function getResponseDataFormatter($className = null) + { + return $this->getDataFormatter(true, $className); + } + + /** + * Handler for object delete + */ + protected function deleteHandler($className, $id) + { + $obj = DataObject::get_by_id($className, $id); + if (!$obj) { + return $this->notFound(); + } + if (!$obj->canDelete()) { + return $this->permissionFailure(); + } + + $obj->delete(); + + $this->getResponse()->setStatusCode(204); // No Content + return true; + } - /** - * Handler for object write - */ - protected function putHandler($className, $id) { - $obj = DataObject::get_by_id($className, $id); - if(!$obj) return $this->notFound(); - if(!$obj->canEdit()) return $this->permissionFailure(); - - $reqFormatter = $this->getRequestDataFormatter($className); - if(!$reqFormatter) return $this->unsupportedMediaType(); - - $responseFormatter = $this->getResponseDataFormatter($className); - if(!$responseFormatter) return $this->unsupportedMediaType(); - - $obj = $this->updateDataObject($obj, $reqFormatter); - - $this->getResponse()->setStatusCode(200); // Success - $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); + /** + * Handler for object write + */ + protected function putHandler($className, $id) + { + $obj = DataObject::get_by_id($className, $id); + if (!$obj) { + return $this->notFound(); + } + if (!$obj->canEdit()) { + return $this->permissionFailure(); + } + + $reqFormatter = $this->getRequestDataFormatter($className); + if (!$reqFormatter) { + return $this->unsupportedMediaType(); + } + + $responseFormatter = $this->getResponseDataFormatter($className); + if (!$responseFormatter) { + return $this->unsupportedMediaType(); + } + + $obj = $this->updateDataObject($obj, $reqFormatter); + + $this->getResponse()->setStatusCode(200); // Success + $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); - // Append the default extension for the output format to the Location header - // or else we'll use the default (XML) - $types = $responseFormatter->supportedExtensions(); - $type = ''; - if (count($types)) { - $type = ".{$types[0]}"; - } + // Append the default extension for the output format to the Location header + // or else we'll use the default (XML) + $types = $responseFormatter->supportedExtensions(); + $type = ''; + if (count($types)) { + $type = ".{$types[0]}"; + } - $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type); - $this->getResponse()->addHeader('Location', $objHref); - - return $responseFormatter->convertDataObject($obj); - } + $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type); + $this->getResponse()->addHeader('Location', $objHref); + + return $responseFormatter->convertDataObject($obj); + } - /** - * Handler for object append / method call. - * - * @todo Posting to an existing URL (without a relation) - * current resolves in creatig a new element, - * rather than a "Conflict" message. - */ - protected function postHandler($className, $id, $relation) { - if($id) { - if(!$relation) { - $this->response->setStatusCode(409); - return 'Conflict'; - } - - $obj = DataObject::get_by_id($className, $id); - if(!$obj) return $this->notFound(); - - if(!$obj->hasMethod($relation)) { - return $this->notFound(); - } - - if(!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) { - return $this->permissionFailure(); - } - - $obj->$relation(); - - $this->getResponse()->setStatusCode(204); // No Content - return true; - } else { - if(!singleton($className)->canCreate()) return $this->permissionFailure(); - $obj = new $className(); - - $reqFormatter = $this->getRequestDataFormatter($className); - if(!$reqFormatter) return $this->unsupportedMediaType(); - - $responseFormatter = $this->getResponseDataFormatter($className); - - $obj = $this->updateDataObject($obj, $reqFormatter); - - $this->getResponse()->setStatusCode(201); // Created - $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); + /** + * Handler for object append / method call. + * + * @todo Posting to an existing URL (without a relation) + * current resolves in creatig a new element, + * rather than a "Conflict" message. + */ + protected function postHandler($className, $id, $relation) + { + if ($id) { + if (!$relation) { + $this->response->setStatusCode(409); + return 'Conflict'; + } + + $obj = DataObject::get_by_id($className, $id); + if (!$obj) { + return $this->notFound(); + } + + if (!$obj->hasMethod($relation)) { + return $this->notFound(); + } + + if (!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) { + return $this->permissionFailure(); + } + + $obj->$relation(); + + $this->getResponse()->setStatusCode(204); // No Content + return true; + } else { + if (!singleton($className)->canCreate()) { + return $this->permissionFailure(); + } + $obj = new $className(); + + $reqFormatter = $this->getRequestDataFormatter($className); + if (!$reqFormatter) { + return $this->unsupportedMediaType(); + } + + $responseFormatter = $this->getResponseDataFormatter($className); + + $obj = $this->updateDataObject($obj, $reqFormatter); + + $this->getResponse()->setStatusCode(201); // Created + $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); - // Append the default extension for the output format to the Location header - // or else we'll use the default (XML) - $types = $responseFormatter->supportedExtensions(); - $type = ''; - if (count($types)) { - $type = ".{$types[0]}"; - } + // Append the default extension for the output format to the Location header + // or else we'll use the default (XML) + $types = $responseFormatter->supportedExtensions(); + $type = ''; + if (count($types)) { + $type = ".{$types[0]}"; + } - $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type); - $this->getResponse()->addHeader('Location', $objHref); - - return $responseFormatter->convertDataObject($obj); - } - } - - /** - * Converts either the given HTTP Body into an array - * (based on the DataFormatter instance), or returns - * the POST variables. - * Automatically filters out certain critical fields - * that shouldn't be set by the client (e.g. ID). - * - * @param DataObject $obj - * @param DataFormatter $formatter - * @return DataObject The passed object - */ - protected function updateDataObject($obj, $formatter) { - // if neither an http body nor POST data is present, return error - $body = $this->request->getBody(); - if(!$body && !$this->request->postVars()) { - $this->getResponse()->setStatusCode(204); // No Content - return 'No Content'; - } - - if(!empty($body)) { - $data = $formatter->convertStringToArray($body); - } else { - // assume application/x-www-form-urlencoded which is automatically parsed by PHP - $data = $this->request->postVars(); - } - - // @todo Disallow editing of certain keys in database - $data = array_diff_key($data, array('ID','Created')); - - $apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access'); - if(is_array($apiAccess) && isset($apiAccess['edit'])) { - $data = array_intersect_key($data, array_combine($apiAccess['edit'],$apiAccess['edit'])); - } + $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type); + $this->getResponse()->addHeader('Location', $objHref); + + return $responseFormatter->convertDataObject($obj); + } + } + + /** + * Converts either the given HTTP Body into an array + * (based on the DataFormatter instance), or returns + * the POST variables. + * Automatically filters out certain critical fields + * that shouldn't be set by the client (e.g. ID). + * + * @param DataObject $obj + * @param DataFormatter $formatter + * @return DataObject The passed object + */ + protected function updateDataObject($obj, $formatter) + { + // if neither an http body nor POST data is present, return error + $body = $this->request->getBody(); + if (!$body && !$this->request->postVars()) { + $this->getResponse()->setStatusCode(204); // No Content + return 'No Content'; + } + + if (!empty($body)) { + $data = $formatter->convertStringToArray($body); + } else { + // assume application/x-www-form-urlencoded which is automatically parsed by PHP + $data = $this->request->postVars(); + } + + // @todo Disallow editing of certain keys in database + $data = array_diff_key($data, array('ID', 'Created')); + + $apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access'); + if (is_array($apiAccess) && isset($apiAccess['edit'])) { + $data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit'])); + } - $obj->update($data); - $obj->write(); - - return $obj; - } - - /** - * Gets a single DataObject by ID, - * through a request like /api/v1// - * - * @param string $className - * @param int $id - * @param array $params - * @return DataList - */ - protected function getObjectQuery($className, $id, $params) { - return DataList::create($className)->byIDs(array($id)); - } - - /** - * @param DataObject $obj - * @param array $params - * @param int|array $sort - * @param int|array $limit - * @return SQLQuery - */ - protected function getObjectsQuery($className, $params, $sort, $limit) { - return $this->getSearchQuery($className, $params, $sort, $limit); - } - - - /** - * @param DataObject $obj - * @param array $params - * @param int|array $sort - * @param int|array $limit - * @param string $relationName - * @return SQLQuery|boolean - */ - protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) { - // The relation method will return a DataList, that getSearchQuery subsequently manipulates - if($obj->hasMethod($relationName)) { - if($relationClass = $obj->has_one($relationName)) { - $joinField = $relationName . 'ID'; - $list = DataList::create($relationClass)->byIDs(array($obj->$joinField)); - } else { - $list = $obj->$relationName(); - } - - $apiAccess = singleton($list->dataClass())->stat('api_access'); - if(!$apiAccess) return false; - - return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list); - } - } - - protected function permissionFailure() { - // return a 401 - $this->getResponse()->setStatusCode(401); - $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"'); - $this->getResponse()->addHeader('Content-Type', 'text/plain'); - return "You don't have access to this item through the API."; - } + $obj->update($data); + $obj->write(); + + return $obj; + } + + /** + * Gets a single DataObject by ID, + * through a request like /api/v1// + * + * @param string $className + * @param int $id + * @param array $params + * @return DataList + */ + protected function getObjectQuery($className, $id, $params) + { + return DataList::create($className)->byIDs(array($id)); + } + + /** + * @param DataObject $obj + * @param array $params + * @param int|array $sort + * @param int|array $limit + * @return SQLQuery + */ + protected function getObjectsQuery($className, $params, $sort, $limit) + { + return $this->getSearchQuery($className, $params, $sort, $limit); + } + + + /** + * @param DataObject $obj + * @param array $params + * @param int|array $sort + * @param int|array $limit + * @param string $relationName + * @return SQLQuery|boolean + */ + protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) + { + // The relation method will return a DataList, that getSearchQuery subsequently manipulates + if ($obj->hasMethod($relationName)) { + if ($relationClass = $obj->has_one($relationName)) { + $joinField = $relationName . 'ID'; + $list = DataList::create($relationClass)->byIDs(array($obj->$joinField)); + } else { + $list = $obj->$relationName(); + } + + $apiAccess = singleton($list->dataClass())->stat('api_access'); + if (!$apiAccess) { + return false; + } + + return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list); + } + } + + protected function permissionFailure() + { + // return a 401 + $this->getResponse()->setStatusCode(401); + $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"'); + $this->getResponse()->addHeader('Content-Type', 'text/plain'); + return "You don't have access to this item through the API."; + } - protected function notFound() { - // return a 404 - $this->getResponse()->setStatusCode(404); - $this->getResponse()->addHeader('Content-Type', 'text/plain'); - return "That object wasn't found"; - } - - protected function methodNotAllowed() { - $this->getResponse()->setStatusCode(405); - $this->getResponse()->addHeader('Content-Type', 'text/plain'); - return "Method Not Allowed"; - } - - protected function unsupportedMediaType() { - $this->response->setStatusCode(415); // Unsupported Media Type - $this->getResponse()->addHeader('Content-Type', 'text/plain'); - return "Unsupported Media Type"; - } - - /** - * A function to authenticate a user - * - * @return Member|false the logged in member - */ - protected function authenticate() { - $authClass = self::config()->authenticator; - return $authClass::authenticate(); - } - - /** - * Return only relations which have $api_access enabled. - * @todo Respect field level permissions once they are available in core - * - * @param string $class - * @param Member $member - * @return array - */ - protected function getAllowedRelations($class, $member = null) { - $allowedRelations = array(); - $obj = singleton($class); - $relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many(); - if($relations) foreach($relations as $relName => $relClass) { - if(singleton($relClass)->stat('api_access')) { - $allowedRelations[] = $relName; - } - } - return $allowedRelations; - } - + protected function notFound() + { + // return a 404 + $this->getResponse()->setStatusCode(404); + $this->getResponse()->addHeader('Content-Type', 'text/plain'); + return "That object wasn't found"; + } + + protected function methodNotAllowed() + { + $this->getResponse()->setStatusCode(405); + $this->getResponse()->addHeader('Content-Type', 'text/plain'); + return "Method Not Allowed"; + } + + protected function unsupportedMediaType() + { + $this->response->setStatusCode(415); // Unsupported Media Type + $this->getResponse()->addHeader('Content-Type', 'text/plain'); + return "Unsupported Media Type"; + } + + /** + * A function to authenticate a user + * + * @return Member|false the logged in member + */ + protected function authenticate() + { + $authClass = self::config()->authenticator; + return $authClass::authenticate(); + } + + /** + * Return only relations which have $api_access enabled. + * @todo Respect field level permissions once they are available in core + * + * @param string $class + * @param Member $member + * @return array + */ + protected function getAllowedRelations($class, $member = null) + { + $allowedRelations = array(); + $obj = singleton($class); + $relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many(); + if ($relations) { + foreach ($relations as $relName => $relClass) { + if (singleton($relClass)->stat('api_access')) { + $allowedRelations[] = $relName; + } + } + } + return $allowedRelations; + } } /** @@ -588,18 +657,21 @@ class RestfulServer extends Controller { * @package framework * @subpackage api */ -class RestfulServer_List { - static $url_handlers = array( - '#ID' => 'handleItem', - ); +class RestfulServer_List +{ + public static $url_handlers = array( + '#ID' => 'handleItem', + ); - function __construct($list) { - $this->list = $list; - } - - function handleItem($request) { - return new RestulServer_Item($this->list->getById($request->param('ID'))); - } + public function __construct($list) + { + $this->list = $list; + } + + public function handleItem($request) + { + return new RestulServer_Item($this->list->getById($request->param('ID'))); + } } /** @@ -608,20 +680,26 @@ class RestfulServer_List { * @package framework * @subpackage api */ -class RestfulServer_Item { - static $url_handlers = array( - '$Relation' => 'handleRelation', - ); +class RestfulServer_Item +{ + public static $url_handlers = array( + '$Relation' => 'handleRelation', + ); - function __construct($item) { - $this->item = $item; - } - - function handleRelation($request) { - $funcName = $request('Relation'); - $relation = $this->item->$funcName(); + public function __construct($item) + { + $this->item = $item; + } + + public function handleRelation($request) + { + $funcName = $request('Relation'); + $relation = $this->item->$funcName(); - if($relation instanceof SS_List) return new RestfulServer_List($relation); - else return new RestfulServer_Item($relation); - } + if ($relation instanceof SS_List) { + return new RestfulServer_List($relation); + } else { + return new RestfulServer_Item($relation); + } + } } diff --git a/tests/travis/_config.php b/tests/travis/_config.php index 3508957..e226259 100644 --- a/tests/travis/_config.php +++ b/tests/travis/_config.php @@ -19,4 +19,6 @@ MySQLDatabase::set_connection_charset('utf8'); SSViewer::set_theme('simple'); // Enable nested URLs for this site (e.g. page/sub-page/) -if(class_exists('SiteTree')) SiteTree::enable_nested_urls(); \ No newline at end of file +if (class_exists('SiteTree')) { + SiteTree::enable_nested_urls(); +} diff --git a/tests/travis/_ss_environment.php b/tests/travis/_ss_environment.php index eb38829..653c416 100644 --- a/tests/travis/_ss_environment.php +++ b/tests/travis/_ss_environment.php @@ -5,33 +5,33 @@ define('SS_ENVIRONMENT_TYPE', 'dev'); /* Database connection */ $db = getenv('TESTDB'); -switch($db) { +switch ($db) { case "PGSQL"; - define('SS_DATABASE_CLASS', 'PostgreSQLDatabase'); - define('SS_DATABASE_USERNAME', 'postgres'); - define('SS_DATABASE_PASSWORD', ''); - break; + define('SS_DATABASE_CLASS', 'PostgreSQLDatabase'); + define('SS_DATABASE_USERNAME', 'postgres'); + define('SS_DATABASE_PASSWORD', ''); + break; case "MYSQL": - define('SS_DATABASE_CLASS', 'MySQLDatabase'); - define('SS_DATABASE_USERNAME', 'root'); - define('SS_DATABASE_PASSWORD', ''); - break; + define('SS_DATABASE_CLASS', 'MySQLDatabase'); + define('SS_DATABASE_USERNAME', 'root'); + define('SS_DATABASE_PASSWORD', ''); + break; default: - define('SS_DATABASE_CLASS', 'SQLitePDODatabase'); - define('SS_DATABASE_USERNAME', 'root'); - define('SS_DATABASE_PASSWORD', ''); + define('SS_DATABASE_CLASS', 'SQLitePDODatabase'); + define('SS_DATABASE_USERNAME', 'root'); + define('SS_DATABASE_PASSWORD', ''); } echo SS_DATABASE_CLASS; -define('SS_DATABASE_SERVER', 'localhost'); +define('SS_DATABASE_SERVER', 'localhost'); define('SS_DATABASE_CHOOSE_NAME', true); -/* Configure a default username and password to access the CMS on all sites in this environment. */ -define('SS_DEFAULT_ADMIN_USERNAME', 'username'); +/* Configure a default username and password to access the CMS on all sites in this environment. */ +define('SS_DEFAULT_ADMIN_USERNAME', 'username'); define('SS_DEFAULT_ADMIN_PASSWORD', 'password'); $_FILE_TO_URL_MAPPING[dirname(__FILE__)] = 'http://localhost'; diff --git a/tests/unit/RestfulServerTest.php b/tests/unit/RestfulServerTest.php index e5cf595..54a9d77 100644 --- a/tests/unit/RestfulServerTest.php +++ b/tests/unit/RestfulServerTest.php @@ -6,452 +6,474 @@ * @todo Test DELETE verb * */ -class RestfulServerTest extends SapphireTest { - - static $fixture_file = 'RestfulServerTest.yml'; +class RestfulServerTest extends SapphireTest +{ + public static $fixture_file = 'RestfulServerTest.yml'; - protected $extraDataObjects = array( - 'RestfulServerTest_Comment', - 'RestfulServerTest_SecretThing', - 'RestfulServerTest_Page', - 'RestfulServerTest_Author', - 'RestfulServerTest_AuthorRating', - ); + protected $extraDataObjects = array( + 'RestfulServerTest_Comment', + 'RestfulServerTest_SecretThing', + 'RestfulServerTest_Page', + 'RestfulServerTest_Author', + 'RestfulServerTest_AuthorRating', + ); - public function testApiAccess() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - $page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1'); - - // normal GET should succeed with $api_access enabled - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - - $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'user'; - - // even with logged in user a GET with $api_access disabled should fail - $url = "/api/v1/RestfulServerTest_Page/" . $page1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 401); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testApiAccessBoolean() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertContains('', $response->getBody()); - $this->assertContains('', $response->getBody()); - $this->assertContains('', $response->getBody()); - $this->assertContains('getBody()); - $this->assertContains('getBody()); - } - - public function testAuthenticatedGET() { - $thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1'); - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + public function testApiAccess() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + $page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1'); + + // normal GET should succeed with $api_access enabled + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + + $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'user'; + + // even with logged in user a GET with $api_access disabled should fail + $url = "/api/v1/RestfulServerTest_Page/" . $page1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 401); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testApiAccessBoolean() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertContains('', $response->getBody()); + $this->assertContains('', $response->getBody()); + $this->assertContains('', $response->getBody()); + $this->assertContains('getBody()); + $this->assertContains('getBody()); + } + + public function testAuthenticatedGET() + { + $thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1'); + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - // @todo create additional mock object with authenticated VIEW permissions - $url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 401); - - $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'user'; - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testAuthenticatedPUT() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $data = array('Comment' => 'created'); - - $response = Director::test($url, $data, null, 'PUT'); - $this->assertEquals($response->getStatusCode(), 401); // Permission failure - - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - $response = Director::test($url, $data, null, 'PUT'); - $this->assertEquals($response->getStatusCode(), 200); // Success - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testGETRelationshipsXML() { - $author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1'); - $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); - $rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2'); - - // @todo should be set up by fixtures, doesn't work for some reason... - $author1->Ratings()->add($rating1); - $author1->Ratings()->add($rating2); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - - $responseArr = Convert::xml2array($response->getBody()); - $ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating']; - $this->assertEquals(count($ratingsArr), 2); - $ratingIDs = array( - (int)$ratingsArr[0]['@attributes']['id'], - (int)$ratingsArr[1]['@attributes']['id'] - ); - $this->assertContains($rating1->ID, $ratingIDs); - $this->assertContains($rating2->ID, $ratingIDs); - } - - public function testGETManyManyRelationshipsXML() { - // author4 has related authors author2 and author3 - $author2 = $this->objFromFixture('RestfulServerTest_Author', 'author2'); - $author3 = $this->objFromFixture('RestfulServerTest_Author', 'author3'); - $author4 = $this->objFromFixture('RestfulServerTest_Author', 'author4'); - - $url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors'; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals(200, $response->getStatusCode()); - $arr = Convert::xml2array($response->getBody()); - $authorsArr = $arr['RestfulServerTest_Author']; - - $this->assertEquals(count($authorsArr), 2); - $ratingIDs = array( - (int)$authorsArr[0]['ID'], - (int)$authorsArr[1]['ID'] - ); - $this->assertContains($author2->ID, $ratingIDs); - $this->assertContains($author3->ID, $ratingIDs); - } + // @todo create additional mock object with authenticated VIEW permissions + $url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 401); + + $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'user'; + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testAuthenticatedPUT() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $data = array('Comment' => 'created'); + + $response = Director::test($url, $data, null, 'PUT'); + $this->assertEquals($response->getStatusCode(), 401); // Permission failure - public function testPUTWithFormEncoded() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $body = 'Name=Updated Comment&Comment=updated'; - $headers = array( - 'Content-Type' => 'application/x-www-form-urlencoded' - ); - $response = Director::test($url, null, null, 'PUT', $body, $headers); - $this->assertEquals($response->getStatusCode(), 200); // Success - // Assumption: XML is default output - $responseArr = Convert::xml2array($response->getBody()); - $this->assertEquals($responseArr['ID'], $comment1->ID); - $this->assertEquals($responseArr['Comment'], 'updated'); - $this->assertEquals($responseArr['Name'], 'Updated Comment'); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testPOSTWithFormEncoded() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - - $url = "/api/v1/RestfulServerTest_Comment"; - $body = 'Name=New Comment&Comment=created'; - $headers = array( - 'Content-Type' => 'application/x-www-form-urlencoded' - ); - $response = Director::test($url, null, null, 'POST', $body, $headers); - $this->assertEquals($response->getStatusCode(), 201); // Created - // Assumption: XML is default output - $responseArr = Convert::xml2array($response->getBody()); - $this->assertTrue($responseArr['ID'] > 0); - $this->assertNotEquals($responseArr['ID'], $comment1->ID); - $this->assertEquals($responseArr['Comment'], 'created'); - $this->assertEquals($responseArr['Name'], 'New Comment'); - $this->assertEquals( - $response->getHeader('Location'), - Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID']) - ); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testPUTwithJSON() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - - // by mimetype - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $body = '{"Comment":"updated"}'; - $response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'application/json')); - $this->assertEquals($response->getStatusCode(), 200); // Updated - $obj = Convert::json2obj($response->getBody()); - $this->assertEquals($obj->ID, $comment1->ID); - $this->assertEquals($obj->Comment, 'updated'); - - // by extension - $url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID); - $body = '{"Comment":"updated"}'; - $response = Director::test($url, null, null, 'PUT', $body); - $this->assertEquals($response->getStatusCode(), 200); // Updated - $this->assertEquals( - $response->getHeader('Location'), - Controller::join_links(Director::absoluteBaseURL(), $url) - ); - $obj = Convert::json2obj($response->getBody()); - $this->assertEquals($obj->ID, $comment1->ID); - $this->assertEquals($obj->Comment, 'updated'); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testPUTwithXML() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - - // by mimetype - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $body = 'updated'; - $response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml')); - $this->assertEquals($response->getStatusCode(), 200); // Updated - $obj = Convert::xml2array($response->getBody()); - $this->assertEquals($obj['ID'], $comment1->ID); - $this->assertEquals($obj['Comment'], 'updated'); - - // by extension - $url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID); - $body = 'updated'; - $response = Director::test($url, null, null, 'PUT', $body); - $this->assertEquals($response->getStatusCode(), 200); // Updated - $this->assertEquals( - $response->getHeader('Location'), - Controller::join_links(Director::absoluteBaseURL(), $url) - ); - $obj = Convert::xml2array($response->getBody()); - $this->assertEquals($obj['ID'], $comment1->ID); - $this->assertEquals($obj['Comment'], 'updated'); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testHTTPAcceptAndContentType() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - - $headers = array('Accept' => 'application/json'); - $response = Director::test($url, null, null, 'GET', null, $headers); - $this->assertEquals($response->getStatusCode(), 200); // Success - $obj = Convert::json2obj($response->getBody()); - $this->assertEquals($obj->ID, $comment1->ID); - $this->assertEquals($response->getHeader('Content-Type'), 'application/json'); - } - - public function testNotFound(){ - $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'user'; - - $url = "/api/v1/RestfulServerTest_Comment/99"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 404); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testMethodNotAllowed() { - $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); - - $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; - $response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD'); - $this->assertEquals($response->getStatusCode(), 405); - } - - public function testConflictOnExistingResourceWhenUsingPost() { - $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; - $response = Director::test($url, null, null, 'POST'); - $this->assertEquals($response->getStatusCode(), 409); - } - - public function testUnsupportedMediaType() { - $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'user'; - - $url = "/api/v1/RestfulServerTest_Comment"; - $data = "Comment||\/||updated"; // weird format - $headers = array('Content-Type' => 'text/weirdformat'); - $response = Director::test($url, null, null, 'POST', $data, $headers); - $this->assertEquals($response->getStatusCode(), 415); - - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - - public function testXMLValueFormatting() { - $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1'); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertContains('' . $rating1->ID . '', $response->getBody()); - $this->assertContains('' . $rating1->Rating . '', $response->getBody()); - } - - public function testApiAccessFieldRestrictions() { - $author1 = $this->objFromFixture('RestfulServerTest_Author','author1'); - $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1'); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertContains('', $response->getBody()); - $this->assertContains('', $response->getBody()); - $this->assertContains('getBody()); - $this->assertNotContains('', $response->getBody()); - $this->assertNotContains('', $response->getBody()); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation'; - $response = Director::test($url, null, null, 'GET'); - $this->assertNotContains('', $response->getBody(), - '"add_fields" URL parameter filters out disallowed fields from $api_access' - ); - $this->assertNotContains('', $response->getBody(), - '"add_fields" URL parameter filters out disallowed relations from $api_access' - ); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?fields=SecretField,SecretRelation'; - $response = Director::test($url, null, null, 'GET'); - $this->assertNotContains('', $response->getBody(), - '"fields" URL parameter filters out disallowed fields from $api_access' - ); - $this->assertNotContains('', $response->getBody(), - '"fields" URL parameter filters out disallowed relations from $api_access' - ); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings'; - $response = Director::test($url, null, null, 'GET'); - $this->assertContains('', $response->getBody(), - 'Relation viewer shows fields allowed through $api_access' - ); - $this->assertNotContains('', $response->getBody(), - 'Relation viewer on has-many filters out disallowed fields from $api_access' - ); - } - - public function testApiAccessRelationRestrictionsInline() { - $author1 = $this->objFromFixture('RestfulServerTest_Author','author1'); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID; - $response = Director::test($url, null, null, 'GET'); - $this->assertNotContains('getBody(), 'Restricts many-many with api_access=false'); - $this->assertNotContains('getBody(), 'Restricts has-many with api_access=false'); - } - - public function testApiAccessRelationRestrictionsOnEndpoint() { - $author1 = $this->objFromFixture('RestfulServerTest_Author','author1'); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false'); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false'); - - $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false'); - } - - public function testApiAccessWithPUT() { - $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1'); - - $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; - $data = array( - 'Rating' => '42', - 'WriteProtectedField' => 'haxx0red' - ); - $response = Director::test($url, $data, null, 'PUT'); - // Assumption: XML is default output - $responseArr = Convert::xml2array($response->getBody()); - $this->assertEquals($responseArr['Rating'], 42); - $this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red'); - } + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + $response = Director::test($url, $data, null, 'PUT'); + $this->assertEquals($response->getStatusCode(), 200); // Success - public function testJSONDataFormatter() { - $formatter = new JSONDataFormatter(); - $editor = $this->objFromFixture('Member', 'editor'); - $user = $this->objFromFixture('Member', 'user'); + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testGETRelationshipsXML() + { + $author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1'); + $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); + $rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2'); + + // @todo should be set up by fixtures, doesn't work for some reason... + $author1->Ratings()->add($rating1); + $author1->Ratings()->add($rating2); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + + $responseArr = Convert::xml2array($response->getBody()); + $ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating']; + $this->assertEquals(count($ratingsArr), 2); + $ratingIDs = array( + (int)$ratingsArr[0]['@attributes']['id'], + (int)$ratingsArr[1]['@attributes']['id'] + ); + $this->assertContains($rating1->ID, $ratingIDs); + $this->assertContains($rating2->ID, $ratingIDs); + } + + public function testGETManyManyRelationshipsXML() + { + // author4 has related authors author2 and author3 + $author2 = $this->objFromFixture('RestfulServerTest_Author', 'author2'); + $author3 = $this->objFromFixture('RestfulServerTest_Author', 'author3'); + $author4 = $this->objFromFixture('RestfulServerTest_Author', 'author4'); + + $url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors'; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals(200, $response->getStatusCode()); + $arr = Convert::xml2array($response->getBody()); + $authorsArr = $arr['RestfulServerTest_Author']; + + $this->assertEquals(count($authorsArr), 2); + $ratingIDs = array( + (int)$authorsArr[0]['ID'], + (int)$authorsArr[1]['ID'] + ); + $this->assertContains($author2->ID, $ratingIDs); + $this->assertContains($author3->ID, $ratingIDs); + } - $this->assertEquals( - $formatter->convertDataObject($editor, array("FirstName", "Email")), - '{"FirstName":"Editor","Email":"editor@test.com"}', - "Correct JSON formatting with field subset"); + public function testPUTWithFormEncoded() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $body = 'Name=Updated Comment&Comment=updated'; + $headers = array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ); + $response = Director::test($url, null, null, 'PUT', $body, $headers); + $this->assertEquals($response->getStatusCode(), 200); // Success + // Assumption: XML is default output + $responseArr = Convert::xml2array($response->getBody()); + $this->assertEquals($responseArr['ID'], $comment1->ID); + $this->assertEquals($responseArr['Comment'], 'updated'); + $this->assertEquals($responseArr['Name'], 'Updated Comment'); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testPOSTWithFormEncoded() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + + $url = "/api/v1/RestfulServerTest_Comment"; + $body = 'Name=New Comment&Comment=created'; + $headers = array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ); + $response = Director::test($url, null, null, 'POST', $body, $headers); + $this->assertEquals($response->getStatusCode(), 201); // Created + // Assumption: XML is default output + $responseArr = Convert::xml2array($response->getBody()); + $this->assertTrue($responseArr['ID'] > 0); + $this->assertNotEquals($responseArr['ID'], $comment1->ID); + $this->assertEquals($responseArr['Comment'], 'created'); + $this->assertEquals($responseArr['Name'], 'New Comment'); + $this->assertEquals( + $response->getHeader('Location'), + Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID']) + ); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testPUTwithJSON() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + + // by mimetype + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $body = '{"Comment":"updated"}'; + $response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'application/json')); + $this->assertEquals($response->getStatusCode(), 200); // Updated + $obj = Convert::json2obj($response->getBody()); + $this->assertEquals($obj->ID, $comment1->ID); + $this->assertEquals($obj->Comment, 'updated'); + + // by extension + $url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID); + $body = '{"Comment":"updated"}'; + $response = Director::test($url, null, null, 'PUT', $body); + $this->assertEquals($response->getStatusCode(), 200); // Updated + $this->assertEquals( + $response->getHeader('Location'), + Controller::join_links(Director::absoluteBaseURL(), $url) + ); + $obj = Convert::json2obj($response->getBody()); + $this->assertEquals($obj->ID, $comment1->ID); + $this->assertEquals($obj->Comment, 'updated'); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testPUTwithXML() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + + // by mimetype + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $body = 'updated'; + $response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml')); + $this->assertEquals($response->getStatusCode(), 200); // Updated + $obj = Convert::xml2array($response->getBody()); + $this->assertEquals($obj['ID'], $comment1->ID); + $this->assertEquals($obj['Comment'], 'updated'); + + // by extension + $url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID); + $body = 'updated'; + $response = Director::test($url, null, null, 'PUT', $body); + $this->assertEquals($response->getStatusCode(), 200); // Updated + $this->assertEquals( + $response->getHeader('Location'), + Controller::join_links(Director::absoluteBaseURL(), $url) + ); + $obj = Convert::xml2array($response->getBody()); + $this->assertEquals($obj['ID'], $comment1->ID); + $this->assertEquals($obj['Comment'], 'updated'); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testHTTPAcceptAndContentType() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + + $headers = array('Accept' => 'application/json'); + $response = Director::test($url, null, null, 'GET', null, $headers); + $this->assertEquals($response->getStatusCode(), 200); // Success + $obj = Convert::json2obj($response->getBody()); + $this->assertEquals($obj->ID, $comment1->ID); + $this->assertEquals($response->getHeader('Content-Type'), 'application/json'); + } + + public function testNotFound() + { + $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'user'; + + $url = "/api/v1/RestfulServerTest_Comment/99"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 404); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testMethodNotAllowed() + { + $comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1'); + + $url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID; + $response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD'); + $this->assertEquals($response->getStatusCode(), 405); + } + + public function testConflictOnExistingResourceWhenUsingPost() + { + $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; + $response = Director::test($url, null, null, 'POST'); + $this->assertEquals($response->getStatusCode(), 409); + } + + public function testUnsupportedMediaType() + { + $_SERVER['PHP_AUTH_USER'] = 'user@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'user'; + + $url = "/api/v1/RestfulServerTest_Comment"; + $data = "Comment||\/||updated"; // weird format + $headers = array('Content-Type' => 'text/weirdformat'); + $response = Director::test($url, null, null, 'POST', $data, $headers); + $this->assertEquals($response->getStatusCode(), 415); + + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } + + public function testXMLValueFormatting() + { + $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertContains('' . $rating1->ID . '', $response->getBody()); + $this->assertContains('' . $rating1->Rating . '', $response->getBody()); + } + + public function testApiAccessFieldRestrictions() + { + $author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1'); + $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertContains('', $response->getBody()); + $this->assertContains('', $response->getBody()); + $this->assertContains('getBody()); + $this->assertNotContains('', $response->getBody()); + $this->assertNotContains('', $response->getBody()); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation'; + $response = Director::test($url, null, null, 'GET'); + $this->assertNotContains('', $response->getBody(), + '"add_fields" URL parameter filters out disallowed fields from $api_access' + ); + $this->assertNotContains('', $response->getBody(), + '"add_fields" URL parameter filters out disallowed relations from $api_access' + ); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?fields=SecretField,SecretRelation'; + $response = Director::test($url, null, null, 'GET'); + $this->assertNotContains('', $response->getBody(), + '"fields" URL parameter filters out disallowed fields from $api_access' + ); + $this->assertNotContains('', $response->getBody(), + '"fields" URL parameter filters out disallowed relations from $api_access' + ); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings'; + $response = Director::test($url, null, null, 'GET'); + $this->assertContains('', $response->getBody(), + 'Relation viewer shows fields allowed through $api_access' + ); + $this->assertNotContains('', $response->getBody(), + 'Relation viewer on has-many filters out disallowed fields from $api_access' + ); + } + + public function testApiAccessRelationRestrictionsInline() + { + $author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1'); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID; + $response = Director::test($url, null, null, 'GET'); + $this->assertNotContains('getBody(), 'Restricts many-many with api_access=false'); + $this->assertNotContains('getBody(), 'Restricts has-many with api_access=false'); + } + + public function testApiAccessRelationRestrictionsOnEndpoint() + { + $author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1'); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false'); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false'); + + $url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false'); + } + + public function testApiAccessWithPUT() + { + $rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1'); + + $url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID; + $data = array( + 'Rating' => '42', + 'WriteProtectedField' => 'haxx0red' + ); + $response = Director::test($url, $data, null, 'PUT'); + // Assumption: XML is default output + $responseArr = Convert::xml2array($response->getBody()); + $this->assertEquals($responseArr['Rating'], 42); + $this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red'); + } - $set = DataObject::get( - "Member", - sprintf('"Member"."ID" IN (%s)', implode(',', array($editor->ID, $user->ID))), - '"Email" ASC' // for sorting for postgres - ); - $this->assertEquals( - $formatter->convertDataObjectSet($set, array("FirstName", "Email")), - '{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' . - '{"FirstName":"User","Email":"user@test.com"}]}', - "Correct JSON formatting on a dataobjectset with field filter"); - } - - public function testApiAccessWithPOST() { - $url = "/api/v1/RestfulServerTest_AuthorRating"; - $data = array( - 'Rating' => '42', - 'WriteProtectedField' => 'haxx0red' - ); - $response = Director::test($url, $data, null, 'POST'); - // Assumption: XML is default output - $responseArr = Convert::xml2array($response->getBody()); - $this->assertEquals($responseArr['Rating'], 42); - $this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red'); - } + public function testJSONDataFormatter() + { + $formatter = new JSONDataFormatter(); + $editor = $this->objFromFixture('Member', 'editor'); + $user = $this->objFromFixture('Member', 'user'); - public function testCanViewRespectedInList() { - // Default content type - $url = "/api/v1/RestfulServerTest_SecretThing/"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - $this->assertNotContains('Unspeakable', $response->getBody()); + $this->assertEquals( + $formatter->convertDataObject($editor, array("FirstName", "Email")), + '{"FirstName":"Editor","Email":"editor@test.com"}', + "Correct JSON formatting with field subset"); - // JSON content type - $url = "/api/v1/RestfulServerTest_SecretThing.json"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - $this->assertNotContains('Unspeakable', $response->getBody()); + $set = DataObject::get( + "Member", + sprintf('"Member"."ID" IN (%s)', implode(',', array($editor->ID, $user->ID))), + '"Email" ASC' // for sorting for postgres + ); + $this->assertEquals( + $formatter->convertDataObjectSet($set, array("FirstName", "Email")), + '{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' . + '{"FirstName":"User","Email":"user@test.com"}]}', + "Correct JSON formatting on a dataobjectset with field filter"); + } + + public function testApiAccessWithPOST() + { + $url = "/api/v1/RestfulServerTest_AuthorRating"; + $data = array( + 'Rating' => '42', + 'WriteProtectedField' => 'haxx0red' + ); + $response = Director::test($url, $data, null, 'POST'); + // Assumption: XML is default output + $responseArr = Convert::xml2array($response->getBody()); + $this->assertEquals($responseArr['Rating'], 42); + $this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red'); + } - // With authentication - $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; - $_SERVER['PHP_AUTH_PW'] = 'editor'; - $url = "/api/v1/RestfulServerTest_SecretThing/"; - $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); - $this->assertContains('Unspeakable', $response->getBody()); - unset($_SERVER['PHP_AUTH_USER']); - unset($_SERVER['PHP_AUTH_PW']); - } - + public function testCanViewRespectedInList() + { + // Default content type + $url = "/api/v1/RestfulServerTest_SecretThing/"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotContains('Unspeakable', $response->getBody()); + + // JSON content type + $url = "/api/v1/RestfulServerTest_SecretThing.json"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertNotContains('Unspeakable', $response->getBody()); + + // With authentication + $_SERVER['PHP_AUTH_USER'] = 'editor@test.com'; + $_SERVER['PHP_AUTH_PW'] = 'editor'; + $url = "/api/v1/RestfulServerTest_SecretThing/"; + $response = Director::test($url, null, null, 'GET'); + $this->assertEquals($response->getStatusCode(), 200); + $this->assertContains('Unspeakable', $response->getBody()); + unset($_SERVER['PHP_AUTH_USER']); + unset($_SERVER['PHP_AUTH_PW']); + } } /** @@ -459,143 +481,153 @@ class RestfulServerTest extends SapphireTest { * but only "editors" can edit or delete them. * */ -class RestfulServerTest_Comment extends DataObject implements PermissionProvider,TestOnly { - - static $api_access = true; - - static $db = array( - "Name" => "Varchar(255)", - "Comment" => "Text" - ); - - static $has_one = array( - 'Page' => 'RestfulServerTest_Page', - 'Author' => 'RestfulServerTest_Author', - ); - - public function providePermissions(){ - return array( - 'EDIT_Comment' => 'Edit Comment Objects', - 'CREATE_Comment' => 'Create Comment Objects', - 'DELETE_Comment' => 'Delete Comment Objects', - ); - } - - public function canView($member = null) { - return true; - } - - public function canEdit($member = null) { - return Permission::checkMember($member, 'EDIT_Comment'); - } - - public function canDelete($member = null) { - return Permission::checkMember($member, 'DELETE_Comment'); - } - - public function canCreate($member = null) { - return Permission::checkMember($member, 'CREATE_Comment'); - } - +class RestfulServerTest_Comment extends DataObject implements PermissionProvider,TestOnly +{ + public static $api_access = true; + + public static $db = array( + "Name" => "Varchar(255)", + "Comment" => "Text" + ); + + public static $has_one = array( + 'Page' => 'RestfulServerTest_Page', + 'Author' => 'RestfulServerTest_Author', + ); + + public function providePermissions() + { + return array( + 'EDIT_Comment' => 'Edit Comment Objects', + 'CREATE_Comment' => 'Create Comment Objects', + 'DELETE_Comment' => 'Delete Comment Objects', + ); + } + + public function canView($member = null) + { + return true; + } + + public function canEdit($member = null) + { + return Permission::checkMember($member, 'EDIT_Comment'); + } + + public function canDelete($member = null) + { + return Permission::checkMember($member, 'DELETE_Comment'); + } + + public function canCreate($member = null) + { + return Permission::checkMember($member, 'CREATE_Comment'); + } } -class RestfulServerTest_SecretThing extends DataObject implements TestOnly,PermissionProvider{ - static $api_access = true; - - static $db = array( - "Name" => "Varchar(255)", - ); - - public function canView($member = null) { - return Permission::checkMember($member, 'VIEW_SecretThing'); - } - - public function providePermissions(){ - return array( - 'VIEW_SecretThing' => 'View Secret Things', - ); - } +class RestfulServerTest_SecretThing extends DataObject implements TestOnly,PermissionProvider +{ + public static $api_access = true; + + public static $db = array( + "Name" => "Varchar(255)", + ); + + public function canView($member = null) + { + return Permission::checkMember($member, 'VIEW_SecretThing'); + } + + public function providePermissions() + { + return array( + 'VIEW_SecretThing' => 'View Secret Things', + ); + } } -class RestfulServerTest_Page extends DataObject implements TestOnly { - - static $api_access = false; - - static $db = array( - 'Title' => 'Text', - 'Content' => 'HTMLText', - ); - - static $has_one = array( - 'Author' => 'RestfulServerTest_Author', - ); - - static $has_many = array( - 'TestComments' => 'RestfulServerTest_Comment' - ); - - static $belongs_many_many = array( - 'RelatedAuthors' => 'RestfulServerTest_Author', - ); - +class RestfulServerTest_Page extends DataObject implements TestOnly +{ + public static $api_access = false; + + public static $db = array( + 'Title' => 'Text', + 'Content' => 'HTMLText', + ); + + public static $has_one = array( + 'Author' => 'RestfulServerTest_Author', + ); + + public static $has_many = array( + 'TestComments' => 'RestfulServerTest_Comment' + ); + + public static $belongs_many_many = array( + 'RelatedAuthors' => 'RestfulServerTest_Author', + ); } -class RestfulServerTest_Author extends DataObject implements TestOnly { - - static $api_access = true; - - static $db = array( - 'Name' => 'Text', - ); - - static $many_many = array( - 'RelatedPages' => 'RestfulServerTest_Page', - 'RelatedAuthors' => 'RestfulServerTest_Author', - ); - - static $has_many = array( - 'PublishedPages' => 'RestfulServerTest_Page', - 'Ratings' => 'RestfulServerTest_AuthorRating', - ); - - public function canView($member = null) { - return true; - } +class RestfulServerTest_Author extends DataObject implements TestOnly +{ + public static $api_access = true; + + public static $db = array( + 'Name' => 'Text', + ); + + public static $many_many = array( + 'RelatedPages' => 'RestfulServerTest_Page', + 'RelatedAuthors' => 'RestfulServerTest_Author', + ); + + public static $has_many = array( + 'PublishedPages' => 'RestfulServerTest_Page', + 'Ratings' => 'RestfulServerTest_AuthorRating', + ); + + public function canView($member = null) + { + return true; + } } -class RestfulServerTest_AuthorRating extends DataObject implements TestOnly { - static $api_access = array( - 'view' => array( - 'Rating', - 'WriteProtectedField', - 'Author' - ), - 'edit' => array( - 'Rating' - ) - ); - - static $db = array( - 'Rating' => 'Int', - 'SecretField' => 'Text', - 'WriteProtectedField' => 'Text', - ); - - static $has_one = array( - 'Author' => 'RestfulServerTest_Author', - 'SecretRelation' => 'RestfulServerTest_Author', - ); - - public function canView($member = null) { - return true; - } - - public function canEdit($member = null) { - return true; - } - - public function canCreate($member = null) { - return true; - } +class RestfulServerTest_AuthorRating extends DataObject implements TestOnly +{ + public static $api_access = array( + 'view' => array( + 'Rating', + 'WriteProtectedField', + 'Author' + ), + 'edit' => array( + 'Rating' + ) + ); + + public static $db = array( + 'Rating' => 'Int', + 'SecretField' => 'Text', + 'WriteProtectedField' => 'Text', + ); + + public static $has_one = array( + 'Author' => 'RestfulServerTest_Author', + 'SecretRelation' => 'RestfulServerTest_Author', + ); + + public function canView($member = null) + { + return true; + } + + public function canEdit($member = null) + { + return true; + } + + public function canCreate($member = null) + { + return true; + } } -