Upgrade: begin SilverStripe 4 compatiblity update

This commit is contained in:
Dylan Wagstaff 2017-11-29 15:20:09 +13:00
parent e5a757d589
commit 6601b42c4b
35 changed files with 1924 additions and 1009 deletions

View File

@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[{*.yml,package.json}] [{*.yml,package.json,*.js}]
indent_size = 2 indent_size = 2
# The indent size used in the package.json file cannot be changed: # The indent size used in the package.json file cannot be changed:

1
.gitattributes vendored
View File

@ -4,3 +4,4 @@
/.gitignore export-ignore /.gitignore export-ignore
/.travis.yml export-ignore /.travis.yml export-ignore
/.scrutinizer.yml export-ignore /.scrutinizer.yml export-ignore
/codecov.yml export-ignore

View File

@ -1,9 +1,15 @@
inherit: true inherit: true
build:
nodes:
analysis:
tests:
override: [php-scrutinizer-run]
checks: checks:
php: php:
code_rating: true code_rating: true
duplication: true duplication: true
filter: filter:
paths: [code/*, tests/*] paths: [src/*, tests/*]

View File

@ -1,28 +1,33 @@
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
sudo: false
language: php language: php
env:
global:
- COMPOSER_ROOT_VERSION=2.0.x-dev
matrix: matrix:
include: include:
- php: 5.4
env: DB=MYSQL CORE_RELEASE=3.3
- php: 5.5
env: DB=MYSQL CORE_RELEASE=3.4
- php: 5.6 - php: 5.6
env: DB=PGSQL CORE_RELEASE=3.5 env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
- php: 7.0 - php: 7.0
env: DB=MYSQL CORE_RELEASE=3 env: DB=MYSQL PHPUNIT_TEST=1
- php: 7.1 - php: 7.1
env: DB=MYSQL CORE_RELEASE=3.6 env: DB=PGSQL PHPUNIT_COVERAGE_TEST=1
before_script: before_script:
- composer self-update || true # Init PHP
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support - phpenv rehash
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss - phpenv config-rm xdebug.ini
- cd ~/builds/ss
- composer install # 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: 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
View 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

View File

@ -1,39 +1,63 @@
# SilverStripe RestfulServer Module # 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 ## Overview
This class gives your application a RESTful API. All you have to do is define static $api_access = true on This class gives your application a RESTful API. All you have to do is set the `api_access` configuration option to `true`
the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in 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 your model layer (ie, the DataObject classes) and not in your Controllers. This is the recommended design for SilverStripe
applications. applications.
## Requirements ## 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 ## 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. unless explicitly controlled through model permissions.
class Article extends DataObject { ```php
static $db = array('Title'=>'Text','Published'=>'Boolean'); namespace Vendor\Project;
static $api_access = true;
}
Example DataObject with advanced api access, limiting viewing and editing to Title attribute only: use SilverStripe\ORM\DataObject;
class Article extends DataObject { class Article extends DataObject {
static $db = array('Title'=>'Text','Published'=>'Boolean');
static $api_access = array( private static $db = [
'view' => array('Title'), 'Title'=>'Text',
'edit' => array('Title'), '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 ## Supported operations
@ -66,7 +90,7 @@ to the url, e.g. /api/v1/(ClassName)/?Title=mytitle.
## Access control ## 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. 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 You should override the following built-in methods to customize permission control on a
class- and object-level: class- and object-level:
@ -76,7 +100,7 @@ class- and object-level:
- `DataObject::canDelete()` - `DataObject::canDelete()`
- `DataObject::canCreate()` - `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. 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.

View File

View File

@ -1,10 +1,6 @@
--- ---
Name: restfulserverroutes Name: restfulserverroutes
After:
- '#rootroutes'
- '#modelascontrollerroutes'
--- ---
Director: SilverStripe\Control\Director:
rules: rules:
'api/v1/live': 'VersionedRestfulServer' 'api/v1': 'SilverStripe\RestfulServer\RestfulServer'
'api/v1': 'RestfulServer'

View File

@ -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
View File

@ -0,0 +1 @@
comment: false

View File

@ -1,12 +1,9 @@
{ {
"name": "silverstripe/restfulserver", "name": "silverstripe/restfulserver",
"description": "Add a RESTful API to your SilverStripe application", "description": "Add a RESTful API to your SilverStripe application",
"type": "silverstripe-module", "type": "silverstripe-vendormodule",
"keywords": [ "keywords": ["silverstripe", "rest", "api"],
"silverstripe", "license": "BSD-3-Clause",
"rest",
"api"
],
"authors": [ "authors": [
{ {
"name": "Hamish Friedlander", "name": "Hamish Friedlander",
@ -18,12 +15,23 @@
} }
], ],
"require": { "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": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.1.x-dev" "dev-master": "2.x-dev"
} }
}, },
"license": "BSD-3-Clause" "prefer-stable": true,
"minimum-stability": "dev"
} }

View File

@ -1,4 +1,4 @@
Copyright (c) 2016, SilverStripe Limited Copyright (c) 2017, SilverStripe Limited
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 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
View 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
View 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>

View File

@ -1,5 +1,12 @@
<?php <?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. * A simple authenticator for the Restful server.
* *
@ -19,23 +26,25 @@ class BasicRestfulAuthenticator
*/ */
public static function authenticate() 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'])) { 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 // With a valid user and password, check the password is correct
$authClass = Authenticator::get_default_authenticator(); $data = [
$member = $authClass::authenticate(array(
'Email' => $_SERVER['PHP_AUTH_USER'], 'Email' => $_SERVER['PHP_AUTH_USER'],
'Password' => $_SERVER['PHP_AUTH_PW'], 'Password' => $_SERVER['PHP_AUTH_PW'],
)); ];
$request = Controller::curr()->getRequest();
//Log the member in and return the member, if they were found $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
$member = null;
foreach ($authenticators as $authenticator) {
$member = $authenticator->authenticate($data, $request);
if ($member) { if ($member) {
$member->LogIn(false); break;
}
}
return $member; return $member;
} }
return false;
}
} }

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -1,10 +1,26 @@
<?php <?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. * Generic RESTful server, which handles webservice access to arbitrary DataObjects.
* Relies on serialization/deserialization into different formats provided * Relies on serialization/deserialization into different formats provided
* by the DataFormatter APIs in core. * 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 Implement PUT/POST/DELETE for relations
* @todo Access-Control for relations (you might be allowed to view Members and Groups, * @todo Access-Control for relations (you might be allowed to view Members and Groups,
* but not their relation with each other) * but not their relation with each other)
@ -28,15 +44,16 @@
*/ */
class RestfulServer extends Controller class RestfulServer extends Controller
{ {
public static $url_handlers = array( private static $url_handlers = array(
'$ClassName/$ID/$Relation' => 'handleAction' '$ClassName!/$ID/$Relation' => 'handleAction',
'' => 'notFound'
#'$ClassName/#ID' => 'handleItem', #'$ClassName/#ID' => 'handleItem',
#'$ClassName' => 'handleList', #'$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 * If no extension is given in the request, resolve to this extension
@ -44,7 +61,7 @@ class RestfulServer extends Controller
* *
* @var string * @var string
*/ */
public static $default_extension = "xml"; private static $default_extension = "xml";
/** /**
* If no extension is given, resolve the request to this mimetype. * If no extension is given, resolve the request to this mimetype.
@ -59,17 +76,18 @@ class RestfulServer extends Controller
*/ */
protected $member; protected $member;
public static $allowed_actions = array( private static $allowed_actions = array(
'index' 'index',
'notFound'
); );
/* /*
function handleItem($request) { 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) { 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. * 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) * 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')) { if (class_exists(SiteTree::class)) {
singleton('SiteTree')->extend('modelascontrollerInit', $this); singleton(SiteTree::class)->extend('modelascontrollerInit', $this);
} }
parent::init(); 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. * This handler acts as the switchboard for the controller.
* Since no $Action url-param is set, all requests are sent here. * 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'])) { $className = $this->unsanitiseClassName($request->param('ClassName'));
return $this->notFound(); $id = $request->param('ID') ?: null;
} $relation = $request->param('Relation') ?: null;
$className = $this->urlParams['ClassName'];
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
// Check input formats // Check input formats
if (!class_exists($className)) { if (!class_exists($className)) {
@ -105,8 +146,7 @@ class RestfulServer extends Controller
if ($id && !is_numeric($id)) { if ($id && !is_numeric($id)) {
return $this->notFound(); return $this->notFound();
} }
if ( if ($relation
$relation
&& !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation) && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
) { ) {
return $this->notFound(); return $this->notFound();
@ -259,8 +299,12 @@ class RestfulServer extends Controller
* @param array $params * @param array $params
* @return SS_List * @return SS_List
*/ */
protected function getSearchQuery($className, $params = null, $sort = null, protected function getSearchQuery(
$limit = null, $existingQuery = null $className,
$params = null,
$sort = null,
$limit = null,
$existingQuery = null
) { ) {
if (singleton($className)->hasMethod('getRestfulSearchContext')) { if (singleton($className)->hasMethod('getRestfulSearchContext')) {
$searchContext = singleton($className)->{'getRestfulSearchContext'}(); $searchContext = singleton($className)->{'getRestfulSearchContext'}();
@ -287,7 +331,7 @@ class RestfulServer extends Controller
$accept = $this->request->getHeader('Accept'); $accept = $this->request->getHeader('Accept');
$mimetypes = $this->request->getAcceptMimetypes(); $mimetypes = $this->request->getAcceptMimetypes();
if (!$className) { if (!$className) {
$className = $this->urlParams['ClassName']; $className = $this->unsanitiseClassName($this->request->param('ClassName'));
} }
// get formatter // get formatter
@ -393,6 +437,7 @@ class RestfulServer extends Controller
if (!$obj) { if (!$obj) {
return $this->notFound(); return $this->notFound();
} }
if (!$obj->canEdit($this->getMember())) { if (!$obj->canEdit($this->getMember())) {
return $this->permissionFailure(); return $this->permissionFailure();
} }
@ -424,7 +469,8 @@ class RestfulServer extends Controller
$type = ".{$types[0]}"; $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); $this->getResponse()->addHeader('Location', $objHref);
return $responseFormatter->convertDataObject($obj); return $responseFormatter->convertDataObject($obj);
@ -493,7 +539,8 @@ class RestfulServer extends Controller
$type = ".{$types[0]}"; $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); $this->getResponse()->addHeader('Location', $objHref);
return $responseFormatter->convertDataObject($obj); return $responseFormatter->convertDataObject($obj);
@ -529,7 +576,8 @@ class RestfulServer extends Controller
// @todo Disallow editing of certain keys in database // @todo Disallow editing of certain keys in database
$data = array_diff_key($data, array('ID', 'Created')); $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'])) { if (is_array($apiAccess) && isset($apiAccess['edit'])) {
$data = array_intersect_key($data, array_combine($apiAccess['edit'], $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 // The relation method will return a DataList, that getSearchQuery subsequently manipulates
if ($obj->hasMethod($relationName)) { 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'; $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 { } else {
$list = $obj->$relationName(); $list = $obj->$relationName();
} }
@ -633,8 +685,10 @@ class RestfulServer extends Controller
*/ */
protected function authenticate() protected function authenticate()
{ {
$authClass = self::config()->authenticator; $authClass = $this->config()->authenticator;
return $authClass::authenticate(); $member = $authClass::authenticate();
Security::setCurrentUser($member);
return $member;
} }
/** /**
@ -649,9 +703,12 @@ class RestfulServer extends Controller
{ {
$allowedRelations = array(); $allowedRelations = array();
$obj = singleton($class); $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) { if ($relations) {
foreach ($relations as $relName => $relClass) { foreach ($relations as $relName => $relClass) {
//remove dot notation from relation names
$parts = explode('.', $relClass);
$relClass = array_shift($parts);
if (singleton($relClass)->stat('api_access')) { if (singleton($relClass)->stat('api_access')) {
$allowedRelations[] = $relName; $allowedRelations[] = $relName;
} }
@ -667,59 +724,6 @@ class RestfulServer extends Controller
*/ */
protected function getMember() protected function getMember()
{ {
return Member::currentUser(); return Security::getCurrentUser();
}
}
/**
* 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);
}
} }
} }

35
src/RestfulServerItem.php Normal file
View 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
View 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
View 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']);
}
}

View 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

View 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;
}
}

View 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;
}
}

View 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');
}
}

View 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,
);
}

View 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',
);
}
}

View File

@ -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>

View File

@ -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();
}

View File

@ -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';

View File

@ -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

View File

@ -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;
}
}

View File

@ -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