Skip to content
This repository
Browse code

API CHANGE Moved RestfulServer into its own module at https://github.…

  • Loading branch information...
commit cb8b11812ce8eed409d0ef6abf6d6a7643c76a5a 1 parent a757c16
Ingo Schommer authored June 04, 2012
2  api/FormEncodedDataFormatter.php
@@ -2,8 +2,6 @@
2 2
 /**
3 3
  * Accepts form encoded strings and converts them
4 4
  * to a valid PHP array via {@link parse_str()}.
5  
- * Use together with {@link RESTfulServer} to submit
6  
- * data via POST or PUT.
7 5
  *
8 6
  * Example when using cURL on commandline:
9 7
  * <code>
660  api/RestfulServer.php
... ...
@@ -1,660 +0,0 @@
1  
-<?php
2  
-/**
3  
- * SilverStripe'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 SilverStripe
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  
- * <b>Supported operations</b>
33  
- * 
34  
- *  - GET /api/v1/(ClassName)/(ID) - gets a database record
35  
- *  - GET /api/v1/(ClassName)/(ID)/(Relation) - get all of the records linked to this database record by the given reatlion
36  
- *  - GET /api/v1/(ClassName)?(Field)=(Val)&(Field)=(Val) - searches for matching database records
37  
- *  - POST /api/v1/(ClassName) - create a new database record
38  
- *  - PUT /api/v1/(ClassName)/(ID) - updates a database record
39  
- *  - PUT /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, replacing the existing record(s) (NOT IMPLEMENTED YET)
40  
- *  - POST /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, appending to the existing record(s) (NOT IMPLEMENTED YET)
41  
- * 
42  
- *  - DELETE /api/v1/(ClassName)/(ID) - deletes a database record (NOT IMPLEMENTED YET)
43  
- *  - 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)
44  
- *
45  
- *  - POST /api/v1/(ClassName)/(ID)/(MethodName) - executes a method on the given object (e.g, publish)
46  
- * 
47  
- * <b>Search</b>
48  
- * 
49  
- * You can trigger searches based on the fields specified on {@link DataObject::searchable_fields} and passed
50  
- * through {@link DataObject::getDefaultSearchContext()}. Just add a key-value pair with the search-term
51  
- * to the url, e.g. /api/v1/(ClassName)/?Title=mytitle.
52  
- * 
53  
- * <b>Other url-modifiers</b>
54  
- * 
55  
- * - &limit=<numeric>: Limit the result set
56  
- * - &relationdepth=<numeric>: Displays links to existing has-one and has-many relationships to a certain depth (Default: 1)
57  
- * - &fields=<string>: Comma-separated list of fields on the output object (defaults to all database-columns).
58  
- *   Handy to limit output for bandwidth and performance reasons.
59  
- * - &sort=<myfield>&dir=<asc|desc>
60  
- * - &add_fields=<string>: Comma-separated list of additional fields, for example dynamic getters.
61  
- * 
62  
- * <b>Access control</b>
63  
- *
64  
- * Access control is implemented through the usual Member system with Basicauth authentication only.
65  
- * By default, you have to bear the ADMIN permission to retrieve or send any data.
66  
- *
67  
- * You should override the following built-in methods to customize permission control on a
68  
- * class- and object-level:
69  
- * - {@link DataObject::canView()}
70  
- * - {@link DataObject::canEdit()}
71  
- * - {@link DataObject::canDelete()}
72  
- * - {@link DataObject::canCreate()}
73  
- * See {@link DataObject} documentation for further details.
74  
- * 
75  
- * You can specify the character-encoding for any input on the HTTP Content-Type.
76  
- * At the moment, only UTF-8 is supported. All output is made in UTF-8 regardless of Accept headers.
77  
- * 
78  
- * @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
79  
- * @todo Implement PUT/POST/DELETE for relations
80  
- * @todo Access-Control for relations (you might be allowed to view Members and Groups, but not their relation with each other)
81  
- * @todo Make SearchContext specification customizeable for each class
82  
- * @todo Allow for range-searches (e.g. on Created column)
83  
- * @todo Allow other authentication methods (currently only HTTP BasicAuth)
84  
- * @todo Filter relation listings by $api_access and canView() permissions
85  
- * @todo Exclude relations when "fields" are specified through URL (they should be explicitly requested in this case)
86  
- * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in SiteTree/Versioned/Hierarchy
87  
- * @todo URL parameter namespacing for search-fields, limit, fields, add_fields (might all be valid dataobject properties)
88  
- *       e.g. you wouldn't be able to search for a "limit" property on your subclass as its overlayed with the search logic
89  
- * @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
90  
- * @todo Access to extendable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22
91  
- * @todo Respect $api_access array notation in search contexts
92  
- * 
93  
- * @package framework
94  
- * @subpackage api
95  
- */
96  
-class RestfulServer extends Controller {
97  
-	static $url_handlers = array(
98  
-		'$ClassName/$ID/$Relation' => 'handleAction'
99  
-		#'$ClassName/#ID' => 'handleItem',
100  
-		#'$ClassName' => 'handleList',
101  
-	);
102  
-
103  
-	protected static $api_base = "api/v1/";
104  
-
105  
-	/**
106  
-	 * If no extension is given in the request, resolve to this extension
107  
-	 * (and subsequently the {@link self::$default_mimetype}.
108  
-	 *
109  
-	 * @var string
110  
-	 */
111  
-	public static $default_extension = "xml";
112  
-	
113  
-	/**
114  
-	 * If no extension is given, resolve the request to this mimetype.
115  
-	 *
116  
-	 * @var string
117  
-	 */
118  
-	protected static $default_mimetype = "text/xml";
119  
-	
120  
-	/**
121  
-	 * @uses authenticate()
122  
-	 * @var Member
123  
-	 */
124  
-	protected $member;
125  
-	
126  
-	static $allowed_actions = array(
127  
-		'index'
128  
-	);
129  
-	
130  
-	/*
131  
-	function handleItem($request) {
132  
-		return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
133  
-	}
134  
-
135  
-	function handleList($request) {
136  
-		return new RestfulServer_List(DataObject::get($request->param("ClassName"),""));
137  
-	}
138  
-	*/
139  
-	
140  
-	/**
141  
-	 * This handler acts as the switchboard for the controller.
142  
-	 * Since no $Action url-param is set, all requests are sent here.
143  
-	 */
144  
-	function index() {
145  
-		if(!isset($this->urlParams['ClassName'])) return $this->notFound();
146  
-		$className = $this->urlParams['ClassName'];
147  
-		$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
148  
-		$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
149  
-		
150  
-		// Check input formats
151  
-		if(!class_exists($className)) return $this->notFound();
152  
-		if($id && !is_numeric($id)) return $this->notFound();
153  
-		if($relation && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)) return $this->notFound();
154  
-		
155  
-		// if api access is disabled, don't proceed
156  
-		$apiAccess = singleton($className)->stat('api_access');
157  
-		if(!$apiAccess) return $this->permissionFailure();
158  
-
159  
-		// authenticate through HTTP BasicAuth
160  
-		$this->member = $this->authenticate();
161  
-
162  
-		// handle different HTTP verbs
163  
-		if($this->request->isGET() || $this->request->isHEAD()) return $this->getHandler($className, $id, $relation);
164  
-		if($this->request->isPOST()) return $this->postHandler($className, $id, $relation);
165  
-		if($this->request->isPUT()) return $this->putHandler($className, $id, $relation);
166  
-		if($this->request->isDELETE()) return $this->deleteHandler($className, $id, $relation);
167  
-
168  
-		// if no HTTP verb matches, return error
169  
-		return $this->methodNotAllowed();
170  
-	}
171  
-	
172  
-	/**
173  
-	 * Handler for object read.
174  
-	 * 
175  
-	 * The data object will be returned in the following format:
176  
-	 *
177  
-	 * <ClassName>
178  
-	 *   <FieldName>Value</FieldName>
179  
-	 *   ...
180  
-	 *   <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" />
181  
-	 *   ...
182  
-	 *   <HasManyRelName>
183  
-	 *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
184  
-	 *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
185  
-	 *   </HasManyRelName>
186  
-	 *   ...
187  
-	 *   <ManyManyRelName>
188  
-	 *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
189  
-	 *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
190  
-	 *   </ManyManyRelName>
191  
-	 * </ClassName>
192  
-	 *
193  
-	 * Access is controlled by two variables:
194  
-	 * 
195  
-	 *   - static $api_access must be set. This enables the API on a class by class basis
196  
-	 *   - $obj->canView() must return true. This lets you implement record-level security
197  
-	 * 
198  
-	 * @todo Access checking
199  
-	 * 
200  
-	 * @param String $className
201  
-	 * @param Int $id
202  
-	 * @param String $relation
203  
-	 * @return String The serialized representation of the requested object(s) - usually XML or JSON.
204  
-	 */
205  
-	protected function getHandler($className, $id, $relationName) {
206  
-		$sort = '';
207  
-		
208  
-		if($this->request->getVar('sort')) {
209  
-			$dir = $this->request->getVar('dir');
210  
-			$sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC'));
211  
-		}
212  
-		
213  
-		$limit = array(
214  
-			'start' => $this->request->getVar('start'),
215  
-			'limit' => $this->request->getVar('limit')
216  
-		);
217  
-		
218  
-		$params = $this->request->getVars();
219  
-		
220  
-		$responseFormatter = $this->getResponseDataFormatter($className);
221  
-		if(!$responseFormatter) return $this->unsupportedMediaType();
222  
-		
223  
-		// $obj can be either a DataObject or a SS_List,
224  
-		// depending on the request
225  
-		if($id) {
226  
-			// Format: /api/v1/<MyClass>/<ID>
227  
-			$obj = $this->getObjectQuery($className, $id, $params)->First();
228  
-			if(!$obj) return $this->notFound();
229  
-			if(!$obj->canView()) return $this->permissionFailure();
230  
-
231  
-			// Format: /api/v1/<MyClass>/<ID>/<Relation>
232  
-			if($relationName) {
233  
-				$obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
234  
-				if(!$obj) return $this->notFound();
235  
-				
236  
-				// TODO Avoid creating data formatter again for relation class (see above)
237  
-				$responseFormatter = $this->getResponseDataFormatter($obj->dataClass());
238  
-			} 
239  
-			
240  
-		} else {
241  
-			// Format: /api/v1/<MyClass>
242  
-			$obj = $this->getObjectsQuery($className, $params, $sort, $limit);
243  
-		}
244  
-		
245  
-		$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
246  
-		
247  
-		$rawFields = $this->request->getVar('fields');
248  
-		$fields = $rawFields ? explode(',', $rawFields) : null;
249  
-
250  
-		if($obj instanceof SS_List) {
251  
-			$responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount());
252  
-			return $responseFormatter->convertDataObjectSet($obj, $fields);
253  
-		} else if(!$obj) {
254  
-			$responseFormatter->setTotalSize(0);
255  
-			return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields);
256  
-		} else {
257  
-			return $responseFormatter->convertDataObject($obj, $fields);
258  
-		}
259  
-	}
260  
-	
261  
-	/**
262  
-	 * Uses the default {@link SearchContext} specified through
263  
-	 * {@link DataObject::getDefaultSearchContext()} to augument
264  
-	 * an existing query object (mostly a component query from {@link DataObject})
265  
-	 * with search clauses. 
266  
-	 * 
267  
-	 * @todo Allow specifying of different searchcontext getters on model-by-model basis
268  
-	 *
269  
-	 * @param string $className
270  
-	 * @param array $params
271  
-	 * @return SS_List
272  
-	 */
273  
-	protected function getSearchQuery($className, $params = null, $sort = null, $limit = null, $existingQuery = null) {
274  
-		if(singleton($className)->hasMethod('getRestfulSearchContext')) {
275  
-			$searchContext = singleton($className)->{'getRestfulSearchContext'}();
276  
-		} else {
277  
-			$searchContext = singleton($className)->getDefaultSearchContext();
278  
-		}
279  
-		return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
280  
-	}
281  
-	
282  
-	/**
283  
-	 * Returns a dataformatter instance based on the request
284  
-	 * extension or mimetype. Falls back to {@link self::$default_extension}.
285  
-	 * 
286  
-	 * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers 
287  
-	 * @param String Classname of a DataObject
288  
-	 * @return DataFormatter
289  
-	 */
290  
-	protected function getDataFormatter($includeAcceptHeader = false, $className = null) {
291  
-		$extension = $this->request->getExtension();
292  
-		$contentTypeWithEncoding = $this->request->getHeader('Content-Type');
293  
-		preg_match('/([^;]*)/',$contentTypeWithEncoding, $contentTypeMatches);
294  
-		$contentType = $contentTypeMatches[0];
295  
-		$accept = $this->request->getHeader('Accept');
296  
-		$mimetypes = $this->request->getAcceptMimetypes();
297  
-		if(!$className) $className = $this->urlParams['ClassName'];
298  
-
299  
-		// get formatter
300  
-		if(!empty($extension)) {
301  
-			$formatter = DataFormatter::for_extension($extension);
302  
-		}elseif($includeAcceptHeader && !empty($accept) && $accept != '*/*') {
303  
-			$formatter = DataFormatter::for_mimetypes($mimetypes);
304  
-			if(!$formatter) $formatter = DataFormatter::for_extension(self::$default_extension);
305  
-		} elseif(!empty($contentType)) {
306  
-			$formatter = DataFormatter::for_mimetype($contentType);
307  
-		} else {
308  
-			$formatter = DataFormatter::for_extension(self::$default_extension);
309  
-		}
310  
-
311  
-		if(!$formatter) return false;
312  
-		
313  
-		// set custom fields
314  
-		if($customAddFields = $this->request->getVar('add_fields')) $formatter->setCustomAddFields(explode(',',$customAddFields));
315  
-		if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
316  
-		$formatter->setCustomRelations($this->getAllowedRelations($className));
317  
-		
318  
-		$apiAccess = singleton($className)->stat('api_access');
319  
-		if(is_array($apiAccess)) {
320  
-			$formatter->setCustomAddFields(array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']));
321  
-			if($formatter->getCustomFields()) {
322  
-				$formatter->setCustomFields(array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view']));
323  
-			} else {
324  
-				$formatter->setCustomFields((array)$apiAccess['view']);
325  
-			}
326  
-			if($formatter->getCustomRelations()) {
327  
-				$formatter->setCustomRelations(array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view']));
328  
-			} else {
329  
-				$formatter->setCustomRelations((array)$apiAccess['view']);
330  
-			}
331  
-			
332  
-		}
333  
-
334  
-		// set relation depth
335  
-		$relationDepth = $this->request->getVar('relationdepth');
336  
-		if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth;
337  
-		
338  
-		return $formatter;		
339  
-	}
340  
-	
341  
-	/**
342  
-	 * @param String Classname of a DataObject
343  
-	 * @return DataFormatter
344  
-	 */
345  
-	protected function getRequestDataFormatter($className = null) {
346  
-		return $this->getDataFormatter(false, $className);
347  
-	}
348  
-	
349  
-	/**
350  
-	 * @param String Classname of a DataObject
351  
-	 * @return DataFormatter
352  
-	 */
353  
-	protected function getResponseDataFormatter($className = null) {
354  
-		return $this->getDataFormatter(true, $className);
355  
-	}
356  
-	
357  
-	/**
358  
-	 * Handler for object delete
359  
-	 */
360  
-	protected function deleteHandler($className, $id) {
361  
-		$obj = DataObject::get_by_id($className, $id);
362  
-		if(!$obj) return $this->notFound();
363  
-		if(!$obj->canDelete()) return $this->permissionFailure();
364  
-		
365  
-		$obj->delete();
366  
-		
367  
-		$this->getResponse()->setStatusCode(204); // No Content
368  
-		return true;
369  
-	}
370  
-
371  
-	/**
372  
-	 * Handler for object write
373  
-	 */
374  
-	protected function putHandler($className, $id) {
375  
-		$obj = DataObject::get_by_id($className, $id);
376  
-		if(!$obj) return $this->notFound();
377  
-		if(!$obj->canEdit()) return $this->permissionFailure();
378  
-		
379  
-		$reqFormatter = $this->getRequestDataFormatter($className);
380  
-		if(!$reqFormatter) return $this->unsupportedMediaType();
381  
-		
382  
-		$responseFormatter = $this->getResponseDataFormatter($className);
383  
-		if(!$responseFormatter) return $this->unsupportedMediaType();
384  
-		
385  
-		$obj = $this->updateDataObject($obj, $reqFormatter);
386  
-		
387  
-		$this->getResponse()->setStatusCode(200); // Success
388  
-		$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
389  
-
390  
-		// Append the default extension for the output format to the Location header
391  
-		// or else we'll use the default (XML)
392  
-		$types = $responseFormatter->supportedExtensions();
393  
-		$type = '';
394  
-		if (count($types)) {
395  
-			$type = ".{$types[0]}";
396  
-		}
397  
-
398  
-		$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
399  
-		$this->getResponse()->addHeader('Location', $objHref);
400  
-		
401  
-		return $responseFormatter->convertDataObject($obj);
402  
-	}
403  
-
404  
-	/**
405  
-	 * Handler for object append / method call.
406  
-	 * 
407  
-	 * @todo Posting to an existing URL (without a relation)
408  
-	 * current resolves in creatig a new element,
409  
-	 * rather than a "Conflict" message.
410  
-	 */
411  
-	protected function postHandler($className, $id, $relation) {
412  
-		if($id) {
413  
-			if(!$relation) {
414  
-				$this->response->setStatusCode(409);
415  
-				return 'Conflict';
416  
-			}
417  
-			
418  
-			$obj = DataObject::get_by_id($className, $id);
419  
-			if(!$obj) return $this->notFound();
420  
-			
421  
-			if(!$obj->hasMethod($relation)) {
422  
-				return $this->notFound();
423  
-			}
424  
-			
425  
-			if(!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) {
426  
-				return $this->permissionFailure();
427  
-			}
428  
-			
429  
-			$obj->$relation();
430  
-			
431  
-			$this->getResponse()->setStatusCode(204); // No Content
432  
-			return true;
433  
-		} else {
434  
-			if(!singleton($className)->canCreate()) return $this->permissionFailure();
435  
-			$obj = new $className();
436  
-		
437  
-			$reqFormatter = $this->getRequestDataFormatter($className);
438  
-			if(!$reqFormatter) return $this->unsupportedMediaType();
439  
-		
440  
-			$responseFormatter = $this->getResponseDataFormatter($className);
441  
-		
442  
-			$obj = $this->updateDataObject($obj, $reqFormatter);
443  
-		
444  
-			$this->getResponse()->setStatusCode(201); // Created
445  
-			$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
446  
-
447  
-			// Append the default extension for the output format to the Location header
448  
-			// or else we'll use the default (XML)
449  
-			$types = $responseFormatter->supportedExtensions();
450  
-			$type = '';
451  
-			if (count($types)) {
452  
-				$type = ".{$types[0]}";
453  
-			}
454  
-
455  
-			$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
456  
-			$this->getResponse()->addHeader('Location', $objHref);
457  
-		
458  
-			return $responseFormatter->convertDataObject($obj);
459  
-		}
460  
-	}
461  
-	
462  
-	/**
463  
-	 * Converts either the given HTTP Body into an array
464  
-	 * (based on the DataFormatter instance), or returns
465  
-	 * the POST variables.
466  
-	 * Automatically filters out certain critical fields
467  
-	 * that shouldn't be set by the client (e.g. ID).
468  
-	 *
469  
-	 * @param DataObject $obj
470  
-	 * @param DataFormatter $formatter
471  
-	 * @return DataObject The passed object
472  
-	 */
473  
-	protected function updateDataObject($obj, $formatter) {
474  
-		// if neither an http body nor POST data is present, return error
475  
-		$body = $this->request->getBody();
476  
-		if(!$body && !$this->request->postVars()) {
477  
-			$this->getResponse()->setStatusCode(204); // No Content
478  
-			return 'No Content';
479  
-		}
480  
-		
481  
-		if(!empty($body)) {
482  
-			$data = $formatter->convertStringToArray($body);
483  
-		} else {
484  
-			// assume application/x-www-form-urlencoded which is automatically parsed by PHP
485  
-			$data = $this->request->postVars();
486  
-		}
487  
-		
488  
-		// @todo Disallow editing of certain keys in database
489  
-		$data = array_diff_key($data, array('ID','Created'));
490  
-		
491  
-		$apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
492  
-		if(is_array($apiAccess) && isset($apiAccess['edit'])) {
493  
-			$data = array_intersect_key($data, array_combine($apiAccess['edit'],$apiAccess['edit']));
494  
-		}
495  
-
496  
-		$obj->update($data);
497  
-		$obj->write();
498  
-		
499  
-		return $obj;
500  
-	}
501  
-	
502  
-	/**
503  
-	 * Gets a single DataObject by ID,
504  
-	 * through a request like /api/v1/<MyClass>/<MyID>
505  
-	 * 
506  
-	 * @param string $className
507  
-	 * @param int $id
508  
-	 * @param array $params
509  
-	 * @return DataList
510  
-	 */
511  
-	protected function getObjectQuery($className, $id, $params) {
512  
-	    return DataList::create($className)->byIDs(array($id));
513  
-	}
514  
-	
515  
-	/**
516  
-	 * @param DataObject $obj
517  
-	 * @param array $params
518  
-	 * @param int|array $sort
519  
-	 * @param int|array $limit
520  
-	 * @return SQLQuery
521  
-	 */
522  
-	protected function getObjectsQuery($className, $params, $sort, $limit) {
523  
-		return $this->getSearchQuery($className, $params, $sort, $limit);
524  
-	}
525  
-	
526  
-	
527  
-	/**
528  
-	 * @param DataObject $obj
529  
-	 * @param array $params
530  
-	 * @param int|array $sort
531  
-	 * @param int|array $limit
532  
-	 * @param string $relationName
533  
-	 * @return SQLQuery|boolean
534  
-	 */
535  
-	protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
536  
-		// The relation method will return a DataList, that getSearchQuery subsequently manipulates
537  
-		if($obj->hasMethod($relationName)) {
538  
-			if($relationClass = $obj->has_one($relationName)) {
539  
-				$joinField = $relationName . 'ID';
540  
-				$list = DataList::create($relationClass)->byIDs(array($obj->$joinField));
541  
-			} else {
542  
-				$list = $obj->$relationName();
543  
-			}
544  
-			
545  
-			$apiAccess = singleton($list->dataClass())->stat('api_access');
546  
-			if(!$apiAccess) return false;
547  
-			
548  
-			return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list);
549  
-		}
550  
-	}
551  
-	
552  
-	protected function permissionFailure() {
553  
-		// return a 401
554  
-		$this->getResponse()->setStatusCode(401);
555  
-		$this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"');
556  
-		$this->getResponse()->addHeader('Content-Type', 'text/plain');
557  
-		return "You don't have access to this item through the API.";
558  
-	}
559  
-
560  
-	protected function notFound() {
561  
-		// return a 404
562  
-		$this->getResponse()->setStatusCode(404);
563  
-		$this->getResponse()->addHeader('Content-Type', 'text/plain');
564  
-		return "That object wasn't found";
565  
-	}
566  
-	
567  
-	protected function methodNotAllowed() {
568  
-		$this->getResponse()->setStatusCode(405);
569  
-		$this->getResponse()->addHeader('Content-Type', 'text/plain');
570  
-		return "Method Not Allowed";
571  
-	}
572  
-	
573  
-	protected function unsupportedMediaType() {
574  
-		$this->response->setStatusCode(415); // Unsupported Media Type
575  
-		$this->getResponse()->addHeader('Content-Type', 'text/plain');
576  
-		return "Unsupported Media Type";
577  
-	}
578  
-	
579  
-	protected function authenticate() {
580  
-		if(!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) return false;
581  
-		
582  
-		if($member = Member::currentUser()) return $member;
583  
-		$member = MemberAuthenticator::authenticate(array(
584  
-			'Email' => $_SERVER['PHP_AUTH_USER'], 
585  
-			'Password' => $_SERVER['PHP_AUTH_PW'],
586  
-		), null);
587  
-		
588  
-		if($member) {
589  
-			$member->LogIn(false);
590  
-			return $member;
591  
-		} else {
592  
-			return false;
593  
-		}
594  
-	}
595  
-	
596  
-	/**
597  
-	 * Return only relations which have $api_access enabled.
598  
-	 * @todo Respect field level permissions once they are available in core
599  
-	 * 
600  
-	 * @param string $class
601  
-	 * @param Member $member
602  
-	 * @return array
603  
-	 */
604  
-	protected function getAllowedRelations($class, $member = null) {
605  
-		$allowedRelations = array();
606  
-		$obj = singleton($class);
607  
-		$relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many();
608  
-		if($relations) foreach($relations as $relName => $relClass) {
609  
-			if(singleton($relClass)->stat('api_access')) {
610  
-				$allowedRelations[] = $relName;
611  
-			}
612  
-		}
613  
-		return $allowedRelations;
614  
-	}
615  
-	
616  
-}
617  
-
618  
-/**
619  
- * Restful server handler for a SS_List
620  
- * 
621  
- * @package framework
622  
- * @subpackage api
623  
- */
624  
-class RestfulServer_List {
625  
-	static $url_handlers = array(
626  
-		'#ID' => 'handleItem',
627  
-	);
628  
-
629  
-	function __construct($list) {
630  
-		$this->list = $list;
631  
-	}
632  
-	
633  
-	function handleItem($request) {
634  
-		return new RestulServer_Item($this->list->getById($request->param('ID')));
635  
-	}
636  
-}
637  
-
638  
-/**
639  
- * Restful server handler for a single DataObject
640  
- * 
641  
- * @package framework
642  
- * @subpackage api
643  
- */
644  
-class RestfulServer_Item {
645  
-	static $url_handlers = array(
646  
-		'$Relation' => 'handleRelation',
647  
-	);
648  
-
649  
-	function __construct($item) {
650  
-		$this->item = $item;
651  
-	}
652  
-	
653  
-	function handleRelation($request) {
654  
-		$funcName = $request('Relation');
655  
-		$relation = $this->item->$funcName();
656  
-
657  
-		if($relation instanceof SS_List) return new RestfulServer_List($relation);
658  
-		else return new RestfulServer_Item($relation);
659  
-	}
660  
-}
23  api/VersionedRestfulServer.php
... ...
@@ -1,23 +0,0 @@
1  
-<?php
2  
-/**
3  
- * Simple wrapper to allow access to the live site via REST
4  
- * 
5  
- * @package framework
6  
- * @subpackage integration
7  
- */ 
8  
-class VersionedRestfulServer extends Controller {
9  
-	
10  
-	static $allowed_actions = array( 
11  
-		'index'
12  
-	);
13  
-	
14  
-	function handleRequest(SS_HTTPRequest $request, DataModel $model) {
15  
-		$this->setDataModel($model);
16  
-		Versioned::reading_stage('Live');
17  
-		$restfulserver = new RestfulServer();
18  
-		$response = $restfulserver->handleRequest($request, $model);
19  
-		return $response;
20  
-	}
21  
-}
22  
-
23  
-
2  control/HTTPRequest.php
@@ -170,7 +170,7 @@ function requestVar($name) {
170 170
 	 * Returns a possible file extension found in parsing the URL
171 171
 	 * as denoted by a "."-character near the end of the URL.
172 172
 	 * Doesn't necessarily have to belong to an existing file,
173  
-	 * for example used for {@link RestfulServer} content-type-switching.
  173
+	 * as extensions can be also used for content-type-switching.
174 174
 	 * 
175 175
 	 * @return string
176 176
 	 */
4  docs/en/changelogs/3.0.0.md
Source Rendered
@@ -518,6 +518,10 @@ Use a [custom alias via function scope](http://api.jquery.com/jQuery.noConflict/
518 518
 
519 519
 See [module on github](https://github.com/silverstripe/silverstripe-widgets).
520 520
 
  521
+### Moved `RestfulServer` and `SapphireSoapServer` API into new modules###
  522
+
  523
+See ["restfulserver"] and ["soapserver"] modules on github.
  524
+
521 525
 ### Moved `Translatable` extension into new 'translatable' module ###
522 526
 
523 527
 If you are translating your `SiteTree` or `DataObject` classes with the `Translatable`
2  docs/en/reference/searchcontext.md
Source Rendered
@@ -188,5 +188,5 @@ See `[api:SearchFilter]` API Documentation
188 188
 ## Related
189 189
 
190 190
 *  `[api:ModelAdmin]`
191  
-*  `[api:RestfulServer]`
  191
+*  [RestfulServer module](https://github.com/silverstripe/silverstripe-restfulserver)
192 192
 *  [Tutorial: Site Search](/tutorials/4-site-search)
2  docs/en/topics/search.md
Source Rendered
@@ -34,7 +34,7 @@ dedicated search service like the [sphinx module](http://silverstripe.org/sphinx
34 34
 ## Related
35 35
 
36 36
 *  `[api:ModelAdmin]`
37  
-*  `[api:RestfulServer]`
  37
+*  [RestfulServer module](https://github.com/silverstripe/silverstripe-restfulserver)
38 38
 *  [Tutorial: Site Search](/tutorials/4-site-search)
39 39
 *  [SearchContext](/reference/searchcontext)
40 40
 *  [genericviews module](http://silverstripe.org/generic-views-module)
567  tests/api/RestfulServerTest.php
... ...
@@ -1,567 +0,0 @@
1  
-<?php
2  
-/**
3  
- * 
4  
- * @todo Test Relation getters
5  
- * @todo Test filter and limit through GET params
6  
- * @todo Test DELETE verb
7  
- *
8  
- */
9  
-class RestfulServerTest extends SapphireTest {
10  
-	
11  
-	static $fixture_file = 'RestfulServerTest.yml';
12  
-
13  
-	protected $extraDataObjects = array(
14  
-		'RestfulServerTest_Comment',
15  
-		'RestfulServerTest_SecretThing',
16  
-		'RestfulServerTest_Page',
17  
-		'RestfulServerTest_Author',
18  
-		'RestfulServerTest_AuthorRating',
19  
-	);
20  
-
21  
-	public function testApiAccess() {
22  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
23  
-		$page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1');
24  
-		
25  
-		// normal GET should succeed with $api_access enabled
26  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
27  
-		$response = Director::test($url, null, null, 'GET');
28  
-		$this->assertEquals($response->getStatusCode(), 200);
29  
-		
30  
-		$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
31  
-		$_SERVER['PHP_AUTH_PW'] = 'user';
32  
-		
33  
-		// even with logged in user a GET with $api_access disabled should fail
34  
-		$url = "/api/v1/RestfulServerTest_Page/" . $page1->ID;
35  
-		$response = Director::test($url, null, null, 'GET');
36  
-		$this->assertEquals($response->getStatusCode(), 401);
37  
-		
38  
-		unset($_SERVER['PHP_AUTH_USER']);
39  
-		unset($_SERVER['PHP_AUTH_PW']);
40  
-	}
41  
-	
42  
-	public function testApiAccessBoolean() {
43  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
44  
-		
45  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
46  
-		$response = Director::test($url, null, null, 'GET');
47  
-		$this->assertContains('<ID>', $response->getBody());
48  
-		$this->assertContains('<Name>', $response->getBody());
49  
-		$this->assertContains('<Comment>', $response->getBody());
50  
-		$this->assertContains('<Page', $response->getBody());
51  
-		$this->assertContains('<Author', $response->getBody());
52  
-	}
53  
-	
54  
-	public function testAuthenticatedGET() {
55  
-		$thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1');
56  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
57  
-		
58  
-		// @todo create additional mock object with authenticated VIEW permissions
59  
-		$url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID;
60  
-		$response = Director::test($url, null, null, 'GET');
61  
-		$this->assertEquals($response->getStatusCode(), 401);
62  
-		
63  
-		$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
64  
-		$_SERVER['PHP_AUTH_PW'] = 'user';
65  
-		
66  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
67  
-		$response = Director::test($url, null, null, 'GET');
68  
-		$this->assertEquals($response->getStatusCode(), 200);
69  
-		
70  
-		unset($_SERVER['PHP_AUTH_USER']);
71  
-		unset($_SERVER['PHP_AUTH_PW']);
72  
-	}
73  
-	
74  
-	public function testAuthenticatedPUT() {
75  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
76  
-		
77  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
78  
-		$data = array('Comment' => 'created');
79  
-		
80  
-		$response = Director::test($url, $data, null, 'PUT');
81  
-		$this->assertEquals($response->getStatusCode(), 401); // Permission failure
82  
-		
83  
-		$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
84  
-		$_SERVER['PHP_AUTH_PW'] = 'editor';
85  
-		$response = Director::test($url, $data, null, 'PUT');
86  
-		$this->assertEquals($response->getStatusCode(), 200); // Success
87  
-		
88  
-		unset($_SERVER['PHP_AUTH_USER']);
89  
-		unset($_SERVER['PHP_AUTH_PW']);
90  
-	}
91  
-	
92  
-	public function testGETRelationshipsXML() {
93  
-		$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
94  
-		$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
95  
-		$rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2');
96  
-		
97  
-		// @todo should be set up by fixtures, doesn't work for some reason...
98  
-		$author1->Ratings()->add($rating1);
99  
-		$author1->Ratings()->add($rating2);
100  
-		
101  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
102  
-		$response = Director::test($url, null, null, 'GET');
103  
-		$this->assertEquals($response->getStatusCode(), 200);
104  
-	
105  
-		$responseArr = Convert::xml2array($response->getBody());
106  
-		$ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating'];
107  
-		$this->assertEquals(count($ratingsArr), 2);
108  
-		$ratingIDs = array(
109  
-			(int)$ratingsArr[0]['@attributes']['id'], 
110  
-			(int)$ratingsArr[1]['@attributes']['id']
111  
-		);
112  
-		$this->assertContains($rating1->ID, $ratingIDs);
113  
-		$this->assertContains($rating2->ID, $ratingIDs);
114  
-	}
115  
-	
116  
-	public function testGETManyManyRelationshipsXML() {
117  
-		// author4 has related authors author2 and author3
118  
-		$author2 = $this->objFromFixture('RestfulServerTest_Author', 'author2');
119  
-		$author3 = $this->objFromFixture('RestfulServerTest_Author', 'author3');
120  
-		$author4 = $this->objFromFixture('RestfulServerTest_Author', 'author4');
121  
-		
122  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
123  
-		$response = Director::test($url, null, null, 'GET');
124  
-		$this->assertEquals(200, $response->getStatusCode());
125  
-		$arr = Convert::xml2array($response->getBody());
126  
-		$authorsArr = $arr['RestfulServerTest_Author'];
127  
-		
128  
-		$this->assertEquals(count($authorsArr), 2);
129  
-		$ratingIDs = array(
130  
-			(int)$authorsArr[0]['ID'], 
131  
-			(int)$authorsArr[1]['ID']
132  
-		);
133  
-		$this->assertContains($author2->ID, $ratingIDs);
134  
-		$this->assertContains($author3->ID, $ratingIDs);
135  
-	}
136  
-
137  
-	public function testPUTWithFormEncoded() {
138  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
139  
-		
140  
-		$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
141  
-		$_SERVER['PHP_AUTH_PW'] = 'editor';
142  
-	
143  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
144  
-		$body = 'Name=Updated Comment&Comment=updated';
145  
-		$headers = array(
146  
-			'Content-Type' => 'application/x-www-form-urlencoded'
147  
-		);
148  
-		$response = Director::test($url, null, null, 'PUT', $body, $headers);
149  
-		$this->assertEquals($response->getStatusCode(), 200); // Success
150  
-		// Assumption: XML is default output
151  
-		$responseArr = Convert::xml2array($response->getBody());
152  
-		$this->assertEquals($responseArr['ID'], $comment1->ID);
153  
-		$this->assertEquals($responseArr['Comment'], 'updated');
154  
-		$this->assertEquals($responseArr['Name'], 'Updated Comment');
155  
-	
156  
-		unset($_SERVER['PHP_AUTH_USER']);
157  
-		unset($_SERVER['PHP_AUTH_PW']);
158  
-	}
159  
-	
160  
-	public function testPOSTWithFormEncoded() {
161  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
162  
-		
163  
-		$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
164  
-		$_SERVER['PHP_AUTH_PW'] = 'editor';
165  
-	
166  
-		$url = "/api/v1/RestfulServerTest_Comment";
167  
-		$body = 'Name=New Comment&Comment=created';
168  
-		$headers = array(
169  
-			'Content-Type' => 'application/x-www-form-urlencoded'
170  
-		);
171  
-		$response = Director::test($url, null, null, 'POST', $body, $headers);
172  
-		$this->assertEquals($response->getStatusCode(), 201); // Created
173  
-		// Assumption: XML is default output
174  
-		$responseArr = Convert::xml2array($response->getBody());
175  
-		$this->assertTrue($responseArr['ID'] > 0);
176  
-		$this->assertNotEquals($responseArr['ID'], $comment1->ID);
177  
-		$this->assertEquals($responseArr['Comment'], 'created');
178  
-		$this->assertEquals($responseArr['Name'], 'New Comment');
179  
-		$this->assertEquals($response->getHeader('Location'), Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID']));
180  
-	
181  
-		unset($_SERVER['PHP_AUTH_USER']);
182  
-		unset($_SERVER['PHP_AUTH_PW']);
183  
-	}
184  
-	
185  
-	public function testPUTwithJSON() {
186  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
187  
-		
188  
-		$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
189  
-		$_SERVER['PHP_AUTH_PW'] = 'editor';
190  
-		
191  
-		// by mimetype
192  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
193  
-		$body = '{"Comment":"updated"}';
194  
-		$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'application/json'));
195  
-		$this->assertEquals($response->getStatusCode(), 200); // Updated
196  
-		$obj = Convert::json2obj($response->getBody());
197  
-		$this->assertEquals($obj->ID, $comment1->ID);
198  
-		$this->assertEquals($obj->Comment, 'updated');
199  
-	
200  
-		// by extension
201  
-		$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID);
202  
-		$body = '{"Comment":"updated"}';
203  
-		$response = Director::test($url, null, null, 'PUT', $body);
204  
-		$this->assertEquals($response->getStatusCode(), 200); // Updated
205  
-		$this->assertEquals($response->getHeader('Location'), Controller::join_links(Director::absoluteBaseURL(), $url));
206  
-		$obj = Convert::json2obj($response->getBody());
207  
-		$this->assertEquals($obj->ID, $comment1->ID);
208  
-		$this->assertEquals($obj->Comment, 'updated');
209  
-		
210  
-		unset($_SERVER['PHP_AUTH_USER']);
211  
-		unset($_SERVER['PHP_AUTH_PW']);
212  
-	}
213  
-	
214  
-	public function testPUTwithXML() {
215  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
216  
-		
217  
-		$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
218  
-		$_SERVER['PHP_AUTH_PW'] = 'editor';
219  
-		
220  
-		// by mimetype
221  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
222  
-		$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
223  
-		$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml'));
224  
-		$this->assertEquals($response->getStatusCode(), 200); // Updated
225  
-		$obj = Convert::xml2array($response->getBody());
226  
-		$this->assertEquals($obj['ID'], $comment1->ID);
227  
-		$this->assertEquals($obj['Comment'], 'updated');
228  
-	
229  
-		// by extension
230  
-		$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID);
231  
-		$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
232  
-		$response = Director::test($url, null, null, 'PUT', $body);
233  
-		$this->assertEquals($response->getStatusCode(), 200); // Updated
234  
-		$this->assertEquals($response->getHeader('Location'), Controller::join_links(Director::absoluteBaseURL(), $url));
235  
-		$obj = Convert::xml2array($response->getBody());
236  
-		$this->assertEquals($obj['ID'], $comment1->ID);
237  
-		$this->assertEquals($obj['Comment'], 'updated');
238  
-		
239  
-		unset($_SERVER['PHP_AUTH_USER']);
240  
-		unset($_SERVER['PHP_AUTH_PW']);
241  
-	}
242  
-		
243  
-	public function testHTTPAcceptAndContentType() {
244  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
245  
-		
246  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
247  
-		
248  
-		$headers = array('Accept' => 'application/json');
249  
-		$response = Director::test($url, null, null, 'GET', null, $headers);
250  
-		$this->assertEquals($response->getStatusCode(), 200); // Success
251  
-		$obj = Convert::json2obj($response->getBody());
252  
-		$this->assertEquals($obj->ID, $comment1->ID);
253  
-		$this->assertEquals($response->getHeader('Content-Type'), 'application/json');
254  
-	}
255  
-	
256  
-	public function testNotFound(){
257  
-		$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
258  
-		$_SERVER['PHP_AUTH_PW'] = 'user';
259  
-		
260  
-		$url = "/api/v1/RestfulServerTest_Comment/99";
261  
-		$response = Director::test($url, null, null, 'GET');
262  
-		$this->assertEquals($response->getStatusCode(), 404);
263  
-		
264  
-		unset($_SERVER['PHP_AUTH_USER']);
265  
-		unset($_SERVER['PHP_AUTH_PW']);
266  
-	}
267  
-	
268  
-	public function testMethodNotAllowed() {
269  
-		$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
270  
-		
271  
-		$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
272  
-		$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
273  
-		$this->assertEquals($response->getStatusCode(), 405);
274  
-	}
275  
-	
276  
-	public function testConflictOnExistingResourceWhenUsingPost() {
277  
-		$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
278  
-		
279  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
280  
-		$response = Director::test($url, null, null, 'POST');
281  
-		$this->assertEquals($response->getStatusCode(), 409);
282  
-	}
283  
-	
284  
-	public function testUnsupportedMediaType() {
285  
-		$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
286  
-		$_SERVER['PHP_AUTH_PW'] = 'user';
287  
-	
288  
-		$url = "/api/v1/RestfulServerTest_Comment";
289  
-		$data = "Comment||\/||updated"; // weird format
290  
-		$headers = array('Content-Type' => 'text/weirdformat');
291  
-		$response = Director::test($url, null, null, 'POST', $data, $headers);
292  
-		$this->assertEquals($response->getStatusCode(), 415);
293  
-		
294  
-		unset($_SERVER['PHP_AUTH_USER']);
295  
-		unset($_SERVER['PHP_AUTH_PW']);
296  
-	}
297  
-	
298  
-	public function testXMLValueFormatting() {
299  
-		$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1');
300  
-		
301  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
302  
-		$response = Director::test($url, null, null, 'GET');
303  
-		$this->assertContains('<ID>' . $rating1->ID . '</ID>', $response->getBody());
304  
-		$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
305  
-	}
306  
-	
307  
-	public function testApiAccessFieldRestrictions() {
308  
-		$author1 = $this->objFromFixture('RestfulServerTest_Author','author1');
309  
-		$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1');
310  
-		
311  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
312  
-		$response = Director::test($url, null, null, 'GET');
313  
-		$this->assertContains('<ID>', $response->getBody());
314  
-		$this->assertContains('<Rating>', $response->getBody());
315  
-		$this->assertContains('<Author', $response->getBody());
316  
-		$this->assertNotContains('<SecretField>', $response->getBody());
317  
-		$this->assertNotContains('<SecretRelation>', $response->getBody());
318  
-		
319  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
320  
-		$response = Director::test($url, null, null, 'GET');
321  
-		$this->assertNotContains('<SecretField>', $response->getBody(),
322  
-			'"add_fields" URL parameter filters out disallowed fields from $api_access'
323  
-		);
324  
-		$this->assertNotContains('<SecretRelation>', $response->getBody(),
325  
-			'"add_fields" URL parameter filters out disallowed relations from $api_access'
326  
-		);
327  
-		
328  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?fields=SecretField,SecretRelation';
329  
-		$response = Director::test($url, null, null, 'GET');
330  
-		$this->assertNotContains('<SecretField>', $response->getBody(),
331  
-			'"fields" URL parameter filters out disallowed fields from $api_access'
332  
-		);
333  
-		$this->assertNotContains('<SecretRelation>', $response->getBody(),
334  
-			'"fields" URL parameter filters out disallowed relations from $api_access'
335  
-		);
336  
-		
337  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings';
338  
-		$response = Director::test($url, null, null, 'GET');
339  
-		$this->assertContains('<Rating>', $response->getBody(),
340  
-			'Relation viewer shows fields allowed through $api_access'
341  
-		);
342  
-		$this->assertNotContains('<SecretField>', $response->getBody(),
343  
-			'Relation viewer on has-many filters out disallowed fields from $api_access'
344  
-		);
345  
-	}
346  
-	
347  
-	public function testApiAccessRelationRestrictionsInline() {
348  
-		$author1 = $this->objFromFixture('RestfulServerTest_Author','author1');
349  
-		
350  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
351  
-		$response = Director::test($url, null, null, 'GET');
352  
-		$this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
353  
-		$this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
354  
-	}
355  
-	
356  
-	public function testApiAccessRelationRestrictionsOnEndpoint() {
357  
-		$author1 = $this->objFromFixture('RestfulServerTest_Author','author1');
358  
-		
359  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage";
360  
-		$response = Director::test($url, null, null, 'GET');
361  
-		$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');
362  
-		
363  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages";
364  
-		$response = Director::test($url, null, null, 'GET');
365  
-		$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');
366  
-		
367  
-		$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages";
368  
-		$response = Director::test($url, null, null, 'GET');
369  
-		$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
370  
-	}
371  
-	
372  
-	public function testApiAccessWithPUT() {
373  
-		$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating','rating1');
374  
-		
375  
-		$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
376  
-		$data = array(
377  
-			'Rating' => '42',
378  
-			'WriteProtectedField' => 'haxx0red'
379  
-		);
380  
-		$response = Director::test($url, $data, null, 'PUT');
381  
-		// Assumption: XML is default output
382  
-		$responseArr = Convert::xml2array($response->getBody());
383  
-		$this->assertEquals($responseArr['Rating'], 42);
384  
-		$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
385  
-	}
386  
-
387  
-	public function testJSONDataFormatter() {
388  
-		$formatter = new JSONDataFormatter();
389  
-		$editor = $this->objFromFixture('Member', 'editor');
390  
-		$user = $this->objFromFixture('Member', 'user');
391  
-