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) . "$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 .= "$relName>\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 .= "$relName>\n";
+ }
+
+ $xml .= "$className>";
+
+ $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