mirror of
https://github.com/silverstripe/silverstripe-restfulserver
synced 2024-10-22 14:05:58 +02:00
Upgrade: begin SilverStripe 4 compatiblity update
This commit is contained in:
parent
e5a757d589
commit
6601b42c4b
@ -10,7 +10,7 @@ indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.yml,package.json}]
|
||||
[{*.yml,package.json,*.js}]
|
||||
indent_size = 2
|
||||
|
||||
# The indent size used in the package.json file cannot be changed:
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -4,3 +4,4 @@
|
||||
/.gitignore export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/.scrutinizer.yml export-ignore
|
||||
/codecov.yml export-ignore
|
||||
|
@ -1,9 +1,15 @@
|
||||
inherit: true
|
||||
|
||||
build:
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override: [php-scrutinizer-run]
|
||||
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
|
||||
filter:
|
||||
paths: [code/*, tests/*]
|
||||
paths: [src/*, tests/*]
|
||||
|
39
.travis.yml
39
.travis.yml
@ -1,28 +1,33 @@
|
||||
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
|
||||
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
env:
|
||||
global:
|
||||
- COMPOSER_ROOT_VERSION=2.0.x-dev
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- php: 5.4
|
||||
env: DB=MYSQL CORE_RELEASE=3.3
|
||||
- php: 5.5
|
||||
env: DB=MYSQL CORE_RELEASE=3.4
|
||||
- php: 5.6
|
||||
env: DB=PGSQL CORE_RELEASE=3.5
|
||||
env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
|
||||
- php: 7.0
|
||||
env: DB=MYSQL CORE_RELEASE=3
|
||||
env: DB=MYSQL PHPUNIT_TEST=1
|
||||
- php: 7.1
|
||||
env: DB=MYSQL CORE_RELEASE=3.6
|
||||
env: DB=PGSQL PHPUNIT_COVERAGE_TEST=1
|
||||
|
||||
before_script:
|
||||
- composer self-update || true
|
||||
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support
|
||||
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
|
||||
- cd ~/builds/ss
|
||||
- composer install
|
||||
# Init PHP
|
||||
- phpenv rehash
|
||||
- phpenv config-rm xdebug.ini
|
||||
|
||||
# Install composer dependencies
|
||||
- composer validate
|
||||
- composer require --no-update silverstripe/recipe-core:1.0.x-dev
|
||||
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql 2.0.x-dev; fi
|
||||
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit restfulserver/tests
|
||||
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
|
||||
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
|
||||
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=vendor/silverstripe/framework/phpcs.xml.dist code/ tests/; fi
|
||||
|
||||
after_success:
|
||||
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi
|
||||
|
15
.upgrade.yml
Normal file
15
.upgrade.yml
Normal file
@ -0,0 +1,15 @@
|
||||
mappings:
|
||||
BasicRestfulAuthenticator: SilverStripe\RestfulServer\BasicRestfulAuthenticator
|
||||
RestfulServer: SilverStripe\RestfulServer\RestfulServer
|
||||
RestfulServerItem: SilverStripe\RestfulServer\RestfulServerItem
|
||||
RestfulServerList: SilverStripe\RestfulServer\RestfulServerList
|
||||
RestfulServerTest: SilverStripe\RestfulServer\Tests\RestfulServerTest
|
||||
RestfulServerTestAuthor: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor
|
||||
RestfulServerTestAuthorRating: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating
|
||||
RestfulServerTestComment: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment
|
||||
RestfulServerTestPage: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage
|
||||
RestfulServerTestSecretThing: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing
|
||||
DataFormatter: SilverStripe\RestfulServer\DataFormatter\DataFormatter
|
||||
FormEncodedDataFormatter: SilverStripe\RestfulServer\DataFormatter\FormEncodedDataFormatter
|
||||
JSONDataFormatter: SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter
|
||||
XMLDataFormatter: SilverStripe\RestfulServer\DataFormatter\XMLDataFormatter
|
68
README.md
68
README.md
@ -1,39 +1,63 @@
|
||||
# SilverStripe RestfulServer Module
|
||||
|
||||
[![Build Status](https://secure.travis-ci.org/silverstripe/silverstripe-restfulserver.png)](http://travis-ci.org/silverstripe/silverstripe-restfulserver)
|
||||
[![Build Status](https://travis-ci.org/silverstripe/silverstripe-restfulserver.svg?branch=master)](https://travis-ci.org/silverstripe/silverstripe-restfulserver)
|
||||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/silverstripe/silverstripe-restfulserver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/silverstripe/silverstripe-restfulserver/?branch=master)
|
||||
[![codecov](https://codecov.io/gh/silverstripe/silverstripe-restfulserver/branch/master/graph/badge.svg)](https://codecov.io/gh/silverstripe/silverstripe-restfulserver)
|
||||
|
||||
## Overview
|
||||
|
||||
This class gives your application a RESTful API. All you have to do is define static $api_access = true on
|
||||
the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in
|
||||
This class gives your application a RESTful API. All you have to do is set the `api_access` configuration option to `true`
|
||||
on the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in
|
||||
your model layer (ie, the DataObject classes) and not in your Controllers. This is the recommended design for SilverStripe
|
||||
applications.
|
||||
|
||||
## Requirements
|
||||
|
||||
* SilverStripe 3.0 or newer
|
||||
* SilverStripe 4.0 or higher
|
||||
|
||||
For a SilverStripe 3.x compatible version of this module, please see the [1.0 branch, or 1.x release line](https://github.com/silverstripe/silverstripe-restfulserver/tree/1.0#readme).
|
||||
|
||||
## Configuration
|
||||
|
||||
Enabling restful access on a model will also enable a SOAP API, see `SOAPModelAccess`.
|
||||
|
||||
Example DataObject with simple api access, giving full access to all object properties and relations,
|
||||
Example DataObject with simple API access, giving full access to all object properties and relations,
|
||||
unless explicitly controlled through model permissions.
|
||||
|
||||
class Article extends DataObject {
|
||||
static $db = array('Title'=>'Text','Published'=>'Boolean');
|
||||
static $api_access = true;
|
||||
}
|
||||
```php
|
||||
namespace Vendor\Project;
|
||||
|
||||
Example DataObject with advanced api access, limiting viewing and editing to Title attribute only:
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Article extends DataObject {
|
||||
static $db = array('Title'=>'Text','Published'=>'Boolean');
|
||||
static $api_access = array(
|
||||
'view' => array('Title'),
|
||||
'edit' => array('Title'),
|
||||
);
|
||||
}
|
||||
class Article extends DataObject {
|
||||
|
||||
private static $db = [
|
||||
'Title'=>'Text',
|
||||
'Published'=>'Boolean'
|
||||
];
|
||||
|
||||
private static $api_access = true;
|
||||
}
|
||||
```
|
||||
|
||||
Example DataObject with advanced API access, limiting viewing and editing to Title attribute only:
|
||||
|
||||
```php
|
||||
namespace Vendor\Project;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
class Article extends DataObject {
|
||||
|
||||
private static $db = [
|
||||
'Title'=>'Text',
|
||||
'Published'=>'Boolean'
|
||||
];
|
||||
|
||||
private static $api_access = [
|
||||
'view' => ['Title'],
|
||||
'edit' => ['Title']
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Supported operations
|
||||
|
||||
@ -66,7 +90,7 @@ to the url, e.g. /api/v1/(ClassName)/?Title=mytitle.
|
||||
|
||||
## Access control
|
||||
|
||||
Access control is implemented through the usual Member system with Basicauth authentication only.
|
||||
Access control is implemented through the usual Member system with BasicAuth authentication only.
|
||||
By default, you have to bear the ADMIN permission to retrieve or send any data.
|
||||
You should override the following built-in methods to customize permission control on a
|
||||
class- and object-level:
|
||||
@ -76,7 +100,7 @@ class- and object-level:
|
||||
- `DataObject::canDelete()`
|
||||
- `DataObject::canCreate()`
|
||||
|
||||
See `DataObject` documentation for further details.
|
||||
See `SilverStripe\ORM\DataObject` documentation for further details.
|
||||
|
||||
You can specify the character-encoding for any input on the HTTP Content-Type.
|
||||
At the moment, only UTF-8 is supported. All output is made in UTF-8 regardless of Accept headers.
|
||||
At the moment, only UTF-8 is supported. All output is made in UTF-8 regardless of Accept headers.
|
||||
|
@ -1,10 +1,6 @@
|
||||
---
|
||||
Name: restfulserverroutes
|
||||
After:
|
||||
- '#rootroutes'
|
||||
- '#modelascontrollerroutes'
|
||||
---
|
||||
Director:
|
||||
SilverStripe\Control\Director:
|
||||
rules:
|
||||
'api/v1/live': 'VersionedRestfulServer'
|
||||
'api/v1': 'RestfulServer'
|
||||
'api/v1': 'SilverStripe\RestfulServer\RestfulServer'
|
||||
|
20
changelog.md
20
changelog.md
@ -1,20 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.0.5]
|
||||
|
||||
* Converted to PSR-2
|
||||
* Added standard Scrutinizer config
|
||||
* Added standard code of conduct
|
||||
* Added standard editor config
|
||||
* Added standard Travis config
|
||||
* Added standard license
|
||||
* Added standard git attributes
|
||||
|
||||
## [1.0.4]
|
||||
|
||||
* Changelog added.
|
||||
* Include 3.2 and php 5.6 in tests
|
1
codecov.yml
Normal file
1
codecov.yml
Normal file
@ -0,0 +1 @@
|
||||
comment: false
|
@ -1,12 +1,9 @@
|
||||
{
|
||||
"name": "silverstripe/restfulserver",
|
||||
"description": "Add a RESTful API to your SilverStripe application",
|
||||
"type": "silverstripe-module",
|
||||
"keywords": [
|
||||
"silverstripe",
|
||||
"rest",
|
||||
"api"
|
||||
],
|
||||
"type": "silverstripe-vendormodule",
|
||||
"keywords": ["silverstripe", "rest", "api"],
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Hamish Friedlander",
|
||||
@ -18,12 +15,23 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"silverstripe/framework": "3.*"
|
||||
"silverstripe/framework": "^4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/PHPUnit": "^5.7",
|
||||
"squizlabs/php_codesniffer": "^3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SilverStripe\\RestfulServer\\": "src",
|
||||
"SilverStripe\\RestfulServer\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1.x-dev"
|
||||
"dev-master": "2.x-dev"
|
||||
}
|
||||
},
|
||||
"license": "BSD-3-Clause"
|
||||
"prefer-stable": true,
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016, SilverStripe Limited
|
||||
Copyright (c) 2017, SilverStripe Limited
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
9
phpcs.xml.dist
Normal file
9
phpcs.xml.dist
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
|
||||
<rule ref="PSR2" >
|
||||
<!-- Current exclusions -->
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||
</rule>
|
||||
</ruleset>
|
13
phpunit.xml.dist
Normal file
13
phpunit.xml.dist
Normal file
@ -0,0 +1,13 @@
|
||||
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
|
||||
<testsuite name="Default">
|
||||
<directory>tests/</directory>
|
||||
</testsuite>
|
||||
<filter>
|
||||
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">code/</directory>
|
||||
<exclude>
|
||||
<directory suffix=".php">tests/</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
@ -1,5 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer;
|
||||
|
||||
use SilverStripe\Security\Authenticator;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Security\Security;
|
||||
|
||||
/**
|
||||
* A simple authenticator for the Restful server.
|
||||
*
|
||||
@ -19,23 +26,25 @@ class BasicRestfulAuthenticator
|
||||
*/
|
||||
public static function authenticate()
|
||||
{
|
||||
//if there is no username or password, break
|
||||
//if there is no username or password, fail
|
||||
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
//Attempt to authenticate with the default authenticator for the site
|
||||
$authClass = Authenticator::get_default_authenticator();
|
||||
$member = $authClass::authenticate(array(
|
||||
// With a valid user and password, check the password is correct
|
||||
$data = [
|
||||
'Email' => $_SERVER['PHP_AUTH_USER'],
|
||||
'Password' => $_SERVER['PHP_AUTH_PW'],
|
||||
));
|
||||
|
||||
//Log the member in and return the member, if they were found
|
||||
if ($member) {
|
||||
$member->LogIn(false);
|
||||
return $member;
|
||||
];
|
||||
$request = Controller::curr()->getRequest();
|
||||
$authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
|
||||
$member = null;
|
||||
foreach ($authenticators as $authenticator) {
|
||||
$member = $authenticator->authenticate($data, $request);
|
||||
if ($member) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return $member;
|
||||
}
|
||||
}
|
350
src/DataFormatter/DataFormatter.php
Normal file
350
src/DataFormatter/DataFormatter.php
Normal file
@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* A DataFormatter object handles transformation of data from SilverStripe model objects to a particular output
|
||||
* format, and vice versa. This is most commonly used in developing RESTful APIs.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage formatters
|
||||
*/
|
||||
abstract class DataFormatter
|
||||
{
|
||||
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* Set priority from 0-100.
|
||||
* If multiple formatters for the same extension exist,
|
||||
* we select the one with highest priority.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $priority = 50;
|
||||
|
||||
/**
|
||||
* Follow relations for the {@link DataObject} instances
|
||||
* ($has_one, $has_many, $many_many).
|
||||
* Set to "0" to disable relation output.
|
||||
*
|
||||
* @todo Support more than one nesting level
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $relationDepth = 1;
|
||||
|
||||
/**
|
||||
* Allows overriding of the fields which are rendered for the
|
||||
* processed dataobjects. By default, this includes all
|
||||
* fields in {@link DataObject::inheritedDatabaseFields()}.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $customFields = null;
|
||||
|
||||
/**
|
||||
* Allows addition of fields
|
||||
* (e.g. custom getters on a DataObject)
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $customAddFields = null;
|
||||
|
||||
/**
|
||||
* Allows to limit or add relations.
|
||||
* Only use in combination with {@link $relationDepth}.
|
||||
* By default, all relations will be shown.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $customRelations = null;
|
||||
|
||||
/**
|
||||
* Fields which should be expicitly excluded from the export.
|
||||
* Comes in handy for field-level permissions.
|
||||
* Will overrule both {@link $customAddFields} and {@link $customFields}
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $removeFields = null;
|
||||
|
||||
/**
|
||||
* Specifies the mimetype in which all strings
|
||||
* returned from the convert*() methods should be used,
|
||||
* e.g. "text/xml".
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $outputContentType = null;
|
||||
|
||||
/**
|
||||
* Used to set totalSize properties on the output
|
||||
* of {@link convertDataObjectSet()}, shows the
|
||||
* total number of records without the "limit" and "offset"
|
||||
* GET parameters. Useful to implement pagination.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $totalSize;
|
||||
|
||||
/**
|
||||
* Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
|
||||
* kills both requests (i.e. URIs) and XML (invalid character in a tag name)
|
||||
* So we'll replace them with a hyphen (-), as it's also unambiguious
|
||||
* in both cases (invalid in a php class name, and safe in an xml tag name)
|
||||
*
|
||||
* @param string $classname
|
||||
* @return string 'escaped' class name
|
||||
*/
|
||||
protected function sanitiseClassName($className)
|
||||
{
|
||||
return str_replace('\\', '-', $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DataFormatter object suitable for handling the given file extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* @return DataFormatter
|
||||
*/
|
||||
public static function for_extension($extension)
|
||||
{
|
||||
$classes = ClassInfo::subclassesFor(DataFormatter::class);
|
||||
array_shift($classes);
|
||||
$sortedClasses = array();
|
||||
foreach ($classes as $class) {
|
||||
$sortedClasses[$class] = singleton($class)->stat('priority');
|
||||
}
|
||||
arsort($sortedClasses);
|
||||
foreach ($sortedClasses as $className => $priority) {
|
||||
$formatter = new $className();
|
||||
if (in_array($extension, $formatter->supportedExtensions())) {
|
||||
return $formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatter for the first matching extension.
|
||||
*
|
||||
* @param array $extensions
|
||||
* @return DataFormatter
|
||||
*/
|
||||
public static function for_extensions($extensions)
|
||||
{
|
||||
foreach ($extensions as $extension) {
|
||||
if ($formatter = self::for_extension($extension)) {
|
||||
return $formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DataFormatter object suitable for handling the given mimetype.
|
||||
*
|
||||
* @param string $mimeType
|
||||
* @return DataFormatter
|
||||
*/
|
||||
public static function for_mimetype($mimeType)
|
||||
{
|
||||
$classes = ClassInfo::subclassesFor(DataFormatter::class);
|
||||
array_shift($classes);
|
||||
$sortedClasses = array();
|
||||
foreach ($classes as $class) {
|
||||
$sortedClasses[$class] = singleton($class)->stat('priority');
|
||||
}
|
||||
arsort($sortedClasses);
|
||||
foreach ($sortedClasses as $className => $priority) {
|
||||
$formatter = new $className();
|
||||
if (in_array($mimeType, $formatter->supportedMimeTypes())) {
|
||||
return $formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatter for the first matching mimetype.
|
||||
* Useful for HTTP Accept headers which can contain
|
||||
* multiple comma-separated mimetypes.
|
||||
*
|
||||
* @param array $mimetypes
|
||||
* @return DataFormatter
|
||||
*/
|
||||
public static function for_mimetypes($mimetypes)
|
||||
{
|
||||
foreach ($mimetypes as $mimetype) {
|
||||
if ($formatter = self::for_mimetype($mimetype)) {
|
||||
return $formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
*/
|
||||
public function setCustomFields($fields)
|
||||
{
|
||||
$this->customFields = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomFields()
|
||||
{
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
*/
|
||||
public function setCustomAddFields($fields)
|
||||
{
|
||||
$this->customAddFields = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $relations
|
||||
*/
|
||||
public function setCustomRelations($relations)
|
||||
{
|
||||
$this->customRelations = $relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomRelations()
|
||||
{
|
||||
return $this->customRelations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomAddFields()
|
||||
{
|
||||
return $this->customAddFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
*/
|
||||
public function setRemoveFields($fields)
|
||||
{
|
||||
$this->removeFields = $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRemoveFields()
|
||||
{
|
||||
return $this->removeFields;
|
||||
}
|
||||
|
||||
public function getOutputContentType()
|
||||
{
|
||||
return $this->outputContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $size
|
||||
*/
|
||||
public function setTotalSize($size)
|
||||
{
|
||||
$this->totalSize = (int)$size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTotalSize()
|
||||
{
|
||||
return $this->totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all fields on the object which should be shown
|
||||
* in the output. Can be customised through {@link self::setCustomFields()}.
|
||||
*
|
||||
* @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
|
||||
* @todo Field level permission checks
|
||||
*
|
||||
* @param DataObject $obj
|
||||
* @return array
|
||||
*/
|
||||
protected function getFieldsForObj($obj)
|
||||
{
|
||||
$dbFields = array();
|
||||
|
||||
// if custom fields are specified, only select these
|
||||
if (is_array($this->customFields)) {
|
||||
foreach ($this->customFields as $fieldName) {
|
||||
// @todo Possible security risk by making methods accessible - implement field-level security
|
||||
if ($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
|
||||
$dbFields[$fieldName] = $fieldName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// by default, all database fields are selected
|
||||
$dbFields = DataObject::getSchema()->fieldSpecs(get_class($obj));
|
||||
// $dbFields = $obj->inheritedDatabaseFields();
|
||||
}
|
||||
|
||||
if (is_array($this->customAddFields)) {
|
||||
foreach ($this->customAddFields as $fieldName) {
|
||||
// @todo Possible security risk by making methods accessible - implement field-level security
|
||||
if ($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
|
||||
$dbFields[$fieldName] = $fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add default required fields
|
||||
$dbFields = array_merge($dbFields, array('ID'=>'Int'));
|
||||
|
||||
if (is_array($this->removeFields)) {
|
||||
$dbFields = array_diff_key($dbFields, array_combine($this->removeFields, $this->removeFields));
|
||||
}
|
||||
|
||||
return $dbFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of the extensions that this data formatter supports
|
||||
*/
|
||||
abstract public function supportedExtensions();
|
||||
|
||||
abstract public function supportedMimeTypes();
|
||||
|
||||
|
||||
/**
|
||||
* Convert a single data object to this format. Return a string.
|
||||
*/
|
||||
abstract public function convertDataObject(DataObjectInterface $do);
|
||||
|
||||
/**
|
||||
* Convert a data object set to this format. Return a string.
|
||||
*/
|
||||
abstract public function convertDataObjectSet(SS_List $set);
|
||||
|
||||
/**
|
||||
* @param string $strData HTTP Payload as string
|
||||
*/
|
||||
public function convertStringToArray($strData)
|
||||
{
|
||||
user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
|
||||
}
|
||||
}
|
46
src/DataFormatter/FormEncodedDataFormatter.php
Normal file
46
src/DataFormatter/FormEncodedDataFormatter.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||
|
||||
/**
|
||||
* Accepts form encoded strings and converts them
|
||||
* to a valid PHP array via {@link parse_str()}.
|
||||
*
|
||||
* Example when using cURL on commandline:
|
||||
* <code>
|
||||
* curl -d "Name=This is a new record" http://host/api/v1/(DataObject)
|
||||
* curl -X PUT -d "Name=This is an updated record" http://host/api/v1/(DataObject)/1
|
||||
* </code>
|
||||
*
|
||||
* @todo Format response form encoded as well - currently uses XMLDataFormatter
|
||||
*
|
||||
* @author Cam Spiers <camspiers at gmail dot com>
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage formatters
|
||||
*/
|
||||
class FormEncodedDataFormatter extends XMLDataFormatter
|
||||
{
|
||||
|
||||
public function supportedExtensions()
|
||||
{
|
||||
return array(
|
||||
);
|
||||
}
|
||||
|
||||
public function supportedMimeTypes()
|
||||
{
|
||||
return array(
|
||||
'application/x-www-form-urlencoded'
|
||||
);
|
||||
}
|
||||
|
||||
public function convertStringToArray($strData)
|
||||
{
|
||||
$postArray = array();
|
||||
parse_str($strData, $postArray);
|
||||
return $postArray;
|
||||
//TODO: It would be nice to implement this function in Convert.php
|
||||
//return Convert::querystr2array($strData);
|
||||
}
|
||||
}
|
170
src/DataFormatter/JSONDataFormatter.php
Normal file
170
src/DataFormatter/JSONDataFormatter.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage formatters
|
||||
*/
|
||||
class JSONDataFormatter extends DataFormatter
|
||||
{
|
||||
/**
|
||||
* @config
|
||||
* @todo pass this from the API to the data formatter somehow
|
||||
*/
|
||||
private static $api_base = "api/v1/";
|
||||
|
||||
protected $outputContentType = 'application/json';
|
||||
|
||||
public function supportedExtensions()
|
||||
{
|
||||
return array(
|
||||
'json',
|
||||
'js'
|
||||
);
|
||||
}
|
||||
|
||||
public function supportedMimeTypes()
|
||||
{
|
||||
return array(
|
||||
'application/json',
|
||||
'text/x-json'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON representation of the given {@link DataObject}.
|
||||
*
|
||||
* @param DataObject $obj The object
|
||||
* @param Array $fields If supplied, only fields in the list will be returned
|
||||
* @param $relations Not used
|
||||
* @return String JSON
|
||||
*/
|
||||
public function convertDataObject(DataObjectInterface $obj, $fields = null, $relations = null)
|
||||
{
|
||||
return Convert::array2json($this->convertDataObjectToJSONObject($obj, $fields, $relations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to do the conversion of a single data object. It builds an empty object and dynamically
|
||||
* adds the properties it needs to it. If it's done as a nested array, json_encode or equivalent won't use
|
||||
* JSON object notation { ... }.
|
||||
* @param DataObjectInterface $obj
|
||||
* @param $fields
|
||||
* @param $relations
|
||||
* @return EmptyJSONObject
|
||||
*/
|
||||
public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields = null, $relations = null)
|
||||
{
|
||||
$className = get_class($obj);
|
||||
$id = $obj->ID;
|
||||
|
||||
$serobj = ArrayData::array_to_object();
|
||||
|
||||
foreach ($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
|
||||
// Field filtering
|
||||
if ($fields && !in_array($fieldName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||
$serobj->$fieldName = $fieldValue;
|
||||
}
|
||||
|
||||
if ($this->relationDepth > 0) {
|
||||
foreach ($obj->hasOne() as $relName => $relClass) {
|
||||
if (!singleton($relClass)->stat('api_access')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Field filtering
|
||||
if ($fields && !in_array($relName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldName = $relName . 'ID';
|
||||
if ($obj->$fieldName) {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$relClass/" . $obj->$fieldName);
|
||||
} else {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName");
|
||||
}
|
||||
$serobj->$relName = ArrayData::array_to_object(array(
|
||||
"className" => $relClass,
|
||||
"href" => "$href.json",
|
||||
"id" => $obj->$fieldName
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($obj->hasMany() + $obj->manyMany() as $relName => $relClass) {
|
||||
//remove dot notation from relation names
|
||||
$parts = explode('.', $relClass);
|
||||
$relClass = array_shift($parts);
|
||||
|
||||
if (!singleton($relClass)->stat('api_access')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Field filtering
|
||||
if ($fields && !in_array($relName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$innerParts = array();
|
||||
$items = $obj->$relName();
|
||||
foreach ($items as $item) {
|
||||
//$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName/$item->ID");
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||
$innerParts[] = ArrayData::array_to_object(array(
|
||||
"className" => $relClass,
|
||||
"href" => "$href.json",
|
||||
"id" => $item->ID
|
||||
));
|
||||
}
|
||||
$serobj->$relName = $innerParts;
|
||||
}
|
||||
}
|
||||
|
||||
return $serobj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON representation of the given {@link SS_List}.
|
||||
*
|
||||
* @param SS_List $set
|
||||
* @return String XML
|
||||
*/
|
||||
public function convertDataObjectSet(SS_List $set, $fields = null)
|
||||
{
|
||||
$items = array();
|
||||
foreach ($set as $do) {
|
||||
if (!$do->canView()) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $this->convertDataObjectToJSONObject($do, $fields);
|
||||
}
|
||||
|
||||
$serobj = ArrayData::array_to_object(array(
|
||||
"totalSize" => (is_numeric($this->totalSize)) ? $this->totalSize : null,
|
||||
"items" => $items
|
||||
));
|
||||
|
||||
return Convert::array2json($serobj);
|
||||
}
|
||||
|
||||
public function convertStringToArray($strData)
|
||||
{
|
||||
return Convert::json2array($strData);
|
||||
}
|
||||
}
|
202
src/DataFormatter/XMLDataFormatter.php
Normal file
202
src/DataFormatter/XMLDataFormatter.php
Normal file
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage formatters
|
||||
*/
|
||||
class XMLDataFormatter extends DataFormatter
|
||||
{
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @todo pass this from the API to the data formatter somehow
|
||||
*/
|
||||
private static $api_base = "api/v1/";
|
||||
|
||||
protected $outputContentType = 'text/xml';
|
||||
|
||||
public function supportedExtensions()
|
||||
{
|
||||
return array(
|
||||
'xml'
|
||||
);
|
||||
}
|
||||
|
||||
public function supportedMimeTypes()
|
||||
{
|
||||
return array(
|
||||
'text/xml',
|
||||
'application/xml',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an XML representation of the given {@link DataObject}.
|
||||
*
|
||||
* @param DataObject $obj
|
||||
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
||||
* @return String XML
|
||||
*/
|
||||
public function convertDataObject(DataObjectInterface $obj, $fields = null)
|
||||
{
|
||||
$response = Controller::curr()->getResponse();
|
||||
if ($response) {
|
||||
$response->addHeader("Content-Type", "text/xml");
|
||||
}
|
||||
|
||||
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . $this->convertDataObjectWithoutHeader($obj, $fields);
|
||||
}
|
||||
|
||||
public function convertDataObjectWithoutHeader(DataObject $obj, $fields = null, $relations = null)
|
||||
{
|
||||
$className = $this->sanitiseClassName(get_class($obj));
|
||||
$id = $obj->ID;
|
||||
$objHref = Director::absoluteURL($this->config()->api_base . "$className/$obj->ID");
|
||||
|
||||
$xml = "<$className href=\"$objHref.xml\">\n";
|
||||
foreach ($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
|
||||
// Field filtering
|
||||
if ($fields && !in_array($fieldName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||
if (!mb_check_encoding($fieldValue, 'utf-8')) {
|
||||
$fieldValue = "(data is badly encoded)";
|
||||
}
|
||||
|
||||
if (is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toXML')) {
|
||||
$xml .= $fieldValue->toXML();
|
||||
} else {
|
||||
if ('HTMLText' == $fieldType) {
|
||||
// Escape HTML values using CDATA
|
||||
$fieldValue = sprintf('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $fieldValue));
|
||||
} else {
|
||||
$fieldValue = Convert::raw2xml($fieldValue);
|
||||
}
|
||||
$xml .= "<$fieldName>$fieldValue</$fieldName>\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->relationDepth > 0) {
|
||||
foreach ($obj->hasOne() as $relName => $relClass) {
|
||||
if (!singleton($relClass)->stat('api_access')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Field filtering
|
||||
if ($fields && !in_array($relName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldName = $relName . 'ID';
|
||||
if ($obj->$fieldName) {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$relClass/" . $obj->$fieldName);
|
||||
} else {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName");
|
||||
}
|
||||
$xml .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"" . $obj->$fieldName
|
||||
. "\"></$relName>\n";
|
||||
}
|
||||
|
||||
foreach ($obj->hasMany() as $relName => $relClass) {
|
||||
//remove dot notation from relation names
|
||||
$parts = explode('.', $relClass);
|
||||
$relClass = array_shift($parts);
|
||||
if (!singleton($relClass)->stat('api_access')) {
|
||||
continue;
|
||||
}
|
||||
// backslashes in FQCNs kills both URIs and XML
|
||||
$relClass = $this->sanitiseClassName($relClass);
|
||||
|
||||
// Field filtering
|
||||
if ($fields && !in_array($relName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xml .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
|
||||
$items = $obj->$relName();
|
||||
if ($items) {
|
||||
foreach ($items as $item) {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||
$xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\"></$relClass>\n";
|
||||
}
|
||||
}
|
||||
$xml .= "</$relName>\n";
|
||||
}
|
||||
|
||||
foreach ($obj->manyMany() as $relName => $relClass) {
|
||||
//remove dot notation from relation names
|
||||
$parts = explode('.', $relClass);
|
||||
$relClass = array_shift($parts);
|
||||
if (!singleton($relClass)->stat('api_access')) {
|
||||
continue;
|
||||
}
|
||||
// backslashes in FQCNs kills both URIs and XML
|
||||
$relClass = $this->sanitiseClassName($relClass);
|
||||
|
||||
// Field filtering
|
||||
if ($fields && !in_array($relName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xml .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
|
||||
$items = $obj->$relName();
|
||||
if ($items) {
|
||||
foreach ($items as $item) {
|
||||
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||
$xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\"></$relClass>\n";
|
||||
}
|
||||
}
|
||||
$xml .= "</$relName>\n";
|
||||
}
|
||||
}
|
||||
|
||||
$xml .= "</$className>";
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an XML representation of the given {@link SS_List}.
|
||||
*
|
||||
* @param SS_List $set
|
||||
* @return String XML
|
||||
*/
|
||||
public function convertDataObjectSet(SS_List $set, $fields = null)
|
||||
{
|
||||
Controller::curr()->getResponse()->addHeader("Content-Type", "text/xml");
|
||||
$className = $this->sanitiseClassName(get_class($set));
|
||||
|
||||
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||
$xml .= (is_numeric($this->totalSize)) ? "<$className totalSize=\"{$this->totalSize}\">\n" : "<$className>\n";
|
||||
foreach ($set as $item) {
|
||||
$xml .= $this->convertDataObjectWithoutHeader($item, $fields);
|
||||
}
|
||||
$xml .= "</$className>";
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
public function convertStringToArray($strData)
|
||||
{
|
||||
return Convert::xml2array($strData);
|
||||
}
|
||||
}
|
@ -1,10 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer;
|
||||
|
||||
use SilverStripe\RestfulServer\BasicRestfulAuthenticator;
|
||||
use SilverStripe\CMS\Model\SiteTree;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\RestfulServer\DataFormatter\DataFormatter;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
|
||||
/**
|
||||
* Generic RESTful server, which handles webservice access to arbitrary DataObjects.
|
||||
* Relies on serialization/deserialization into different formats provided
|
||||
* by the DataFormatter APIs in core.
|
||||
*
|
||||
* @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
|
||||
* @todo Finish RestfulServerItem and RestfulServerList implementation and re-enable $url_handlers
|
||||
* @todo Implement PUT/POST/DELETE for relations
|
||||
* @todo Access-Control for relations (you might be allowed to view Members and Groups,
|
||||
* but not their relation with each other)
|
||||
@ -28,15 +44,16 @@
|
||||
*/
|
||||
class RestfulServer extends Controller
|
||||
{
|
||||
public static $url_handlers = array(
|
||||
'$ClassName/$ID/$Relation' => 'handleAction'
|
||||
private static $url_handlers = array(
|
||||
'$ClassName!/$ID/$Relation' => 'handleAction',
|
||||
'' => 'notFound'
|
||||
#'$ClassName/#ID' => 'handleItem',
|
||||
#'$ClassName' => 'handleList',
|
||||
);
|
||||
|
||||
protected static $api_base = "api/v1/";
|
||||
private static $api_base = "api/v1/";
|
||||
|
||||
protected static $authenticator = 'BasicRestfulAuthenticator';
|
||||
private static $authenticator = BasicRestfulAuthenticator::class;
|
||||
|
||||
/**
|
||||
* If no extension is given in the request, resolve to this extension
|
||||
@ -44,7 +61,7 @@ class RestfulServer extends Controller
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $default_extension = "xml";
|
||||
private static $default_extension = "xml";
|
||||
|
||||
/**
|
||||
* If no extension is given, resolve the request to this mimetype.
|
||||
@ -59,17 +76,18 @@ class RestfulServer extends Controller
|
||||
*/
|
||||
protected $member;
|
||||
|
||||
public static $allowed_actions = array(
|
||||
'index'
|
||||
private static $allowed_actions = array(
|
||||
'index',
|
||||
'notFound'
|
||||
);
|
||||
|
||||
/*
|
||||
function handleItem($request) {
|
||||
return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
|
||||
return new RestfulServerItem(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
|
||||
}
|
||||
|
||||
function handleList($request) {
|
||||
return new RestfulServer_List(DataObject::get($request->param("ClassName"),""));
|
||||
return new RestfulServerList(DataObject::get($request->param("ClassName"),""));
|
||||
}
|
||||
*/
|
||||
|
||||
@ -79,24 +97,47 @@ class RestfulServer extends Controller
|
||||
* to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live.
|
||||
* TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API)
|
||||
*/
|
||||
if (class_exists('SiteTree')) {
|
||||
singleton('SiteTree')->extend('modelascontrollerInit', $this);
|
||||
if (class_exists(SiteTree::class)) {
|
||||
singleton(SiteTree::class)->extend('modelascontrollerInit', $this);
|
||||
}
|
||||
parent::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
|
||||
* kills both requests (i.e. URIs) and XML (invalid character in a tag name)
|
||||
* So we'll replace them with a hyphen (-), as it's also unambiguious
|
||||
* in both cases (invalid in a php class name, and safe in an xml tag name)
|
||||
*
|
||||
* @param string $classname
|
||||
* @return string 'escaped' class name
|
||||
*/
|
||||
protected function sanitiseClassName($className)
|
||||
{
|
||||
return str_replace('\\', '-', $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hyphen escaped class names back into fully qualified
|
||||
* PHP safe variant.
|
||||
*
|
||||
* @param string $classname
|
||||
* @return string syntactically valid classname
|
||||
*/
|
||||
protected function unsanitiseClassName($className)
|
||||
{
|
||||
return str_replace('-', '\\', $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* This handler acts as the switchboard for the controller.
|
||||
* Since no $Action url-param is set, all requests are sent here.
|
||||
*/
|
||||
public function index()
|
||||
public function index(HTTPRequest $request)
|
||||
{
|
||||
if (!isset($this->urlParams['ClassName'])) {
|
||||
return $this->notFound();
|
||||
}
|
||||
$className = $this->urlParams['ClassName'];
|
||||
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
|
||||
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
|
||||
$className = $this->unsanitiseClassName($request->param('ClassName'));
|
||||
$id = $request->param('ID') ?: null;
|
||||
$relation = $request->param('Relation') ?: null;
|
||||
|
||||
// Check input formats
|
||||
if (!class_exists($className)) {
|
||||
@ -105,8 +146,7 @@ class RestfulServer extends Controller
|
||||
if ($id && !is_numeric($id)) {
|
||||
return $this->notFound();
|
||||
}
|
||||
if (
|
||||
$relation
|
||||
if ($relation
|
||||
&& !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
|
||||
) {
|
||||
return $this->notFound();
|
||||
@ -259,8 +299,12 @@ class RestfulServer extends Controller
|
||||
* @param array $params
|
||||
* @return SS_List
|
||||
*/
|
||||
protected function getSearchQuery($className, $params = null, $sort = null,
|
||||
$limit = null, $existingQuery = null
|
||||
protected function getSearchQuery(
|
||||
$className,
|
||||
$params = null,
|
||||
$sort = null,
|
||||
$limit = null,
|
||||
$existingQuery = null
|
||||
) {
|
||||
if (singleton($className)->hasMethod('getRestfulSearchContext')) {
|
||||
$searchContext = singleton($className)->{'getRestfulSearchContext'}();
|
||||
@ -287,7 +331,7 @@ class RestfulServer extends Controller
|
||||
$accept = $this->request->getHeader('Accept');
|
||||
$mimetypes = $this->request->getAcceptMimetypes();
|
||||
if (!$className) {
|
||||
$className = $this->urlParams['ClassName'];
|
||||
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
|
||||
}
|
||||
|
||||
// get formatter
|
||||
@ -393,6 +437,7 @@ class RestfulServer extends Controller
|
||||
if (!$obj) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$obj->canEdit($this->getMember())) {
|
||||
return $this->permissionFailure();
|
||||
}
|
||||
@ -424,7 +469,8 @@ class RestfulServer extends Controller
|
||||
$type = ".{$types[0]}";
|
||||
}
|
||||
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
|
||||
$urlSafeClassName = $this->sanitiseClassName(get_class($obj));
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$urlSafeClassName/$obj->ID" . $type);
|
||||
$this->getResponse()->addHeader('Location', $objHref);
|
||||
|
||||
return $responseFormatter->convertDataObject($obj);
|
||||
@ -493,7 +539,8 @@ class RestfulServer extends Controller
|
||||
$type = ".{$types[0]}";
|
||||
}
|
||||
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
|
||||
$urlSafeClassName = $this->sanitiseClassName(get_class($obj));
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$urlSafeClassName/$obj->ID" . $type);
|
||||
$this->getResponse()->addHeader('Location', $objHref);
|
||||
|
||||
return $responseFormatter->convertDataObject($obj);
|
||||
@ -529,7 +576,8 @@ class RestfulServer extends Controller
|
||||
// @todo Disallow editing of certain keys in database
|
||||
$data = array_diff_key($data, array('ID', 'Created'));
|
||||
|
||||
$apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
|
||||
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
|
||||
$apiAccess = singleton($className)->config()->api_access;
|
||||
if (is_array($apiAccess) && isset($apiAccess['edit'])) {
|
||||
$data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
|
||||
}
|
||||
@ -579,9 +627,13 @@ class RestfulServer extends Controller
|
||||
{
|
||||
// The relation method will return a DataList, that getSearchQuery subsequently manipulates
|
||||
if ($obj->hasMethod($relationName)) {
|
||||
if ($relationClass = $obj->has_one($relationName)) {
|
||||
// $this->HasOneName() will return a dataobject or null, neither
|
||||
// of which helps us get the classname in a consistent fashion.
|
||||
// So we must use a way that is reliable.
|
||||
if ($relationClass = DataObject::getSchema()->hasOneComponent(get_class($obj), $relationName)) {
|
||||
$joinField = $relationName . 'ID';
|
||||
$list = DataList::create($relationClass)->byIDs(array($obj->$joinField));
|
||||
// Again `byID` will return the wrong type for our purposes. So use `byIDs`
|
||||
$list = DataList::create($relationClass)->byIDs([$obj->$joinField]);
|
||||
} else {
|
||||
$list = $obj->$relationName();
|
||||
}
|
||||
@ -633,8 +685,10 @@ class RestfulServer extends Controller
|
||||
*/
|
||||
protected function authenticate()
|
||||
{
|
||||
$authClass = self::config()->authenticator;
|
||||
return $authClass::authenticate();
|
||||
$authClass = $this->config()->authenticator;
|
||||
$member = $authClass::authenticate();
|
||||
Security::setCurrentUser($member);
|
||||
return $member;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -649,9 +703,12 @@ class RestfulServer extends Controller
|
||||
{
|
||||
$allowedRelations = array();
|
||||
$obj = singleton($class);
|
||||
$relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many();
|
||||
$relations = (array)$obj->hasOne() + (array)$obj->hasMany() + (array)$obj->manyMany();
|
||||
if ($relations) {
|
||||
foreach ($relations as $relName => $relClass) {
|
||||
//remove dot notation from relation names
|
||||
$parts = explode('.', $relClass);
|
||||
$relClass = array_shift($parts);
|
||||
if (singleton($relClass)->stat('api_access')) {
|
||||
$allowedRelations[] = $relName;
|
||||
}
|
||||
@ -667,59 +724,6 @@ class RestfulServer extends Controller
|
||||
*/
|
||||
protected function getMember()
|
||||
{
|
||||
return Member::currentUser();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restful server handler for a SS_List
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage api
|
||||
*/
|
||||
class RestfulServer_List
|
||||
{
|
||||
public static $url_handlers = array(
|
||||
'#ID' => 'handleItem',
|
||||
);
|
||||
|
||||
public function __construct($list)
|
||||
{
|
||||
$this->list = $list;
|
||||
}
|
||||
|
||||
public function handleItem($request)
|
||||
{
|
||||
return new RestulServer_Item($this->list->getById($request->param('ID')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restful server handler for a single DataObject
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage api
|
||||
*/
|
||||
class RestfulServer_Item
|
||||
{
|
||||
public static $url_handlers = array(
|
||||
'$Relation' => 'handleRelation',
|
||||
);
|
||||
|
||||
public function __construct($item)
|
||||
{
|
||||
$this->item = $item;
|
||||
}
|
||||
|
||||
public function handleRelation($request)
|
||||
{
|
||||
$funcName = $request('Relation');
|
||||
$relation = $this->item->$funcName();
|
||||
|
||||
if ($relation instanceof SS_List) {
|
||||
return new RestfulServer_List($relation);
|
||||
} else {
|
||||
return new RestfulServer_Item($relation);
|
||||
}
|
||||
return Security::getCurrentUser();
|
||||
}
|
||||
}
|
35
src/RestfulServerItem.php
Normal file
35
src/RestfulServerItem.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer;
|
||||
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
/**
|
||||
* Restful server handler for a single DataObject
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage api
|
||||
*/
|
||||
class RestfulServerItem
|
||||
{
|
||||
private static $url_handlers = array(
|
||||
'$Relation' => 'handleRelation',
|
||||
);
|
||||
|
||||
public function __construct($item)
|
||||
{
|
||||
$this->item = $item;
|
||||
}
|
||||
|
||||
public function handleRelation($request)
|
||||
{
|
||||
$funcName = $request('Relation');
|
||||
$relation = $this->item->$funcName();
|
||||
|
||||
if ($relation instanceof SS_List) {
|
||||
return new RestfulServerList($relation);
|
||||
} else {
|
||||
return new RestfulServerItem($relation);
|
||||
}
|
||||
}
|
||||
}
|
26
src/RestfulServerList.php
Normal file
26
src/RestfulServerList.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer;
|
||||
|
||||
/**
|
||||
* Restful server handler for a SS_List
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage api
|
||||
*/
|
||||
class RestfulServerList
|
||||
{
|
||||
private static $url_handlers = array(
|
||||
'#ID' => 'handleItem',
|
||||
);
|
||||
|
||||
public function __construct($list)
|
||||
{
|
||||
$this->list = $list;
|
||||
}
|
||||
|
||||
public function handleItem($request)
|
||||
{
|
||||
return new RestfulServerItem($this->list->getById($request->param('ID')));
|
||||
}
|
||||
}
|
575
tests/RestfulServerTest.php
Normal file
575
tests/RestfulServerTest.php
Normal file
@ -0,0 +1,575 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests;
|
||||
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;
|
||||
use Page;
|
||||
|
||||
/**
|
||||
*
|
||||
* @todo Test Relation getters
|
||||
* @todo Test filter and limit through GET params
|
||||
* @todo Test DELETE verb
|
||||
*
|
||||
*/
|
||||
class RestfulServerTest extends SapphireTest
|
||||
{
|
||||
protected static $fixture_file = 'RestfulServerTest.yml';
|
||||
|
||||
protected $baseURI = 'http://www.fakesite.test';
|
||||
|
||||
protected static $extra_dataobjects = [
|
||||
RestfulServerTestComment::class,
|
||||
RestfulServerTestSecretThing::class,
|
||||
RestfulServerTestPage::class,
|
||||
RestfulServerTestAuthor::class,
|
||||
RestfulServerTestAuthorRating::class,
|
||||
];
|
||||
|
||||
protected function urlSafeClassname($classname)
|
||||
{
|
||||
return str_replace('\\', '-', $classname);
|
||||
}
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
Director::config()->set('alternate_base_url', $this->baseURI);
|
||||
Security::setCurrentUser(null);
|
||||
}
|
||||
|
||||
public function testApiAccess()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
$page1 = $this->objFromFixture(RestfulServerTestPage::class, 'page1');
|
||||
|
||||
// normal GET should succeed with $api_access enabled
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
// even with logged in user a GET with $api_access disabled should fail
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestPage::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $page1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(401, $response->getStatusCode());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testApiAccessBoolean()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>', $response->getBody());
|
||||
$this->assertContains('<Name>', $response->getBody());
|
||||
$this->assertContains('<Comment>', $response->getBody());
|
||||
$this->assertContains('<Page', $response->getBody());
|
||||
$this->assertContains('<Author', $response->getBody());
|
||||
}
|
||||
|
||||
public function testAuthenticatedGET()
|
||||
{
|
||||
$thing1 = $this->objFromFixture(RestfulServerTestSecretThing::class, 'thing1');
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
// @todo create additional mock object with authenticated VIEW permissions
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $thing1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(401, $response->getStatusCode());
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testAuthenticatedPUT()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$data = array('Comment' => 'created');
|
||||
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
$this->assertEquals(401, $response->getStatusCode()); // Permission failure
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testGETRelationshipsXML()
|
||||
{
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
$rating2 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating2');
|
||||
|
||||
// @todo should be set up by fixtures, doesn't work for some reason...
|
||||
$author1->Ratings()->add($rating1);
|
||||
$author1->Ratings()->add($rating2);
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$xmlTagSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$ratingsArr = $responseArr['Ratings'][$xmlTagSafeClassName];
|
||||
$this->assertEquals(2, count($ratingsArr));
|
||||
$ratingIDs = array(
|
||||
(int)$ratingsArr[0]['@attributes']['id'],
|
||||
(int)$ratingsArr[1]['@attributes']['id']
|
||||
);
|
||||
$this->assertContains($rating1->ID, $ratingIDs);
|
||||
$this->assertContains($rating2->ID, $ratingIDs);
|
||||
}
|
||||
|
||||
public function testGETManyManyRelationshipsXML()
|
||||
{
|
||||
// author4 has related authors author2 and author3
|
||||
$author2 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author2');
|
||||
$author3 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author3');
|
||||
$author4 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author4');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author4->ID . '/RelatedAuthors';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$arr = Convert::xml2array($response->getBody());
|
||||
$xmlSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$authorsArr = $arr[$xmlSafeClassName];
|
||||
|
||||
$this->assertEquals(2, count($authorsArr));
|
||||
$ratingIDs = array(
|
||||
(int)$authorsArr[0]['ID'],
|
||||
(int)$authorsArr[1]['ID']
|
||||
);
|
||||
$this->assertContains($author2->ID, $ratingIDs);
|
||||
$this->assertContains($author3->ID, $ratingIDs);
|
||||
}
|
||||
|
||||
public function testPUTWithFormEncoded()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$body = 'Name=Updated Comment&Comment=updated';
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
);
|
||||
$response = Director::test($url, null, null, 'PUT', $body, $headers);
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $responseArr['ID']);
|
||||
$this->assertEquals('updated', $responseArr['Comment']);
|
||||
$this->assertEquals('Updated Comment', $responseArr['Name']);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPOSTWithFormEncoded()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname";
|
||||
$body = 'Name=New Comment&Comment=created';
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
);
|
||||
$response = Director::test($url, null, null, 'POST', $body, $headers);
|
||||
$this->assertEquals(201, $response->getStatusCode()); // Created
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertTrue($responseArr['ID'] > 0);
|
||||
$this->assertNotEquals($responseArr['ID'], $comment1->ID);
|
||||
$this->assertEquals('created', $responseArr['Comment']);
|
||||
$this->assertEquals('New Comment', $responseArr['Name']);
|
||||
$this->assertEquals(
|
||||
Controller::join_links($url, $responseArr['ID'] . '.xml'),
|
||||
$response->getHeader('Location')
|
||||
);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPostWithoutBodyReturnsNoContent()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$url = "{$this->baseURI}/api/v1/" . RestfulServerTestComment::class;
|
||||
$response = Director::test($url, null, null, 'POST');
|
||||
|
||||
$this->assertEquals('No Content', $response->getBody());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPUTwithJSON()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
// by acceptance mimetype
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$body = '{"Comment":"updated"}';
|
||||
$response = Director::test($url, null, null, 'PUT', $body, array(
|
||||
'Content-Type'=>'application/json',
|
||||
'Accept' => 'application/json'
|
||||
));
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $obj->ID);
|
||||
$this->assertEquals('updated', $obj->Comment);
|
||||
|
||||
// by extension
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/{$comment1->ID}.json";
|
||||
$body = '{"Comment":"updated"}';
|
||||
$response = Director::test($url, null, null, 'PUT', $body);
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||
$this->assertEquals($url, $response->getHeader('Location'));
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $obj->ID);
|
||||
$this->assertEquals('updated', $obj->Comment);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPUTwithXML()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
// by mimetype
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$body = '<RestfulServerTestComment><Comment>updated</Comment></RestfulServerTestComment>';
|
||||
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml'));
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||
$obj = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $obj['ID']);
|
||||
$this->assertEquals('updated', $obj['Comment']);
|
||||
|
||||
// by extension
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/{$comment1->ID}.xml";
|
||||
$body = '<RestfulServerTestComment><Comment>updated</Comment></RestfulServerTestComment>';
|
||||
$response = Director::test($url, null, null, 'PUT', $body);
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||
$this->assertEquals($url, $response->getHeader('Location'));
|
||||
$obj = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $obj['ID']);
|
||||
$this->assertEquals('updated', $obj['Comment']);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testHTTPAcceptAndContentType()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
|
||||
$headers = array('Accept' => 'application/json');
|
||||
$response = Director::test($url, null, null, 'GET', null, $headers);
|
||||
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($comment1->ID, $obj->ID);
|
||||
$this->assertEquals('application/json', $response->getHeader('Content-Type'));
|
||||
}
|
||||
|
||||
public function testNotFound()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/99";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testMethodNotAllowed()
|
||||
{
|
||||
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
|
||||
$this->assertEquals(405, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testConflictOnExistingResourceWhenUsingPost()
|
||||
{
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'POST');
|
||||
$this->assertEquals(409, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testUnsupportedMediaType()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname";
|
||||
$data = "Comment||\/||updated"; // weird format
|
||||
$headers = array('Content-Type' => 'text/weirdformat');
|
||||
$response = Director::test($url, null, null, 'POST', $data, $headers);
|
||||
$this->assertEquals(415, $response->getStatusCode());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testXMLValueFormatting()
|
||||
{
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>' . $rating1->ID . '</ID>', $response->getBody());
|
||||
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
||||
}
|
||||
|
||||
public function testApiAccessFieldRestrictions()
|
||||
{
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>', $response->getBody());
|
||||
$this->assertContains('<Rating>', $response->getBody());
|
||||
$this->assertContains('<Author', $response->getBody());
|
||||
$this->assertNotContains('<SecretField>', $response->getBody());
|
||||
$this->assertNotContains('<SecretRelation>', $response->getBody());
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains(
|
||||
'<SecretField>',
|
||||
$response->getBody(),
|
||||
'"add_fields" URL parameter filters out disallowed fields from $api_access'
|
||||
);
|
||||
$this->assertNotContains(
|
||||
'<SecretRelation>',
|
||||
$response->getBody(),
|
||||
'"add_fields" URL parameter filters out disallowed relations from $api_access'
|
||||
);
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID . '?fields=SecretField,SecretRelation';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains(
|
||||
'<SecretField>',
|
||||
$response->getBody(),
|
||||
'"fields" URL parameter filters out disallowed fields from $api_access'
|
||||
);
|
||||
$this->assertNotContains(
|
||||
'<SecretRelation>',
|
||||
$response->getBody(),
|
||||
'"fields" URL parameter filters out disallowed relations from $api_access'
|
||||
);
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . '/Ratings';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains(
|
||||
'<Rating>',
|
||||
$response->getBody(),
|
||||
'Relation viewer shows fields allowed through $api_access'
|
||||
);
|
||||
$this->assertNotContains(
|
||||
'<SecretField>',
|
||||
$response->getBody(),
|
||||
'Relation viewer on has-many filters out disallowed fields from $api_access'
|
||||
);
|
||||
}
|
||||
|
||||
public function testApiAccessRelationRestrictionsInline()
|
||||
{
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
|
||||
$this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
|
||||
}
|
||||
|
||||
public function testApiAccessRelationRestrictionsOnEndpoint()
|
||||
{
|
||||
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/ProfilePage";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/RelatedPages";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/PublishedPages";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
|
||||
}
|
||||
|
||||
public function testApiAccessWithPUT()
|
||||
{
|
||||
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||
$data = array(
|
||||
'Rating' => '42',
|
||||
'WriteProtectedField' => 'haxx0red'
|
||||
);
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(42, $responseArr['Rating']);
|
||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||
}
|
||||
|
||||
public function testJSONDataFormatter()
|
||||
{
|
||||
$formatter = new JSONDataFormatter();
|
||||
$editor = $this->objFromFixture(Member::class, 'editor');
|
||||
$user = $this->objFromFixture(Member::class, 'user');
|
||||
|
||||
// The DataFormatter performs canView calls
|
||||
// these are `Member`s so we need to be ADMIN types
|
||||
$this->logInWithPermission('ADMIN');
|
||||
|
||||
$this->assertEquals(
|
||||
'{"FirstName":"Editor","Email":"editor@test.com"}',
|
||||
$formatter->convertDataObject($editor, ["FirstName", "Email"]),
|
||||
"Correct JSON formatting with field subset"
|
||||
);
|
||||
|
||||
$set = Member::get()
|
||||
->filter('ID', [$editor->ID, $user->ID])
|
||||
->sort('"Email" ASC'); // for sorting for postgres
|
||||
$this->assertEquals(
|
||||
'{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' .
|
||||
'{"FirstName":"User","Email":"user@test.com"}]}',
|
||||
$formatter->convertDataObjectSet($set, ["FirstName", "Email"]),
|
||||
"Correct JSON formatting on a dataobjectset with field filter"
|
||||
);
|
||||
}
|
||||
|
||||
public function testApiAccessWithPOST()
|
||||
{
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||
$data = [
|
||||
'Rating' => '42',
|
||||
'WriteProtectedField' => 'haxx0red'
|
||||
];
|
||||
$response = Director::test($url, $data, null, 'POST');
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(42, $responseArr['Rating']);
|
||||
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||
}
|
||||
|
||||
public function testCanViewRespectedInList()
|
||||
{
|
||||
// Default content type
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||
|
||||
// JSON content type
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname.json";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||
$responseArray = Convert::json2array($response->getBody());
|
||||
$this->assertSame(0, $responseArray['totalSize']);
|
||||
|
||||
// With authentication
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertContains('Unspeakable', $response->getBody());
|
||||
// Assumption: default formatter is XML
|
||||
$responseArray = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(1, $responseArray['@attributes']['totalSize']);
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
}
|
66
tests/RestfulServerTest.yml
Normal file
66
tests/RestfulServerTest.yml
Normal file
@ -0,0 +1,66 @@
|
||||
SilverStripe\Security\Member:
|
||||
editor:
|
||||
FirstName: Editor
|
||||
Email: editor@test.com
|
||||
Password: editor
|
||||
user:
|
||||
FirstName: User
|
||||
Email: user@test.com
|
||||
Password: user
|
||||
SilverStripe\Security\Group:
|
||||
editorgroup:
|
||||
Title: Editors
|
||||
Code: editors
|
||||
Members: =>SilverStripe\Security\Member.editor
|
||||
usergroup:
|
||||
Title: Users
|
||||
Code: users
|
||||
Members: =>SilverStripe\Security\Member.user
|
||||
SilverStripe\Security\Permission:
|
||||
perm1:
|
||||
Code: CREATE_Comment
|
||||
Group: =>SilverStripe\Security\Group.usergroup
|
||||
perm3:
|
||||
Code: EDIT_Comment
|
||||
Group: =>SilverStripe\Security\Group.editorgroup
|
||||
perm4:
|
||||
Code: DELETE_Comment
|
||||
Group: =>SilverStripe\Security\Group.editorgroup
|
||||
perm5:
|
||||
Code: CREATE_Comment
|
||||
Group: =>SilverStripe\Security\Group.editorgroup
|
||||
perm6:
|
||||
Code: VIEW_SecretThing
|
||||
Group: =>SilverStripe\Security\Group.editorgroup
|
||||
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage:
|
||||
page1:
|
||||
Title: Testpage without API Access
|
||||
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment:
|
||||
comment1:
|
||||
Name: Joe
|
||||
Comment: This is a test comment
|
||||
Page: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage.page1
|
||||
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor:
|
||||
author1:
|
||||
FirstName: Author 1
|
||||
author2:
|
||||
FirstName: Author 2
|
||||
author3:
|
||||
Firstname: Author 3
|
||||
author4:
|
||||
FirstName: Author 4
|
||||
RelatedAuthors: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author2,=>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author3
|
||||
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating:
|
||||
rating1:
|
||||
Rating: 3
|
||||
WriteProtectedField: Dont overwrite me
|
||||
SecretField: Dont look at me!
|
||||
Author: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||
SecretRelation: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||
rating2:
|
||||
Rating: 5
|
||||
Author: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||
SecretRelation: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing:
|
||||
thing1:
|
||||
Name: Unspeakable
|
35
tests/Stubs/RestfulServerTestAuthor.php
Normal file
35
tests/Stubs/RestfulServerTestAuthor.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class RestfulServerTestAuthor extends DataObject implements TestOnly
|
||||
{
|
||||
private static $api_access = true;
|
||||
|
||||
private static $table_name = 'RestfulServerTestAuthor';
|
||||
|
||||
private static $db = array(
|
||||
'Name' => 'Text',
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
'RelatedPages' => RestfulServerTestPage::class,
|
||||
'RelatedAuthors' => RestfulServerTestAuthor::class,
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'PublishedPages' => RestfulServerTestPage::class,
|
||||
'Ratings' => RestfulServerTestAuthorRating::class,
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
49
tests/Stubs/RestfulServerTestAuthorRating.php
Normal file
49
tests/Stubs/RestfulServerTestAuthorRating.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class RestfulServerTestAuthorRating extends DataObject implements TestOnly
|
||||
{
|
||||
private static $api_access = array(
|
||||
'view' => array(
|
||||
'Rating',
|
||||
'WriteProtectedField',
|
||||
'Author'
|
||||
),
|
||||
'edit' => array(
|
||||
'Rating'
|
||||
)
|
||||
);
|
||||
|
||||
private static $table_name = 'RestfulServerTestAuthorRating';
|
||||
|
||||
private static $db = array(
|
||||
'Rating' => 'Int',
|
||||
'SecretField' => 'Text',
|
||||
'WriteProtectedField' => 'Text',
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Author' => RestfulServerTestAuthor::class,
|
||||
'SecretRelation' => RestfulServerTestAuthor::class,
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canCreate($member = null, $context = array())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
61
tests/Stubs/RestfulServerTestComment.php
Normal file
61
tests/Stubs/RestfulServerTestComment.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Security\PermissionProvider;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
/**
|
||||
* Everybody can view comments, logged in members in the "users" group can create comments,
|
||||
* but only "editors" can edit or delete them.
|
||||
*
|
||||
*/
|
||||
class RestfulServerTestComment extends DataObject implements PermissionProvider, TestOnly
|
||||
{
|
||||
private static $api_access = true;
|
||||
|
||||
private static $table_name = 'RestfulServerTestComment';
|
||||
|
||||
private static $db = array(
|
||||
"Name" => "Varchar(255)",
|
||||
"Comment" => "Text"
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Page' => RestfulServerTestPage::class,
|
||||
'Author' => RestfulServerTestAuthor::class,
|
||||
);
|
||||
|
||||
public function providePermissions()
|
||||
{
|
||||
return array(
|
||||
'EDIT_Comment' => 'Edit Comment Objects',
|
||||
'CREATE_Comment' => 'Create Comment Objects',
|
||||
'DELETE_Comment' => 'Delete Comment Objects',
|
||||
);
|
||||
}
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'EDIT_Comment');
|
||||
}
|
||||
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'DELETE_Comment');
|
||||
}
|
||||
|
||||
public function canCreate($member = null, $context = array())
|
||||
{
|
||||
return Permission::checkMember($member, 'CREATE_Comment');
|
||||
}
|
||||
}
|
32
tests/Stubs/RestfulServerTestPage.php
Normal file
32
tests/Stubs/RestfulServerTestPage.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
|
||||
class RestfulServerTestPage extends DataObject implements TestOnly
|
||||
{
|
||||
private static $api_access = false;
|
||||
|
||||
private static $table_name = 'RestfulServerTestPage';
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Text',
|
||||
'Content' => 'HTMLText',
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Author' => RestfulServerTestAuthor::class,
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'TestComments' => RestfulServerTestComment::class
|
||||
);
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'RelatedAuthors' => RestfulServerTestAuthor::class,
|
||||
);
|
||||
}
|
31
tests/Stubs/RestfulServerTestSecretThing.php
Normal file
31
tests/Stubs/RestfulServerTestSecretThing.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||
|
||||
use SilverStripe\Security\Permission;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Security\PermissionProvider;
|
||||
|
||||
class RestfulServerTestSecretThing extends DataObject implements TestOnly, PermissionProvider
|
||||
{
|
||||
private static $api_access = true;
|
||||
|
||||
private static $table_name = 'RestfulServerTestSecretThing';
|
||||
|
||||
private static $db = array(
|
||||
"Name" => "Varchar(255)",
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'VIEW_SecretThing');
|
||||
}
|
||||
|
||||
public function providePermissions()
|
||||
{
|
||||
return array(
|
||||
'VIEW_SecretThing' => 'View Secret Things',
|
||||
);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="SilverStripe">
|
||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||
|
||||
<!-- exclude SCSS-generated CSS files -->
|
||||
<exclude-pattern>*/css/*</exclude-pattern>
|
||||
<exclude-pattern>css/*</exclude-pattern>
|
||||
|
||||
<!-- exclude thirdparty content -->
|
||||
<exclude-pattern>thirdparty/*</exclude-pattern>
|
||||
|
||||
<rule ref="Generic.Files.LineEndings.InvalidEOLChar">
|
||||
<severity>8</severity>
|
||||
</rule>
|
||||
<rule ref="Generic.Files.LineEndings">
|
||||
<properties>
|
||||
<property name="eolChar" value="\n" />
|
||||
</properties>
|
||||
</rule>
|
||||
<rule ref="Generic.Files.LineLength.TooLong">
|
||||
<severity>7</severity>
|
||||
</rule>
|
||||
<rule ref="Generic.Files.LineLength.MaxExceeded">
|
||||
<severity>8</severity>
|
||||
</rule>
|
||||
<rule ref="Generic.Files.LineLength">
|
||||
<properties>
|
||||
<property name="lineLimit" value="120"/>
|
||||
<property name="absoluteLineLimit" value="120"/>
|
||||
</properties>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
global $project;
|
||||
$project = 'mysite';
|
||||
|
||||
global $database;
|
||||
$database = '';
|
||||
|
||||
require_once('conf/ConfigureFromEnv.php');
|
||||
|
||||
global $databaseConfig;
|
||||
$databaseConfig['memory'] = true;
|
||||
$databaseConfig['path'] = dirname(dirname(__FILE__)) .'/assets/';
|
||||
|
||||
MySQLDatabase::set_connection_charset('utf8');
|
||||
|
||||
// Set the current theme. More themes can be downloaded from
|
||||
// http://www.silverstripe.org/themes/
|
||||
SSViewer::set_theme('simple');
|
||||
|
||||
// Enable nested URLs for this site (e.g. page/sub-page/)
|
||||
if (class_exists('SiteTree')) {
|
||||
SiteTree::enable_nested_urls();
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
ob_start();
|
||||
|
||||
define('SS_ENVIRONMENT_TYPE', 'dev');
|
||||
|
||||
/* Database connection */
|
||||
$db = getenv('TESTDB');
|
||||
switch ($db) {
|
||||
case "PGSQL";
|
||||
define('SS_DATABASE_CLASS', 'PostgreSQLDatabase');
|
||||
define('SS_DATABASE_USERNAME', 'postgres');
|
||||
define('SS_DATABASE_PASSWORD', '');
|
||||
break;
|
||||
|
||||
case "MYSQL":
|
||||
define('SS_DATABASE_CLASS', 'MySQLDatabase');
|
||||
define('SS_DATABASE_USERNAME', 'root');
|
||||
define('SS_DATABASE_PASSWORD', '');
|
||||
break;
|
||||
|
||||
default:
|
||||
define('SS_DATABASE_CLASS', 'SQLitePDODatabase');
|
||||
define('SS_DATABASE_USERNAME', 'root');
|
||||
define('SS_DATABASE_PASSWORD', '');
|
||||
}
|
||||
|
||||
echo SS_DATABASE_CLASS;
|
||||
|
||||
define('SS_DATABASE_SERVER', 'localhost');
|
||||
define('SS_DATABASE_CHOOSE_NAME', true);
|
||||
|
||||
|
||||
/* Configure a default username and password to access the CMS on all sites in this environment. */
|
||||
define('SS_DEFAULT_ADMIN_USERNAME', 'username');
|
||||
define('SS_DEFAULT_ADMIN_PASSWORD', 'password');
|
||||
|
||||
$_FILE_TO_URL_MAPPING[dirname(__FILE__)] = 'http://localhost';
|
@ -1,23 +0,0 @@
|
||||
### USAGE: before_script <base-folder>
|
||||
|
||||
BUILD_DIR=$1
|
||||
|
||||
# Fetch all dependencies
|
||||
# TODO Replace with different composer.json variations
|
||||
echo "Checking out installer@$CORE_RELEASE"
|
||||
git clone --depth=100 --quiet --branch $CORE_RELEASE git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
|
||||
echo "Checking out framework@$CORE_RELEASE"
|
||||
git clone --depth=100 --quiet --branch $CORE_RELEASE git://github.com/silverstripe/sapphire.git $BUILD_DIR/framework
|
||||
echo "Checking out sqlite3"
|
||||
git clone --depth=100 --quiet git://github.com/silverstripe-labs/silverstripe-sqlite3.git $BUILD_DIR/sqlite3
|
||||
echo "Checking out postgresql"
|
||||
git clone --depth=100 --quiet git://github.com/silverstripe/silverstripe-postgresql.git $BUILD_DIR/postgresql
|
||||
|
||||
# Copy setup files
|
||||
cp ./tests/travis/_ss_environment.php $BUILD_DIR
|
||||
cp ./tests/travis/_config.php $BUILD_DIR/mysite
|
||||
|
||||
# Copy actual project code into build directory (checked out by travis)
|
||||
cp -r . $BUILD_DIR/restfulserver
|
||||
|
||||
cd $BUILD_DIR
|
@ -1,651 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
*
|
||||
* @todo Test Relation getters
|
||||
* @todo Test filter and limit through GET params
|
||||
* @todo Test DELETE verb
|
||||
*
|
||||
*/
|
||||
class RestfulServerTest extends SapphireTest
|
||||
{
|
||||
public static $fixture_file = 'RestfulServerTest.yml';
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'RestfulServerTest_Comment',
|
||||
'RestfulServerTest_SecretThing',
|
||||
'RestfulServerTest_Page',
|
||||
'RestfulServerTest_Author',
|
||||
'RestfulServerTest_AuthorRating',
|
||||
);
|
||||
|
||||
public function testApiAccess()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
$page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1');
|
||||
|
||||
// normal GET should succeed with $api_access enabled
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
// even with logged in user a GET with $api_access disabled should fail
|
||||
$url = "/api/v1/RestfulServerTest_Page/" . $page1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 401);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testApiAccessBoolean()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>', $response->getBody());
|
||||
$this->assertContains('<Name>', $response->getBody());
|
||||
$this->assertContains('<Comment>', $response->getBody());
|
||||
$this->assertContains('<Page', $response->getBody());
|
||||
$this->assertContains('<Author', $response->getBody());
|
||||
}
|
||||
|
||||
public function testAuthenticatedGET()
|
||||
{
|
||||
$thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1');
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
// @todo create additional mock object with authenticated VIEW permissions
|
||||
$url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 401);
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testAuthenticatedPUT()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$data = array('Comment' => 'created');
|
||||
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
$this->assertEquals($response->getStatusCode(), 401); // Permission failure
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testGETRelationshipsXML()
|
||||
{
|
||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
||||
$rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2');
|
||||
|
||||
// @todo should be set up by fixtures, doesn't work for some reason...
|
||||
$author1->Ratings()->add($rating1);
|
||||
$author1->Ratings()->add($rating2);
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating'];
|
||||
$this->assertEquals(count($ratingsArr), 2);
|
||||
$ratingIDs = array(
|
||||
(int)$ratingsArr[0]['@attributes']['id'],
|
||||
(int)$ratingsArr[1]['@attributes']['id']
|
||||
);
|
||||
$this->assertContains($rating1->ID, $ratingIDs);
|
||||
$this->assertContains($rating2->ID, $ratingIDs);
|
||||
}
|
||||
|
||||
public function testGETManyManyRelationshipsXML()
|
||||
{
|
||||
// author4 has related authors author2 and author3
|
||||
$author2 = $this->objFromFixture('RestfulServerTest_Author', 'author2');
|
||||
$author3 = $this->objFromFixture('RestfulServerTest_Author', 'author3');
|
||||
$author4 = $this->objFromFixture('RestfulServerTest_Author', 'author4');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$arr = Convert::xml2array($response->getBody());
|
||||
$authorsArr = $arr['RestfulServerTest_Author'];
|
||||
|
||||
$this->assertEquals(count($authorsArr), 2);
|
||||
$ratingIDs = array(
|
||||
(int)$authorsArr[0]['ID'],
|
||||
(int)$authorsArr[1]['ID']
|
||||
);
|
||||
$this->assertContains($author2->ID, $ratingIDs);
|
||||
$this->assertContains($author3->ID, $ratingIDs);
|
||||
}
|
||||
|
||||
public function testPUTWithFormEncoded()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$body = 'Name=Updated Comment&Comment=updated';
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
);
|
||||
$response = Director::test($url, null, null, 'PUT', $body, $headers);
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($responseArr['ID'], $comment1->ID);
|
||||
$this->assertEquals($responseArr['Comment'], 'updated');
|
||||
$this->assertEquals($responseArr['Name'], 'Updated Comment');
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPOSTWithFormEncoded()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment";
|
||||
$body = 'Name=New Comment&Comment=created';
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
);
|
||||
$response = Director::test($url, null, null, 'POST', $body, $headers);
|
||||
$this->assertEquals($response->getStatusCode(), 201); // Created
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertTrue($responseArr['ID'] > 0);
|
||||
$this->assertNotEquals($responseArr['ID'], $comment1->ID);
|
||||
$this->assertEquals($responseArr['Comment'], 'created');
|
||||
$this->assertEquals($responseArr['Name'], 'New Comment');
|
||||
$this->assertEquals(
|
||||
$response->getHeader('Location'),
|
||||
Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID'])
|
||||
);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPostWithoutBodyReturnsNoContent()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
$url = '/api/v1/RestfulServerTest_Comment';
|
||||
$response = Director::test($url, null, null, 'POST');
|
||||
|
||||
$this->assertEquals('No Content', $response->getBody());
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPUTwithJSON()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
// by mimetype
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$body = '{"Comment":"updated"}';
|
||||
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'application/json'));
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($obj->ID, $comment1->ID);
|
||||
$this->assertEquals($obj->Comment, 'updated');
|
||||
|
||||
// by extension
|
||||
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID);
|
||||
$body = '{"Comment":"updated"}';
|
||||
$response = Director::test($url, null, null, 'PUT', $body);
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
||||
$this->assertEquals(
|
||||
$response->getHeader('Location'),
|
||||
Controller::join_links(Director::absoluteBaseURL(), $url)
|
||||
);
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($obj->ID, $comment1->ID);
|
||||
$this->assertEquals($obj->Comment, 'updated');
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testPUTwithXML()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
|
||||
// by mimetype
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
|
||||
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml'));
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
||||
$obj = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($obj['ID'], $comment1->ID);
|
||||
$this->assertEquals($obj['Comment'], 'updated');
|
||||
|
||||
// by extension
|
||||
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID);
|
||||
$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
|
||||
$response = Director::test($url, null, null, 'PUT', $body);
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
||||
$this->assertEquals(
|
||||
$response->getHeader('Location'),
|
||||
Controller::join_links(Director::absoluteBaseURL(), $url)
|
||||
);
|
||||
$obj = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($obj['ID'], $comment1->ID);
|
||||
$this->assertEquals($obj['Comment'], 'updated');
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testHTTPAcceptAndContentType()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
|
||||
$headers = array('Accept' => 'application/json');
|
||||
$response = Director::test($url, null, null, 'GET', null, $headers);
|
||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
||||
$obj = Convert::json2obj($response->getBody());
|
||||
$this->assertEquals($obj->ID, $comment1->ID);
|
||||
$this->assertEquals($response->getHeader('Content-Type'), 'application/json');
|
||||
}
|
||||
|
||||
public function testNotFound()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/99";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 404);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testMethodNotAllowed()
|
||||
{
|
||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
||||
$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
|
||||
$this->assertEquals($response->getStatusCode(), 405);
|
||||
}
|
||||
|
||||
public function testConflictOnExistingResourceWhenUsingPost()
|
||||
{
|
||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'POST');
|
||||
$this->assertEquals($response->getStatusCode(), 409);
|
||||
}
|
||||
|
||||
public function testUnsupportedMediaType()
|
||||
{
|
||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Comment";
|
||||
$data = "Comment||\/||updated"; // weird format
|
||||
$headers = array('Content-Type' => 'text/weirdformat');
|
||||
$response = Director::test($url, null, null, 'POST', $data, $headers);
|
||||
$this->assertEquals($response->getStatusCode(), 415);
|
||||
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
|
||||
public function testXMLValueFormatting()
|
||||
{
|
||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>' . $rating1->ID . '</ID>', $response->getBody());
|
||||
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
||||
}
|
||||
|
||||
public function testApiAccessFieldRestrictions()
|
||||
{
|
||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<ID>', $response->getBody());
|
||||
$this->assertContains('<Rating>', $response->getBody());
|
||||
$this->assertContains('<Author', $response->getBody());
|
||||
$this->assertNotContains('<SecretField>', $response->getBody());
|
||||
$this->assertNotContains('<SecretRelation>', $response->getBody());
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
||||
'"add_fields" URL parameter filters out disallowed fields from $api_access'
|
||||
);
|
||||
$this->assertNotContains('<SecretRelation>', $response->getBody(),
|
||||
'"add_fields" URL parameter filters out disallowed relations from $api_access'
|
||||
);
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?fields=SecretField,SecretRelation';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
||||
'"fields" URL parameter filters out disallowed fields from $api_access'
|
||||
);
|
||||
$this->assertNotContains('<SecretRelation>', $response->getBody(),
|
||||
'"fields" URL parameter filters out disallowed relations from $api_access'
|
||||
);
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings';
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertContains('<Rating>', $response->getBody(),
|
||||
'Relation viewer shows fields allowed through $api_access'
|
||||
);
|
||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
||||
'Relation viewer on has-many filters out disallowed fields from $api_access'
|
||||
);
|
||||
}
|
||||
|
||||
public function testApiAccessRelationRestrictionsInline()
|
||||
{
|
||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
|
||||
$this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
|
||||
}
|
||||
|
||||
public function testApiAccessRelationRestrictionsOnEndpoint()
|
||||
{
|
||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
|
||||
}
|
||||
|
||||
public function testApiAccessWithPUT()
|
||||
{
|
||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
||||
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
||||
$data = array(
|
||||
'Rating' => '42',
|
||||
'WriteProtectedField' => 'haxx0red'
|
||||
);
|
||||
$response = Director::test($url, $data, null, 'PUT');
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($responseArr['Rating'], 42);
|
||||
$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
|
||||
}
|
||||
|
||||
public function testJSONDataFormatter()
|
||||
{
|
||||
$formatter = new JSONDataFormatter();
|
||||
$editor = $this->objFromFixture('Member', 'editor');
|
||||
$user = $this->objFromFixture('Member', 'user');
|
||||
|
||||
$this->assertEquals(
|
||||
$formatter->convertDataObject($editor, array("FirstName", "Email")),
|
||||
'{"FirstName":"Editor","Email":"editor@test.com"}',
|
||||
"Correct JSON formatting with field subset");
|
||||
|
||||
$set = DataObject::get(
|
||||
"Member",
|
||||
sprintf('"Member"."ID" IN (%s)', implode(',', array($editor->ID, $user->ID))),
|
||||
'"Email" ASC' // for sorting for postgres
|
||||
);
|
||||
$this->assertEquals(
|
||||
$formatter->convertDataObjectSet($set, array("FirstName", "Email")),
|
||||
'{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' .
|
||||
'{"FirstName":"User","Email":"user@test.com"}]}',
|
||||
"Correct JSON formatting on a dataobjectset with field filter");
|
||||
}
|
||||
|
||||
public function testApiAccessWithPOST()
|
||||
{
|
||||
$url = "/api/v1/RestfulServerTest_AuthorRating";
|
||||
$data = array(
|
||||
'Rating' => '42',
|
||||
'WriteProtectedField' => 'haxx0red'
|
||||
);
|
||||
$response = Director::test($url, $data, null, 'POST');
|
||||
// Assumption: XML is default output
|
||||
$responseArr = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals($responseArr['Rating'], 42);
|
||||
$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
|
||||
}
|
||||
|
||||
public function testCanViewRespectedInList()
|
||||
{
|
||||
// Default content type
|
||||
$url = "/api/v1/RestfulServerTest_SecretThing/";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||
|
||||
// JSON content type
|
||||
$url = "/api/v1/RestfulServerTest_SecretThing.json";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||
$responseArray = Convert::json2array($response->getBody());
|
||||
$this->assertSame(0, $responseArray['totalSize']);
|
||||
|
||||
// With authentication
|
||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||
$url = "/api/v1/RestfulServerTest_SecretThing/";
|
||||
$response = Director::test($url, null, null, 'GET');
|
||||
$this->assertEquals($response->getStatusCode(), 200);
|
||||
$this->assertContains('Unspeakable', $response->getBody());
|
||||
// Assumption: default formatter is XML
|
||||
$responseArray = Convert::xml2array($response->getBody());
|
||||
$this->assertEquals(1, $responseArray['@attributes']['totalSize']);
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Everybody can view comments, logged in members in the "users" group can create comments,
|
||||
* but only "editors" can edit or delete them.
|
||||
*
|
||||
*/
|
||||
class RestfulServerTest_Comment extends DataObject implements PermissionProvider,TestOnly
|
||||
{
|
||||
public static $api_access = true;
|
||||
|
||||
public static $db = array(
|
||||
"Name" => "Varchar(255)",
|
||||
"Comment" => "Text"
|
||||
);
|
||||
|
||||
public static $has_one = array(
|
||||
'Page' => 'RestfulServerTest_Page',
|
||||
'Author' => 'RestfulServerTest_Author',
|
||||
);
|
||||
|
||||
public function providePermissions()
|
||||
{
|
||||
return array(
|
||||
'EDIT_Comment' => 'Edit Comment Objects',
|
||||
'CREATE_Comment' => 'Create Comment Objects',
|
||||
'DELETE_Comment' => 'Delete Comment Objects',
|
||||
);
|
||||
}
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'EDIT_Comment');
|
||||
}
|
||||
|
||||
public function canDelete($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'DELETE_Comment');
|
||||
}
|
||||
|
||||
public function canCreate($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'CREATE_Comment');
|
||||
}
|
||||
}
|
||||
|
||||
class RestfulServerTest_SecretThing extends DataObject implements TestOnly,PermissionProvider
|
||||
{
|
||||
public static $api_access = true;
|
||||
|
||||
public static $db = array(
|
||||
"Name" => "Varchar(255)",
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return Permission::checkMember($member, 'VIEW_SecretThing');
|
||||
}
|
||||
|
||||
public function providePermissions()
|
||||
{
|
||||
return array(
|
||||
'VIEW_SecretThing' => 'View Secret Things',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestfulServerTest_Page extends DataObject implements TestOnly
|
||||
{
|
||||
public static $api_access = false;
|
||||
|
||||
public static $db = array(
|
||||
'Title' => 'Text',
|
||||
'Content' => 'HTMLText',
|
||||
);
|
||||
|
||||
public static $has_one = array(
|
||||
'Author' => 'RestfulServerTest_Author',
|
||||
);
|
||||
|
||||
public static $has_many = array(
|
||||
'TestComments' => 'RestfulServerTest_Comment'
|
||||
);
|
||||
|
||||
public static $belongs_many_many = array(
|
||||
'RelatedAuthors' => 'RestfulServerTest_Author',
|
||||
);
|
||||
}
|
||||
|
||||
class RestfulServerTest_Author extends DataObject implements TestOnly
|
||||
{
|
||||
public static $api_access = true;
|
||||
|
||||
public static $db = array(
|
||||
'Name' => 'Text',
|
||||
);
|
||||
|
||||
public static $many_many = array(
|
||||
'RelatedPages' => 'RestfulServerTest_Page',
|
||||
'RelatedAuthors' => 'RestfulServerTest_Author',
|
||||
);
|
||||
|
||||
public static $has_many = array(
|
||||
'PublishedPages' => 'RestfulServerTest_Page',
|
||||
'Ratings' => 'RestfulServerTest_AuthorRating',
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class RestfulServerTest_AuthorRating extends DataObject implements TestOnly
|
||||
{
|
||||
public static $api_access = array(
|
||||
'view' => array(
|
||||
'Rating',
|
||||
'WriteProtectedField',
|
||||
'Author'
|
||||
),
|
||||
'edit' => array(
|
||||
'Rating'
|
||||
)
|
||||
);
|
||||
|
||||
public static $db = array(
|
||||
'Rating' => 'Int',
|
||||
'SecretField' => 'Text',
|
||||
'WriteProtectedField' => 'Text',
|
||||
);
|
||||
|
||||
public static $has_one = array(
|
||||
'Author' => 'RestfulServerTest_Author',
|
||||
'SecretRelation' => 'RestfulServerTest_Author',
|
||||
);
|
||||
|
||||
public function canView($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canEdit($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canCreate($member = null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
Member:
|
||||
editor:
|
||||
FirstName: Editor
|
||||
Email: editor@test.com
|
||||
Password: editor
|
||||
user:
|
||||
FirstName: User
|
||||
Email: user@test.com
|
||||
Password: user
|
||||
Group:
|
||||
editorgroup:
|
||||
Title: Editors
|
||||
Code: editors
|
||||
Members: =>Member.editor
|
||||
usergroup:
|
||||
Title: Users
|
||||
Code: users
|
||||
Members: =>Member.user
|
||||
Permission:
|
||||
perm1:
|
||||
Code: CREATE_Comment
|
||||
Group: =>Group.usergroup
|
||||
perm3:
|
||||
Code: EDIT_Comment
|
||||
Group: =>Group.editorgroup
|
||||
perm4:
|
||||
Code: DELETE_Comment
|
||||
Group: =>Group.editorgroup
|
||||
perm5:
|
||||
Code: CREATE_Comment
|
||||
Group: =>Group.editorgroup
|
||||
perm6:
|
||||
Code: VIEW_SecretThing
|
||||
Group: =>Group.editorgroup
|
||||
RestfulServerTest_Page:
|
||||
page1:
|
||||
Title: Testpage without API Access
|
||||
RestfulServerTest_Comment:
|
||||
comment1:
|
||||
Name: Joe
|
||||
Comment: This is a test comment
|
||||
Page: =>RestfulServerTest_Page.page1
|
||||
RestfulServerTest_Author:
|
||||
author1:
|
||||
FirstName: Author 1
|
||||
author2:
|
||||
FirstName: Author 2
|
||||
author3:
|
||||
Firstname: Author 3
|
||||
author4:
|
||||
FirstName: Author 4
|
||||
RelatedAuthors: =>RestfulServerTest_Author.author2,=>RestfulServerTest_Author.author3
|
||||
RestfulServerTest_AuthorRating:
|
||||
rating1:
|
||||
Rating: 3
|
||||
WriteProtectedField: Dont overwrite me
|
||||
SecretField: Dont look at me!
|
||||
Author: =>RestfulServerTest_Author.author1
|
||||
SecretRelation: =>RestfulServerTest_Author.author1
|
||||
rating2:
|
||||
Rating: 5
|
||||
Author: =>RestfulServerTest_Author.author1
|
||||
SecretRelation: =>RestfulServerTest_Author.author1
|
||||
RestfulServerTest_SecretThing:
|
||||
thing1:
|
||||
Name: Unspeakable
|
Loading…
Reference in New Issue
Block a user