diff --git a/api/RestfulServer.php b/api/RestfulServer.php new file mode 100644 index 000000000..593dffd2f --- /dev/null +++ b/api/RestfulServer.php @@ -0,0 +1,178 @@ +urlParams['ClassName']; + $id = $this->urlParams['ID']; + + switch($requestMethod) { + case 'GET': + return $this->getHandler($className, $id); + + case 'PUT': + return $this->putHandler($className, $id); + + case 'DELETE': + return $this->deleteHandler($className, $id); + + case 'POST': + } + } + + /** + * 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 + */ + protected function getHandler($className, $id) { + $obj = DataObject::get_by_id($className, $id); + if(!$obj) { + return $this->notFound(); + } + + if($obj->stat('api_access') && $obj->canView()) { + + $xml = "\n<$className>\n"; + foreach($obj->db() as $fieldName => $fieldType) { + $xml .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "\n"; + } + + foreach($obj->has_one() as $relName => $relObj) { + $fieldName = $relName . 'ID'; + if($obj->$fieldName) { + $href = Director::absoluteURL(self::$api_base . "$relObj/" . $obj->$fieldName); + } else { + $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); + } + $xml .= "<$relName linktype=\"has_one\" href=\"$href\" id=\"{$obj->$fieldName}\" />\n"; + } + + foreach($obj->has_many() as $relName => $relObj) { + $xml .= "<$relName linktype=\"has_many\">\n"; + $items = $obj->$relName(); + foreach($items as $item) { + //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); + $href = Director::absoluteURL(self::$api_base . "$relObj/$item->ID"); + $xml .= "<$relObj href=\"$href\" id=\"{$item->ID}\" />\n"; + } + $xml .= "\n"; + } + + foreach($obj->many_many() as $relName => $relObj) { + $xml .= "<$relName linktype=\"many_many\">\n"; + $items = $obj->$relName(); + foreach($items as $item) { + //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); + $href = Director::absoluteURL(self::$api_base . "$relObj/$item->ID"); + $xml .= "<$relObj href=\"$href\" id=\"{$item->ID}\" />\n"; + } + $xml .= "\n"; + } + + $xml .= ""; + + $this->getResponse()->addHeader("Content-type", "text/xml"); + + return $xml; + } else { + return $this->permissionFailure(); + } + } + + /** + * Handler for object delete + */ + protected function deleteHandler($className, $id) { + if($id) { + $obj = DataObject::get_by_id($className, $id); + if($obj->stat('api_access') && $obj->canDelete()) { + $obj->delete(); + } else { + return $this->permissionFailure(); + } + + } + } + + /** + * Handler for object write + */ + protected function putHandler($className, $id) { + return $this->permissionFailure(); + } + + /** + * Handler for object append / method call + */ + protected function postHandler($className, $id) { + return $this->permissionFailure(); + } + + + protected function permissionFailure() { + // return a 401 + $this->getResponse()->setStatusCode(403); + return "You don't have access to this item through the API."; + } + + protected function notFound() { + // return a 404 + $this->getResponse()->setStatusCode(404); + return "That object wasn't found"; + } + +} \ No newline at end of file diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 1d85a8a2d..cdc1d0168 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -59,6 +59,13 @@ class DataObject extends ViewableData implements DataObjectInterface { * @var string */ static $plural_name = null; + + + /** + * Allow API access to this object? + * @todo Define the options that can be set here + */ + static $api_access = false; /** diff --git a/main.php b/main.php index a7eeaafc8..0a6713e15 100644 --- a/main.php +++ b/main.php @@ -128,6 +128,7 @@ Director::addRules(10, array( 'images/$Action/$Class/$ID/$Field' => 'Image_Uploader', '' => 'RootURLController', 'sitemap.xml' => 'GoogleSitemap', + 'api/v1/$ClassName/$ID' => 'RestfulServer', )); Director::addRules(1, array( diff --git a/tests/api/RestfulServerTest.php b/tests/api/RestfulServerTest.php new file mode 100644 index 000000000..9da1f4bff --- /dev/null +++ b/tests/api/RestfulServerTest.php @@ -0,0 +1,42 @@ +idFromFixture('Page', 'page1'); + + $page1 = Director::test("api/v1/Page/$pageID", 'GET'); + $page1xml = new RestfulService_Response($page1->getBody(), $page1->getStatusCode()); + + // Test fields + $this->assertEquals(200, $page1xml->getStatusCode()); + $this->assertEquals('First Page', (string)$page1xml->xpath_one('/Page/Title')); + + // Test has_many relationships + $comments = $page1xml->xpath('/Page/Comments/PageComment'); + $this->assertEquals(Director::absoluteURL('api/v1/PageComment/3'), (string)$comments[0]['href']); + $this->assertEquals(Director::absoluteURL('api/v1/PageComment/4'), (string)$comments[1]['href']); + $this->assertEquals(3, (string)$comments[0]['id']); + $this->assertEquals(4, (string)$comments[1]['id']); + + /// Test has_one relationships + $parent = $page1xml->xpath_one('/Page/Parent'); + $this->assertEquals(Director::absoluteURL('api/v1/SiteTree/1'), (string)$parent['href']); + $this->assertEquals(1, (string)$parent['id']); + + /* + $deletion = $service->get('Page/1', 'DELETE'); + if($deletion->successfulStatus()) { + echo 'deleted'; + } else { + switch($deletion->statusCode()) { + case 403: echo "You don't have permission to delete that object"; break; + default: echo "There was an error deleting"; break; + } + } + */ + + } +} \ No newline at end of file diff --git a/tests/api/RestfulServerTest.yml b/tests/api/RestfulServerTest.yml new file mode 100644 index 000000000..8c5c14d4a --- /dev/null +++ b/tests/api/RestfulServerTest.yml @@ -0,0 +1,25 @@ +PageComment: + comment1: + Name: Joe + Comment: This is a test comment + comment2: + Name: Jane + Comment: This is another test comment + comment3: + Name: Bob + Comment: Another comment + comment4: + Name: Bob + Comment: Second comment by Bob + +Page: + home: + Title: Home + Comments: =>PageComment.comment1,=>PageComment.comment2 + page1: + Title: First Page + Parent: =>Page.home + Content:

Some test content

+ Comments: =>PageComment.comment3,=>PageComment.comment4 + page2: + Title: Second Page \ No newline at end of file