Webylon 3.2 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • 1c
    • exchange
      • catalog
  • auth
  • Booking
  • building
    • company
  • cart
    • shipping
    • steppedcheckout
  • Catalog
    • monument
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • RestfulServer
  • RestfulServer_Item
  • RestfulServer_List
  • SOAPModelAccess
  1 <?php
  2 /**
  3  * Sapphire's generic RESTful server.
  4  * 
  5  * This class gives your application a RESTful API for free.  All you have to do is define static $api_access = true on
  6  * the appropriate DataObjects.  You will need to ensure that all of your data manipulation and security is defined in
  7  * your model layer (ie, the DataObject classes) and not in your Controllers.  This is the recommended design for Sapphire
  8  * applications.
  9  * 
 10  * Enabling restful access on a model will also enable a SOAP API, see {@link SOAPModelAccess}.
 11  * 
 12  * Example DataObject with simple api access, giving full access to all object properties and relations,
 13  * unless explicitly controlled through model permissions.
 14  * <code>
 15  * class Article extends DataObject {
 16  *  static $db = array('Title'=>'Text','Published'=>'Boolean');
 17  *  static $api_access = true;
 18  * }
 19  * </code>
 20  *
 21  * * Example DataObject with advanced api access, limiting viewing and editing to Title attribute only:
 22  * <code>
 23  * class Article extends DataObject {
 24  *  static $db = array('Title'=>'Text','Published'=>'Boolean');
 25  *  static $api_access = array(
 26  *      'view' => array('Title'),
 27  *      'edit' => array('Title'),
 28  *  );
 29  * }
 30  * </code>
 31  * 
 32  * Supported operations:
 33  *  - GET /api/v1/(ClassName)/(ID) - gets a database record
 34  *  - GET /api/v1/(ClassName)/(ID)/(Relation) - get all of the records linked to this database record by the given reatlion
 35  *  - GET /api/v1/(ClassName)?(Field)=(Val)&(Field)=(Val) - searches for matching database records
 36  *  - POST /api/v1/(ClassName) - create a new database record
 37  *  - PUT /api/v1/(ClassName)/(ID) - updates a database record
 38  *  - PUT /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, replacing the existing record(s) (NOT IMPLEMENTED YET)
 39  *  - POST /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, appending to the existing record(s) (NOT IMPLEMENTED YET)
 40  * 
 41  *  - DELETE /api/v1/(ClassName)/(ID) - deletes a database record (NOT IMPLEMENTED YET)
 42  *  - DELETE /api/v1/(ClassName)/(ID)/(Relation)/(ForeignID) - remove the relationship between two database records, but don't actually delete the foreign object (NOT IMPLEMENTED YET)
 43  *
 44  *  - POST /api/v1/(ClassName)/(ID)/(MethodName) - executes a method on the given object (e.g, publish)
 45  * 
 46  * You can trigger searches based on the fields specified on {@link DataObject::searchable_fields} and passed
 47  * through {@link DataObject::getDefaultSearchContext()}. Just add a key-value pair with the search-term
 48  * to the url, e.g. /api/v1/(ClassName)/?Title=mytitle.
 49  * 
 50  * Other url-modifiers:
 51  * - &limit=<numeric>: Limit the result set
 52  * - &relationdepth=<numeric>: Displays links to existing has-one and has-many relationships to a certain depth (Default: 1)
 53  * - &fields=<string>: Comma-separated list of fields on the output object (defaults to all database-columns).
 54  *   Handy to limit output for bandwidth and performance reasons.
 55  * - &sort=<myfield>&dir=<asc|desc>
 56  * - &add_fields=<string>: Comma-separated list of additional fields, for example dynamic getters.
 57  *
 58  * Access control is implemented through the usual Member system with Basicauth authentication only.
 59  * By default, you have to bear the ADMIN permission to retrieve or send any data.
 60  *
 61  * You should override the following built-in methods to customize permission control on a
 62  * class- and object-level:
 63  * - {@link DataObject::canView()}
 64  * - {@link DataObject::canEdit()}
 65  * - {@link DataObject::canDelete()}
 66  * - {@link DataObject::canCreate()}
 67  * See {@link DataObject} documentation for further details.
 68  * 
 69  * You can specify the character-encoding for any input on the HTTP Content-Type.
 70  * At the moment, only UTF-8 is supported. All output is made in UTF-8 regardless of Accept headers.
 71  * 
 72  * @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
 73  * @todo Implement PUT/POST/DELETE for relations
 74  * @todo Access-Control for relations (you might be allowed to view Members and Groups, but not their relation with each other)
 75  * @todo Make SearchContext specification customizeable for each class
 76  * @todo Allow for range-searches (e.g. on Created column)
 77  * @todo Allow other authentication methods (currently only HTTP BasicAuth)
 78  * @todo Filter relation listings by $api_access and canView() permissions
 79  * @todo Exclude relations when "fields" are specified through URL (they should be explicitly requested in this case)
 80  * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in SiteTree/Versioned/Hierarchy
 81  * @todo URL parameter namespacing for search-fields, limit, fields, add_fields (might all be valid dataobject properties)
 82  *       e.g. you wouldn't be able to search for a "limit" property on your subclass as its overlayed with the search logic
 83  * @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
 84  * @todo Access to decoratable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22
 85  * @todo Respect $api_access array notation in search contexts
 86  * 
 87  * @package sapphire
 88  * @subpackage api
 89  */
 90 class RestfulServer extends Controller {
 91     static $url_handlers = array(
 92         '$ClassName/$ID/$Relation' => 'handleAction'
 93         #'$ClassName/#ID' => 'handleItem',
 94         #'$ClassName' => 'handleList',
 95     );
 96 
 97     protected static $api_base = "api/v1/";
 98 
 99     /**
100      * If no extension is given in the request, resolve to this extension
101      * (and subsequently the {@link self::$default_mimetype}.
102      *
103      * @var string
104      */
105     public static $default_extension = "xml";
106     
107     /**
108      * If no extension is given, resolve the request to this mimetype.
109      *
110      * @var string
111      */
112     protected static $default_mimetype = "text/xml";
113     
114     /**
115      * @uses authenticate()
116      * @var Member
117      */
118     protected $member;
119     
120     /*
121     function handleItem($request) {
122         return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
123     }
124 
125     function handleList($request) {
126         return new RestfulServer_List(DataObject::get($request->param("ClassName"),""));
127     }
128     */
129     
130     /**
131      * This handler acts as the switchboard for the controller.
132      * Since no $Action url-param is set, all requests are sent here.
133      */
134     function index() {
135         if(!isset($this->urlParams['ClassName'])) return $this->notFound();
136         $className = $this->urlParams['ClassName'];
137         $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
138         $relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
139         
140         // Check input formats
141         if(!class_exists($className)) return $this->notFound();
142         if($id && !is_numeric($id)) return $this->notFound();
143         if($relation && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)) return $this->notFound();
144         
145         // if api access is disabled, don't proceed
146         $apiAccess = singleton($className)->stat('api_access');
147         if(!$apiAccess) return $this->permissionFailure();
148 
149         // authenticate through HTTP BasicAuth
150         $this->member = $this->authenticate();
151 
152         // handle different HTTP verbs
153         if($this->request->isGET() || $this->request->isHEAD()) return $this->getHandler($className, $id, $relation);
154         if($this->request->isPOST()) return $this->postHandler($className, $id, $relation);
155         if($this->request->isPUT()) return $this->putHandler($className, $id, $relation);
156         if($this->request->isDELETE()) return $this->deleteHandler($className, $id, $relation);
157 
158         // if no HTTP verb matches, return error
159         return $this->methodNotAllowed();
160     }
161     
162     /**
163      * Handler for object read.
164      * 
165      * The data object will be returned in the following format:
166      *
167      * <ClassName>
168      *   <FieldName>Value</FieldName>
169      *   ...
170      *   <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" />
171      *   ...
172      *   <HasManyRelName>
173      *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
174      *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
175      *   </HasManyRelName>
176      *   ...
177      *   <ManyManyRelName>
178      *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
179      *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
180      *   </ManyManyRelName>
181      * </ClassName>
182      *
183      * Access is controlled by two variables:
184      * 
185      *   - static $api_access must be set. This enables the API on a class by class basis
186      *   - $obj->canView() must return true. This lets you implement record-level security
187      * 
188      * @todo Access checking
189      * 
190      * @param String $className
191      * @param Int $id
192      * @param String $relation
193      * @return String The serialized representation of the requested object(s) - usually XML or JSON.
194      */
195     protected function getHandler($className, $id, $relationName) {
196         $sort = array(
197             'sort' => $this->request->getVar('sort'),
198             'dir' => $this->request->getVar('dir')
199         );
200         $limit = array(
201             'start' => $this->request->getVar('start'),
202             'limit' => $this->request->getVar('limit')
203         );
204         
205         $params = $this->request->getVars();
206         
207         $responseFormatter = $this->getResponseDataFormatter();
208         if(!$responseFormatter) return $this->unsupportedMediaType();
209         
210         // $obj can be either a DataObject or a DataObjectSet,
211         // depending on the request
212         if($id) {
213             // Format: /api/v1/<MyClass>/<ID>
214             $query = $this->getObjectQuery($className, $id, $params);
215             $obj = singleton($className)->buildDataObjectSet($query->execute());
216             if(!$obj) return $this->notFound();
217             $obj = $obj->First();
218             if(!$obj->canView()) return $this->permissionFailure();
219 
220             // Format: /api/v1/<MyClass>/<ID>/<Relation>
221             if($relationName) {
222                 $query = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
223                 if($query === false) return $this->notFound();
224                 $obj = singleton($className)->buildDataObjectSet($query->execute());
225             } 
226             
227         } else {
228             // Format: /api/v1/<MyClass>
229             $query = $this->getObjectsQuery($className, $params, $sort, $limit);
230             $obj = singleton($className)->buildDataObjectSet($query->execute());
231 
232             // show empty serialized result when no records are present
233             if(!$obj) $obj = new DataObjectSet();
234         }
235         
236         $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
237         
238         $rawFields = $this->request->getVar('fields');
239         $fields = $rawFields ? explode(',', $rawFields) : null;
240 
241         if($obj instanceof DataObjectSet) {
242             $responseFormatter->setTotalSize($query->unlimitedRowCount());
243             return $responseFormatter->convertDataObjectSet($obj, $fields);
244         } else if(!$obj) {
245             $responseFormatter->setTotalSize(0);
246             return $responseFormatter->convertDataObjectSet(new DataObjectSet(), $fields);
247         } else {
248             return $responseFormatter->convertDataObject($obj, $fields);
249         }
250     }
251     
252     /**
253      * Uses the default {@link SearchContext} specified through
254      * {@link DataObject::getDefaultSearchContext()} to augument
255      * an existing query object (mostly a component query from {@link DataObject})
256      * with search clauses. 
257      * 
258      * @todo Allow specifying of different searchcontext getters on model-by-model basis
259      *
260      * @param string $className
261      * @param array $params
262      * @return DataObjectSet
263      */
264     protected function getSearchQuery($className, $params = null, $sort = null, $limit = null, $existingQuery = null) {
265         if(singleton($className)->hasMethod('getRestfulSearchContext')) {
266             $searchContext = singleton($className)->{'getRestfulSearchContext'}();
267         } else {
268             $searchContext = singleton($className)->getDefaultSearchContext();
269         }
270         $query = $searchContext->getQuery($params, $sort, $limit, $existingQuery);
271         
272         return $query;
273     }
274     
275     /**
276      * Returns a dataformatter instance based on the request
277      * extension or mimetype. Falls back to {@link self::$default_extension}.
278      * 
279      * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers 
280      * @return DataFormatter
281      */
282     protected function getDataFormatter($includeAcceptHeader = false) {
283         $extension = $this->request->getExtension();
284         $contentTypeWithEncoding = $this->request->getHeader('Content-Type');
285         preg_match('/([^;]*)/',$contentTypeWithEncoding, $contentTypeMatches);
286         $contentType = $contentTypeMatches[0];
287         $accept = $this->request->getHeader('Accept');
288         $mimetypes = $this->request->getAcceptMimetypes();
289 
290         // get formatter
291         if(!empty($extension)) {
292             $formatter = DataFormatter::for_extension($extension);
293         }elseif($includeAcceptHeader && !empty($accept) && $accept != '*/*') {
294             $formatter = DataFormatter::for_mimetypes($mimetypes);
295             if(!$formatter) $formatter = DataFormatter::for_extension(self::$default_extension);
296         } elseif(!empty($contentType)) {
297             $formatter = DataFormatter::for_mimetype($contentType);
298         } else {
299             $formatter = DataFormatter::for_extension(self::$default_extension);
300         }
301 
302         if(!$formatter) return false;
303         
304         // set custom fields
305         if($customAddFields = $this->request->getVar('add_fields')) $formatter->setCustomAddFields(explode(',',$customAddFields));
306         if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
307         $formatter->setCustomRelations($this->getAllowedRelations($this->urlParams['ClassName']));
308         
309         $apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
310         if(is_array($apiAccess)) {
311             $formatter->setCustomAddFields(array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']));
312             if($formatter->getCustomFields()) {
313                 $formatter->setCustomFields(array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view']));
314             } else {
315                 $formatter->setCustomFields((array)$apiAccess['view']);
316             }
317             if($formatter->getCustomRelations()) {
318                 $formatter->setCustomRelations(array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view']));
319             } else {
320                 $formatter->setCustomRelations((array)$apiAccess['view']);
321             }
322             
323         }
324 
325         // set relation depth
326         $relationDepth = $this->request->getVar('relationdepth');
327         if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth;
328         
329         return $formatter;      
330     }
331     
332     protected function getRequestDataFormatter() {
333         return $this->getDataFormatter(false);
334     }
335     
336     protected function getResponseDataFormatter() {
337         return $this->getDataFormatter(true);
338     }
339     
340     /**
341      * Handler for object delete
342      */
343     protected function deleteHandler($className, $id) {
344         $obj = DataObject::get_by_id($className, $id);
345         if(!$obj) return $this->notFound();
346         if(!$obj->canDelete()) return $this->permissionFailure();
347         
348         $obj->delete();
349         
350         $this->getResponse()->setStatusCode(204); // No Content
351         return true;
352     }
353 
354     /**
355      * Handler for object write
356      */
357     protected function putHandler($className, $id) {
358         $obj = DataObject::get_by_id($className, $id);
359         if(!$obj) return $this->notFound();
360         if(!$obj->canEdit()) return $this->permissionFailure();
361         
362         $reqFormatter = $this->getRequestDataFormatter();
363         if(!$reqFormatter) return $this->unsupportedMediaType();
364         
365         $responseFormatter = $this->getResponseDataFormatter();
366         if(!$responseFormatter) return $this->unsupportedMediaType();
367         
368         $obj = $this->updateDataObject($obj, $reqFormatter);
369         
370         $this->getResponse()->setStatusCode(200); // Success
371         $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
372         $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
373         $this->getResponse()->addHeader('Location', $objHref);
374         
375         return $responseFormatter->convertDataObject($obj);
376     }
377 
378     /**
379      * Handler for object append / method call.
380      * 
381      * @todo Posting to an existing URL (without a relation)
382      * current resolves in creatig a new element,
383      * rather than a "Conflict" message.
384      */
385     protected function postHandler($className, $id, $relation) {
386         if($id) {
387             if(!$relation) {
388                 $this->response->setStatusCode(409);
389                 return 'Conflict';
390             }
391             
392             $obj = DataObject::get_by_id($className, $id);
393             if(!$obj) return $this->notFound();
394             
395             if(!$obj->hasMethod($relation)) {
396                 return $this->notFound();
397             }
398             
399             if(!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) {
400                 return $this->permissionFailure();
401             }
402             
403             $obj->$relation();
404             
405             $this->getResponse()->setStatusCode(204); // No Content
406             return true;
407         } else {
408             if(!singleton($className)->canCreate()) return $this->permissionFailure();
409             $obj = new $className();
410         
411             $reqFormatter = $this->getRequestDataFormatter();
412             if(!$reqFormatter) return $this->unsupportedMediaType();
413         
414             $responseFormatter = $this->getResponseDataFormatter();
415         
416             $obj = $this->updateDataObject($obj, $reqFormatter);
417         
418             $this->getResponse()->setStatusCode(201); // Created
419             $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
420             $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
421             $this->getResponse()->addHeader('Location', $objHref);
422         
423             return $responseFormatter->convertDataObject($obj);
424         }
425     }
426     
427     /**
428      * Converts either the given HTTP Body into an array
429      * (based on the DataFormatter instance), or returns
430      * the POST variables.
431      * Automatically filters out certain critical fields
432      * that shouldn't be set by the client (e.g. ID).
433      *
434      * @param DataObject $obj
435      * @param DataFormatter $formatter
436      * @return DataObject The passed object
437      */
438     protected function updateDataObject($obj, $formatter) {
439         // if neither an http body nor POST data is present, return error
440         $body = $this->request->getBody();
441         if(!$body && !$this->request->postVars()) {
442             $this->getResponse()->setStatusCode(204); // No Content
443             return 'No Content';
444         }
445         
446         if(!empty($body)) {
447             $data = $formatter->convertStringToArray($body);
448         } else {
449             // assume application/x-www-form-urlencoded which is automatically parsed by PHP
450             $data = $this->request->postVars();
451         }
452         
453         // @todo Disallow editing of certain keys in database
454         $data = array_diff_key($data, array('ID','Created'));
455         
456         $apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
457         if(is_array($apiAccess) && isset($apiAccess['edit'])) {
458             $data = array_intersect_key($data, array_combine($apiAccess['edit'],$apiAccess['edit']));
459         }
460 
461         $obj->update($data);
462         $obj->write();
463         
464         return $obj;
465     }
466     
467     /**
468      * Gets a single DataObject by ID,
469      * through a request like /api/v1/<MyClass>/<MyID>
470      * 
471      * @param string $className
472      * @param int $id
473      * @param array $params
474      * @return SQLQuery
475      */
476     protected function getObjectQuery($className, $id, $params) {
477         $baseClass = ClassInfo::baseDataClass($className);
478         return singleton($className)->extendedSQL(
479             "\"$baseClass\".\"ID\" = {$id}"
480         );
481     }
482     
483     /**
484      * @param DataObject $obj
485      * @param array $params
486      * @param int|array $sort
487      * @param int|array $limit
488      * @return SQLQuery
489      */
490     protected function getObjectsQuery($className, $params, $sort, $limit) {
491         return $this->getSearchQuery($className, $params, $sort, $limit);
492     }
493     
494     
495     /**
496      * @param DataObject $obj
497      * @param array $params
498      * @param int|array $sort
499      * @param int|array $limit
500      * @param string $relationName
501      * @return SQLQuery|boolean
502      */
503     protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
504         if($obj->hasMethod("{$relationName}Query")) {
505             // @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading)
506             $query = $obj->{"{$relationName}Query"}(null, $sort, null, $limit);
507             $relationClass = $obj->{"{$relationName}Class"}();
508         } elseif($relationClass = $obj->many_many($relationName)) {
509             // many_many() returns different notation
510             $relationClass = $relationClass[1];
511             $query = $obj->getManyManyComponentsQuery($relationName);
512         } elseif($relationClass = $obj->has_many($relationName)) {
513             $query = $obj->getComponentsQuery($relationName);
514         } elseif($relationClass = $obj->has_one($relationName)) {
515             $query = null;
516         } else {
517             return false;
518         }
519 
520         // get all results
521         return $this->getSearchQuery($relationClass, $params, $sort, $limit, $query);
522     }
523     
524     protected function permissionFailure() {
525         // return a 401
526         $this->getResponse()->setStatusCode(401);
527         $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"');
528         return "You don't have access to this item through the API.";
529     }
530 
531     protected function notFound() {
532         // return a 404
533         $this->getResponse()->setStatusCode(404);
534         return "That object wasn't found";
535     }
536     
537     protected function methodNotAllowed() {
538         $this->getResponse()->setStatusCode(405);
539         return "Method Not Allowed";
540     }
541     
542     protected function unsupportedMediaType() {
543         $this->response->setStatusCode(415); // Unsupported Media Type
544         return "Unsupported Media Type";
545     }
546     
547     protected function authenticate() {
548         if(!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) return false;
549         
550         if($member = Member::currentMember()) return $member;
551         $member = MemberAuthenticator::authenticate(array(
552             'Email' => $_SERVER['PHP_AUTH_USER'], 
553             'Password' => $_SERVER['PHP_AUTH_PW'],
554         ), null);
555         
556         if($member) {
557             $member->LogIn(false);
558             return $member;
559         } else {
560             return false;
561         }
562     }
563     
564     /**
565      * Return only relations which have $api_access enabled.
566      * @todo Respect field level permissions once they are available in core
567      * 
568      * @param string $class
569      * @param Member $member
570      * @return array
571      */
572     protected function getAllowedRelations($class, $member = null) {
573         $allowedRelations = array();
574         $obj = singleton($class);
575         $relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many();
576         if($relations) foreach($relations as $relName => $relClass) {
577             if(singleton($relClass)->stat('api_access')) {
578                 $allowedRelations[] = $relName;
579             }
580         }
581         return $allowedRelations;
582     }
583     
584 }
585 
586 /**
587  * Restful server handler for a DataObjectSet
588  * 
589  * @package sapphire
590  * @subpackage api
591  */
592 class RestfulServer_List {
593     static $url_handlers = array(
594         '#ID' => 'handleItem',
595     );
596 
597     function __construct($list) {
598         $this->list = $list;
599     }
600     
601     function handleItem($request) {
602         return new RestulServer_Item($this->list->getById($request->param('ID')));
603     }
604 }
605 
606 /**
607  * Restful server handler for a single DataObject
608  * 
609  * @package sapphire
610  * @subpackage api
611  */
612 class RestfulServer_Item {
613     static $url_handlers = array(
614         '$Relation' => 'handleRelation',
615     );
616 
617     function __construct($item) {
618         $this->item = $item;
619     }
620     
621     function handleRelation($request) {
622         $funcName = $request('Relation');
623         $relation = $this->item->$funcName();
624 
625         if($relation instanceof DataObjectSet) return new RestfulServer_List($relation);
626         else return new RestfulServer_Item($relation);
627     }
628 }
629 
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -
Webylon 3.2 API Docs API documentation generated by ApiGen 2.8.0