1 <?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
90 class RestfulServer extends Controller {
91 static $url_handlers = array(
92 '$ClassName/$ID/$Relation' => 'handleAction'
93
94
95 );
96
97 protected static $api_base = "api/v1/";
98
99 100 101 102 103 104
105 public static $default_extension = "xml";
106
107 108 109 110 111
112 protected static $default_mimetype = "text/xml";
113
114 115 116 117
118 protected $member;
119
120 121 122 123 124 125 126 127 128
129
130 131 132 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
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
146 $apiAccess = singleton($className)->stat('api_access');
147 if(!$apiAccess) return $this->permissionFailure();
148
149
150 $this->member = $this->authenticate();
151
152
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
159 return $this->methodNotAllowed();
160 }
161
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 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
211
212 if($id) {
213
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
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
229 $query = $this->getObjectsQuery($className, $params, $sort, $limit);
230 $obj = singleton($className)->buildDataObjectSet($query->execute());
231
232
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 254 255 256 257 258 259 260 261 262 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 277 278 279 280 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
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
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
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 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);
351 return true;
352 }
353
354 355 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);
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 380 381 382 383 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);
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);
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 429 430 431 432 433 434 435 436 437
438 protected function updateDataObject($obj, $formatter) {
439
440 $body = $this->request->getBody();
441 if(!$body && !$this->request->postVars()) {
442 $this->getResponse()->setStatusCode(204);
443 return 'No Content';
444 }
445
446 if(!empty($body)) {
447 $data = $formatter->convertStringToArray($body);
448 } else {
449
450 $data = $this->request->postVars();
451 }
452
453
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 469 470 471 472 473 474 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 485 486 487 488 489
490 protected function getObjectsQuery($className, $params, $sort, $limit) {
491 return $this->getSearchQuery($className, $params, $sort, $limit);
492 }
493
494
495 496 497 498 499 500 501 502
503 protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) {
504 if($obj->hasMethod("{$relationName}Query")) {
505
506 $query = $obj->{"{$relationName}Query"}(null, $sort, null, $limit);
507 $relationClass = $obj->{"{$relationName}Class"}();
508 } elseif($relationClass = $obj->many_many($relationName)) {
509
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
521 return $this->getSearchQuery($relationClass, $params, $sort, $limit, $query);
522 }
523
524 protected function permissionFailure() {
525
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
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);
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 566 567 568 569 570 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 588 589 590 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 608 609 610 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.
-