Merge pull request #42 from creative-commoners/pulls/2.0/towards-two

Upgrade: begin SilverStripe 4 compatiblity update
This commit is contained in:
Robbie Averill 2017-12-06 13:10:05 +13:00 committed by GitHub
commit 47ec185c12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1591 additions and 691 deletions

View File

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

1
.gitattributes vendored
View File

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

View File

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

View File

@ -1,28 +1,33 @@
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
sudo: false
language: php
env:
global:
- COMPOSER_ROOT_VERSION=2.0.x-dev
matrix:
include:
- php: 5.4
env: DB=MYSQL CORE_RELEASE=3.3
- php: 5.5
env: DB=MYSQL CORE_RELEASE=3.4
- php: 5.6
env: DB=PGSQL CORE_RELEASE=3.5
env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
- php: 7.0
env: DB=MYSQL CORE_RELEASE=3
env: DB=MYSQL PHPUNIT_TEST=1
- php: 7.1
env: DB=MYSQL CORE_RELEASE=3.6
env: DB=PGSQL PHPUNIT_COVERAGE_TEST=1
before_script:
- composer self-update || true
- git clone git://github.com/silverstripe/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
- composer install
# Init PHP
- phpenv rehash
- phpenv config-rm xdebug.ini
# Install composer dependencies
- composer validate
- composer require --no-update silverstripe/recipe-core:1.0.x-dev
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql 2.0.x-dev; fi
- composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile
script:
- vendor/bin/phpunit restfulserver/tests
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs src/ tests/ *.php; 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
FormEncodedDataFormatter: SilverStripe\RestfulServer\DataFormatter\FormEncodedDataFormatter
JSONDataFormatter: SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter
XMLDataFormatter: SilverStripe\RestfulServer\DataFormatter\XMLDataFormatter

View File

@ -1,39 +1,63 @@
# SilverStripe RestfulServer Module
[![Build Status](https://secure.travis-ci.org/silverstripe/silverstripe-restfulserver.png)](http://travis-ci.org/silverstripe/silverstripe-restfulserver)
[![Build Status](https://travis-ci.org/silverstripe/silverstripe-restfulserver.svg?branch=master)](https://travis-ci.org/silverstripe/silverstripe-restfulserver)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/silverstripe/silverstripe-restfulserver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/silverstripe/silverstripe-restfulserver/?branch=master)
[![codecov](https://codecov.io/gh/silverstripe/silverstripe-restfulserver/branch/master/graph/badge.svg)](https://codecov.io/gh/silverstripe/silverstripe-restfulserver)
## Overview
This class gives your application a RESTful API. All you have to do is define static $api_access = true on
the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in
This class gives your application a RESTful API. All you have to do is set the `api_access` configuration option to `true`
on the appropriate DataObjects. You will need to ensure that all of your data manipulation and security is defined in
your model layer (ie, the DataObject classes) and not in your Controllers. This is the recommended design for SilverStripe
applications.
## Requirements
* SilverStripe 3.0 or newer
* SilverStripe 4.0 or higher
For a SilverStripe 3.x compatible version of this module, please see the [1.0 branch, or 1.x release line](https://github.com/silverstripe/silverstripe-restfulserver/tree/1.0#readme).
## Configuration
Enabling restful access on a model will also enable a SOAP API, see `SOAPModelAccess`.
Example DataObject with simple api access, giving full access to all object properties and relations,
Example DataObject with simple API access, giving full access to all object properties and relations,
unless explicitly controlled through model permissions.
class Article extends DataObject {
static $db = array('Title'=>'Text','Published'=>'Boolean');
static $api_access = true;
}
```php
namespace Vendor\Project;
Example DataObject with advanced api access, limiting viewing and editing to Title attribute only:
use SilverStripe\ORM\DataObject;
class Article extends DataObject {
static $db = array('Title'=>'Text','Published'=>'Boolean');
static $api_access = array(
'view' => array('Title'),
'edit' => array('Title'),
);
private static $db = [
'Title'=>'Text',
'Published'=>'Boolean'
];
private static $api_access = true;
}
```
Example DataObject with advanced API access, limiting viewing and editing to Title attribute only:
```php
namespace Vendor\Project;
use SilverStripe\ORM\DataObject;
class Article extends DataObject {
private static $db = [
'Title'=>'Text',
'Published'=>'Boolean'
];
private static $api_access = [
'view' => ['Title'],
'edit' => ['Title']
];
}
```
## Supported operations
@ -66,7 +90,7 @@ to the url, e.g. /api/v1/(ClassName)/?Title=mytitle.
## Access control
Access control is implemented through the usual Member system with Basicauth authentication only.
Access control is implemented through the usual Member system with BasicAuth authentication only.
By default, you have to bear the ADMIN permission to retrieve or send any data.
You should override the following built-in methods to customize permission control on a
class- and object-level:
@ -76,7 +100,7 @@ class- and object-level:
- `DataObject::canDelete()`
- `DataObject::canCreate()`
See `DataObject` documentation for further details.
See `SilverStripe\ORM\DataObject` documentation for further details.
You can specify the character-encoding for any input on the HTTP Content-Type.
At the moment, only UTF-8 is supported. All output is made in UTF-8 regardless of Accept headers.

View File

View File

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

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",
"description": "Add a RESTful API to your SilverStripe application",
"type": "silverstripe-module",
"keywords": [
"silverstripe",
"rest",
"api"
],
"type": "silverstripe-vendormodule",
"keywords": ["silverstripe", "rest", "api"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Hamish Friedlander",
@ -18,12 +15,24 @@
}
],
"require": {
"silverstripe/framework": "3.*"
"silverstripe/framework": "^4"
},
"require-dev": {
"phpunit/PHPUnit": "^5.7",
"squizlabs/php_codesniffer": "^3.0",
"silverstripe/versioned": "^1"
},
"autoload": {
"psr-4": {
"SilverStripe\\RestfulServer\\": "src",
"SilverStripe\\RestfulServer\\Tests\\": "tests"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
"dev-master": "2.x-dev"
}
},
"license": "BSD-3-Clause"
"prefer-stable": true,
"minimum-stability": "dev"
}

View File

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

9
phpcs.xml.dist Normal file
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">src/</directory>
<exclude>
<directory suffix=".php">tests/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -1,5 +1,12 @@
<?php
namespace SilverStripe\RestfulServer;
use SilverStripe\Security\Authenticator;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Security\Security;
/**
* A simple authenticator for the Restful server.
*
@ -19,23 +26,25 @@ class BasicRestfulAuthenticator
*/
public static function authenticate()
{
//if there is no username or password, break
//if there is no username or password, fail
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
return false;
return null;
}
//Attempt to authenticate with the default authenticator for the site
$authClass = Authenticator::get_default_authenticator();
$member = $authClass::authenticate(array(
// With a valid user and password, check the password is correct
$data = [
'Email' => $_SERVER['PHP_AUTH_USER'],
'Password' => $_SERVER['PHP_AUTH_PW'],
));
//Log the member in and return the member, if they were found
];
$request = Controller::curr()->getRequest();
$authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
$member = null;
foreach ($authenticators as $authenticator) {
$member = $authenticator->authenticate($data, $request);
if ($member) {
$member->LogIn(false);
break;
}
}
return $member;
}
return false;
}
}

347
src/DataFormatter.php Normal file
View File

@ -0,0 +1,347 @@
<?php
namespace SilverStripe\RestfulServer;
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.
*/
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,43 @@
<?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>
*/
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\RestfulServer\DataFormatter;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\Core\Convert;
use SilverStripe\View\ArrayData;
use SilverStripe\Control\Director;
use SilverStripe\ORM\SS_List;
/**
* Formats a DataObject's member fields into a JSON string
*/
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\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;
/**
* Formats a DataObject's member fields into an XML string
*/
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,25 @@
<?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;
use SilverStripe\Control\HTTPRequest;
/**
* Generic RESTful server, which handles webservice access to arbitrary DataObjects.
* Relies on serialization/deserialization into different formats provided
* by the DataFormatter APIs in core.
*
* @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
* @todo Implement PUT/POST/DELETE for relations
* @todo Access-Control for relations (you might be allowed to view Members and Groups,
* but not their relation with each other)
@ -22,21 +37,25 @@
* @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
* @todo Access to extendable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22
* @todo Respect $api_access array notation in search contexts
*
* @package framework
* @subpackage api
*/
class RestfulServer extends Controller
{
public static $url_handlers = array(
'$ClassName/$ID/$Relation' => 'handleAction'
#'$ClassName/#ID' => 'handleItem',
#'$ClassName' => 'handleList',
private static $url_handlers = array(
'$ClassName!/$ID/$Relation' => 'handleAction',
'' => 'notFound'
);
protected static $api_base = "api/v1/";
/**
* @config
* @var string root of the api route, MUST have a trailing slash
*/
private static $api_base = "api/v1/";
protected static $authenticator = 'BasicRestfulAuthenticator';
/**
* @config
* @var string Class name for an authenticator to use on API access
*/
private static $authenticator = BasicRestfulAuthenticator::class;
/**
* If no extension is given in the request, resolve to this extension
@ -44,7 +63,7 @@ class RestfulServer extends Controller
*
* @var string
*/
public static $default_extension = "xml";
private static $default_extension = "xml";
/**
* If no extension is given, resolve the request to this mimetype.
@ -59,44 +78,58 @@ class RestfulServer extends Controller
*/
protected $member;
public static $allowed_actions = array(
'index'
private static $allowed_actions = array(
'index',
'notFound'
);
/*
function handleItem($request) {
return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
}
function handleList($request) {
return new RestfulServer_List(DataObject::get($request->param("ClassName"),""));
}
*/
public function init()
{
/* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults
* to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live.
* TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API)
*/
if (class_exists('SiteTree')) {
singleton('SiteTree')->extend('modelascontrollerInit', $this);
if (class_exists(SiteTree::class)) {
singleton(SiteTree::class)->extend('modelascontrollerInit', $this);
}
parent::init();
}
/**
* Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
* kills both requests (i.e. URIs) and XML (invalid character in a tag name)
* So we'll replace them with a hyphen (-), as it's also unambiguious
* in both cases (invalid in a php class name, and safe in an xml tag name)
*
* @param string $classname
* @return string 'escaped' class name
*/
protected function sanitiseClassName($className)
{
return str_replace('\\', '-', $className);
}
/**
* Convert hyphen escaped class names back into fully qualified
* PHP safe variant.
*
* @param string $classname
* @return string syntactically valid classname
*/
protected function unsanitiseClassName($className)
{
return str_replace('-', '\\', $className);
}
/**
* This handler acts as the switchboard for the controller.
* Since no $Action url-param is set, all requests are sent here.
*/
public function index()
public function index(HTTPRequest $request)
{
if (!isset($this->urlParams['ClassName'])) {
return $this->notFound();
}
$className = $this->urlParams['ClassName'];
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
$className = $this->unsanitiseClassName($request->param('ClassName'));
$id = $request->param('ID') ?: null;
$relation = $request->param('Relation') ?: null;
// Check input formats
if (!class_exists($className)) {
@ -105,8 +138,7 @@ class RestfulServer extends Controller
if ($id && !is_numeric($id)) {
return $this->notFound();
}
if (
$relation
if ($relation
&& !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
) {
return $this->notFound();
@ -259,8 +291,12 @@ class RestfulServer extends Controller
* @param array $params
* @return SS_List
*/
protected function getSearchQuery($className, $params = null, $sort = null,
$limit = null, $existingQuery = null
protected function getSearchQuery(
$className,
$params = null,
$sort = null,
$limit = null,
$existingQuery = null
) {
if (singleton($className)->hasMethod('getRestfulSearchContext')) {
$searchContext = singleton($className)->{'getRestfulSearchContext'}();
@ -287,7 +323,7 @@ class RestfulServer extends Controller
$accept = $this->request->getHeader('Accept');
$mimetypes = $this->request->getAcceptMimetypes();
if (!$className) {
$className = $this->urlParams['ClassName'];
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
}
// get formatter
@ -393,6 +429,7 @@ class RestfulServer extends Controller
if (!$obj) {
return $this->notFound();
}
if (!$obj->canEdit($this->getMember())) {
return $this->permissionFailure();
}
@ -424,7 +461,9 @@ class RestfulServer extends Controller
$type = ".{$types[0]}";
}
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
$urlSafeClassName = $this->sanitiseClassName(get_class($obj));
$apiBase = $this->config()->api_base;
$objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
$this->getResponse()->addHeader('Location', $objHref);
return $responseFormatter->convertDataObject($obj);
@ -493,7 +532,9 @@ class RestfulServer extends Controller
$type = ".{$types[0]}";
}
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
$urlSafeClassName = $this->sanitiseClassName(get_class($obj));
$apiBase = $this->config()->api_base;
$objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
$this->getResponse()->addHeader('Location', $objHref);
return $responseFormatter->convertDataObject($obj);
@ -529,7 +570,8 @@ class RestfulServer extends Controller
// @todo Disallow editing of certain keys in database
$data = array_diff_key($data, array('ID', 'Created'));
$apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
$className = $this->unsanitiseClassName($this->request->param('ClassName'));
$apiAccess = singleton($className)->config()->api_access;
if (is_array($apiAccess) && isset($apiAccess['edit'])) {
$data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
}
@ -579,9 +621,13 @@ class RestfulServer extends Controller
{
// The relation method will return a DataList, that getSearchQuery subsequently manipulates
if ($obj->hasMethod($relationName)) {
if ($relationClass = $obj->has_one($relationName)) {
// $this->HasOneName() will return a dataobject or null, neither
// of which helps us get the classname in a consistent fashion.
// So we must use a way that is reliable.
if ($relationClass = DataObject::getSchema()->hasOneComponent(get_class($obj), $relationName)) {
$joinField = $relationName . 'ID';
$list = DataList::create($relationClass)->byIDs(array($obj->$joinField));
// Again `byID` will return the wrong type for our purposes. So use `byIDs`
$list = DataList::create($relationClass)->byIDs([$obj->$joinField]);
} else {
$list = $obj->$relationName();
}
@ -633,8 +679,10 @@ class RestfulServer extends Controller
*/
protected function authenticate()
{
$authClass = self::config()->authenticator;
return $authClass::authenticate();
$authClass = $this->config()->authenticator;
$member = $authClass::authenticate();
Security::setCurrentUser($member);
return $member;
}
/**
@ -649,9 +697,12 @@ class RestfulServer extends Controller
{
$allowedRelations = array();
$obj = singleton($class);
$relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many();
$relations = (array)$obj->hasOne() + (array)$obj->hasMany() + (array)$obj->manyMany();
if ($relations) {
foreach ($relations as $relName => $relClass) {
//remove dot notation from relation names
$parts = explode('.', $relClass);
$relClass = array_shift($parts);
if (singleton($relClass)->stat('api_access')) {
$allowedRelations[] = $relName;
}
@ -667,59 +718,6 @@ class RestfulServer extends Controller
*/
protected function getMember()
{
return Member::currentUser();
}
}
/**
* Restful server handler for a SS_List
*
* @package framework
* @subpackage api
*/
class RestfulServer_List
{
public static $url_handlers = array(
'#ID' => 'handleItem',
);
public function __construct($list)
{
$this->list = $list;
}
public function handleItem($request)
{
return new RestulServer_Item($this->list->getById($request->param('ID')));
}
}
/**
* Restful server handler for a single DataObject
*
* @package framework
* @subpackage api
*/
class RestfulServer_Item
{
public static $url_handlers = array(
'$Relation' => 'handleRelation',
);
public function __construct($item)
{
$this->item = $item;
}
public function handleRelation($request)
{
$funcName = $request('Relation');
$relation = $this->item->$funcName();
if ($relation instanceof SS_List) {
return new RestfulServer_List($relation);
} else {
return new RestfulServer_Item($relation);
}
return Security::getCurrentUser();
}
}

32
src/RestfulServerItem.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace SilverStripe\RestfulServer;
use SilverStripe\ORM\SS_List;
/**
* Restful server handler for a single DataObject
*/
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);
}
}
}

23
src/RestfulServerList.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\RestfulServer;
/**
* Restful server handler for a SS_List
*/
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')));
}
}

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,4 +1,22 @@
<?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
@ -8,33 +26,50 @@
*/
class RestfulServerTest extends SapphireTest
{
public static $fixture_file = 'RestfulServerTest.yml';
protected static $fixture_file = 'RestfulServerTest.yml';
protected $extraDataObjects = array(
'RestfulServerTest_Comment',
'RestfulServerTest_SecretThing',
'RestfulServerTest_Page',
'RestfulServerTest_Author',
'RestfulServerTest_AuthorRating',
);
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('RestfulServerTest_Comment', 'comment1');
$page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$page1 = $this->objFromFixture(RestfulServerTestPage::class, 'page1');
// normal GET should succeed with $api_access enabled
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$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
$url = "/api/v1/RestfulServerTest_Page/" . $page1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestPage::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $page1->ID;
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 401);
$this->assertEquals(401, $response->getStatusCode());
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -42,9 +77,10 @@ class RestfulServerTest extends SapphireTest
public function testApiAccessBoolean()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$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());
@ -55,20 +91,22 @@ class RestfulServerTest extends SapphireTest
public function testAuthenticatedGET()
{
$thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1');
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$thing1 = $this->objFromFixture(RestfulServerTestSecretThing::class, 'thing1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
// @todo create additional mock object with authenticated VIEW permissions
$url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $thing1->ID;
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 401);
$this->assertEquals(401, $response->getStatusCode());
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
$_SERVER['PHP_AUTH_PW'] = 'user';
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -76,18 +114,19 @@ class RestfulServerTest extends SapphireTest
public function testAuthenticatedPUT()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$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($response->getStatusCode(), 401); // Permission failure
$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($response->getStatusCode(), 200); // Success
$this->assertEquals(200, $response->getStatusCode()); // Success
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -95,21 +134,23 @@ class RestfulServerTest extends SapphireTest
public function testGETRelationshipsXML()
{
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
$rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2');
$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);
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID;
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
$responseArr = Convert::xml2array($response->getBody());
$ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating'];
$this->assertEquals(count($ratingsArr), 2);
$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']
@ -121,17 +162,19 @@ class RestfulServerTest extends SapphireTest
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');
$author2 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author2');
$author3 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author3');
$author4 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author4');
$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
$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());
$authorsArr = $arr['RestfulServerTest_Author'];
$xmlSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthor::class);
$authorsArr = $arr[$xmlSafeClassName];
$this->assertEquals(count($authorsArr), 2);
$this->assertEquals(2, count($authorsArr));
$ratingIDs = array(
(int)$authorsArr[0]['ID'],
(int)$authorsArr[1]['ID']
@ -142,23 +185,24 @@ class RestfulServerTest extends SapphireTest
public function testPUTWithFormEncoded()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
$_SERVER['PHP_AUTH_PW'] = 'editor';
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$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($response->getStatusCode(), 200); // Success
$this->assertEquals(200, $response->getStatusCode()); // 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');
$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']);
@ -166,27 +210,28 @@ class RestfulServerTest extends SapphireTest
public function testPOSTWithFormEncoded()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
$_SERVER['PHP_AUTH_PW'] = 'editor';
$url = "/api/v1/RestfulServerTest_Comment";
$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($response->getStatusCode(), 201); // Created
$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($responseArr['Comment'], 'created');
$this->assertEquals($responseArr['Name'], 'New Comment');
$this->assertEquals('created', $responseArr['Comment']);
$this->assertEquals('New Comment', $responseArr['Name']);
$this->assertEquals(
$response->getHeader('Location'),
Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID'])
Controller::join_links($url, $responseArr['ID'] . '.xml'),
$response->getHeader('Location')
);
unset($_SERVER['PHP_AUTH_USER']);
@ -198,7 +243,7 @@ class RestfulServerTest extends SapphireTest
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
$_SERVER['PHP_AUTH_PW'] = 'editor';
$url = '/api/v1/RestfulServerTest_Comment';
$url = "{$this->baseURI}/api/v1/" . RestfulServerTestComment::class;
$response = Director::test($url, null, null, 'POST');
$this->assertEquals('No Content', $response->getBody());
@ -208,32 +253,34 @@ class RestfulServerTest extends SapphireTest
public function testPUTwithJSON()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
$_SERVER['PHP_AUTH_PW'] = 'editor';
// by mimetype
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
// 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'));
$this->assertEquals($response->getStatusCode(), 200); // 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($obj->ID, $comment1->ID);
$this->assertEquals($obj->Comment, 'updated');
$this->assertEquals($comment1->ID, $obj->ID);
$this->assertEquals('updated', $obj->Comment);
// by extension
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID);
$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($response->getStatusCode(), 200); // Updated
$this->assertEquals(
$response->getHeader('Location'),
Controller::join_links(Director::absoluteBaseURL(), $url)
);
$this->assertEquals(200, $response->getStatusCode()); // Updated
$this->assertEquals($url, $response->getHeader('Location'));
$obj = Convert::json2obj($response->getBody());
$this->assertEquals($obj->ID, $comment1->ID);
$this->assertEquals($obj->Comment, 'updated');
$this->assertEquals($comment1->ID, $obj->ID);
$this->assertEquals('updated', $obj->Comment);
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -241,32 +288,31 @@ class RestfulServerTest extends SapphireTest
public function testPUTwithXML()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, '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>';
$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($response->getStatusCode(), 200); // Updated
$this->assertEquals(200, $response->getStatusCode()); // Updated
$obj = Convert::xml2array($response->getBody());
$this->assertEquals($obj['ID'], $comment1->ID);
$this->assertEquals($obj['Comment'], 'updated');
$this->assertEquals($comment1->ID, $obj['ID']);
$this->assertEquals('updated', $obj['Comment']);
// by extension
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID);
$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
$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($response->getStatusCode(), 200); // Updated
$this->assertEquals(
$response->getHeader('Location'),
Controller::join_links(Director::absoluteBaseURL(), $url)
);
$this->assertEquals(200, $response->getStatusCode()); // Updated
$this->assertEquals($url, $response->getHeader('Location'));
$obj = Convert::xml2array($response->getBody());
$this->assertEquals($obj['ID'], $comment1->ID);
$this->assertEquals($obj['Comment'], 'updated');
$this->assertEquals($comment1->ID, $obj['ID']);
$this->assertEquals('updated', $obj['Comment']);
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -274,16 +320,17 @@ class RestfulServerTest extends SapphireTest
public function testHTTPAcceptAndContentType()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$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($response->getStatusCode(), 200); // Success
$this->assertEquals(200, $response->getStatusCode()); // Success
$obj = Convert::json2obj($response->getBody());
$this->assertEquals($obj->ID, $comment1->ID);
$this->assertEquals($response->getHeader('Content-Type'), 'application/json');
$this->assertEquals($comment1->ID, $obj->ID);
$this->assertEquals('application/json', $response->getHeader('Content-Type'));
}
public function testNotFound()
@ -291,9 +338,10 @@ class RestfulServerTest extends SapphireTest
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
$_SERVER['PHP_AUTH_PW'] = 'user';
$url = "/api/v1/RestfulServerTest_Comment/99";
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/99";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 404);
$this->assertEquals(404, $response->getStatusCode());
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -301,20 +349,22 @@ class RestfulServerTest extends SapphireTest
public function testMethodNotAllowed()
{
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
$this->assertEquals($response->getStatusCode(), 405);
$this->assertEquals(405, $response->getStatusCode());
}
public function testConflictOnExistingResourceWhenUsingPost()
{
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
$response = Director::test($url, null, null, 'POST');
$this->assertEquals($response->getStatusCode(), 409);
$this->assertEquals(409, $response->getStatusCode());
}
public function testUnsupportedMediaType()
@ -322,11 +372,12 @@ class RestfulServerTest extends SapphireTest
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
$_SERVER['PHP_AUTH_PW'] = 'user';
$url = "/api/v1/RestfulServerTest_Comment";
$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($response->getStatusCode(), 415);
$this->assertEquals(415, $response->getStatusCode());
unset($_SERVER['PHP_AUTH_USER']);
unset($_SERVER['PHP_AUTH_PW']);
@ -334,9 +385,10 @@ class RestfulServerTest extends SapphireTest
public function testXMLValueFormatting()
{
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
$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());
@ -344,10 +396,11 @@ class RestfulServerTest extends SapphireTest
public function testApiAccessFieldRestrictions()
{
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
$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());
@ -355,39 +408,55 @@ class RestfulServerTest extends SapphireTest
$this->assertNotContains('<SecretField>', $response->getBody());
$this->assertNotContains('<SecretRelation>', $response->getBody());
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
$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(),
$this->assertNotContains(
'<SecretField>',
$response->getBody(),
'"add_fields" URL parameter filters out disallowed fields from $api_access'
);
$this->assertNotContains('<SecretRelation>', $response->getBody(),
$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';
$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(),
$this->assertNotContains(
'<SecretField>',
$response->getBody(),
'"fields" URL parameter filters out disallowed fields from $api_access'
);
$this->assertNotContains('<SecretRelation>', $response->getBody(),
$this->assertNotContains(
'<SecretRelation>',
$response->getBody(),
'"fields" URL parameter filters out disallowed relations from $api_access'
);
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings';
$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(),
$this->assertContains(
'<Rating>',
$response->getBody(),
'Relation viewer shows fields allowed through $api_access'
);
$this->assertNotContains('<SecretField>', $response->getBody(),
$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');
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
$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');
@ -395,26 +464,30 @@ class RestfulServerTest extends SapphireTest
public function testApiAccessRelationRestrictionsOnEndpoint()
{
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage";
$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');
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages";
$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');
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages";
$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('RestfulServerTest_AuthorRating', 'rating1');
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
$data = array(
'Rating' => '42',
'WriteProtectedField' => 'haxx0red'
@ -422,59 +495,65 @@ class RestfulServerTest extends SapphireTest
$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');
$this->assertEquals(42, $responseArr['Rating']);
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
}
public function testJSONDataFormatter()
{
$formatter = new JSONDataFormatter();
$editor = $this->objFromFixture('Member', 'editor');
$user = $this->objFromFixture('Member', 'user');
$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(
$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
$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(
$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");
$formatter->convertDataObjectSet($set, ["FirstName", "Email"]),
"Correct JSON formatting on a dataobjectset with field filter"
);
}
public function testApiAccessWithPOST()
{
$url = "/api/v1/RestfulServerTest_AuthorRating";
$data = array(
$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($responseArr['Rating'], 42);
$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
$this->assertEquals(42, $responseArr['Rating']);
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
}
public function testCanViewRespectedInList()
{
// Default content type
$url = "/api/v1/RestfulServerTest_SecretThing/";
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
$this->assertNotContains('Unspeakable', $response->getBody());
// JSON content type
$url = "/api/v1/RestfulServerTest_SecretThing.json";
$url = "{$this->baseURI}/api/v1/$urlSafeClassname.json";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
$this->assertNotContains('Unspeakable', $response->getBody());
$responseArray = Convert::json2array($response->getBody());
$this->assertSame(0, $responseArray['totalSize']);
@ -482,9 +561,10 @@ class RestfulServerTest extends SapphireTest
// With authentication
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
$_SERVER['PHP_AUTH_PW'] = 'editor';
$url = "/api/v1/RestfulServerTest_SecretThing/";
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
$response = Director::test($url, null, null, 'GET');
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(200, $response->getStatusCode());
$this->assertContains('Unspeakable', $response->getBody());
// Assumption: default formatter is XML
$responseArray = Convert::xml2array($response->getBody());
@ -493,159 +573,3 @@ class RestfulServerTest extends SapphireTest
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,4 +1,4 @@
Member:
SilverStripe\Security\Member:
editor:
FirstName: Editor
Email: editor@test.com
@ -7,40 +7,40 @@ Member:
FirstName: User
Email: user@test.com
Password: user
Group:
SilverStripe\Security\Group:
editorgroup:
Title: Editors
Code: editors
Members: =>Member.editor
Members: =>SilverStripe\Security\Member.editor
usergroup:
Title: Users
Code: users
Members: =>Member.user
Permission:
Members: =>SilverStripe\Security\Member.user
SilverStripe\Security\Permission:
perm1:
Code: CREATE_Comment
Group: =>Group.usergroup
Group: =>SilverStripe\Security\Group.usergroup
perm3:
Code: EDIT_Comment
Group: =>Group.editorgroup
Group: =>SilverStripe\Security\Group.editorgroup
perm4:
Code: DELETE_Comment
Group: =>Group.editorgroup
Group: =>SilverStripe\Security\Group.editorgroup
perm5:
Code: CREATE_Comment
Group: =>Group.editorgroup
Group: =>SilverStripe\Security\Group.editorgroup
perm6:
Code: VIEW_SecretThing
Group: =>Group.editorgroup
RestfulServerTest_Page:
Group: =>SilverStripe\Security\Group.editorgroup
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage:
page1:
Title: Testpage without API Access
RestfulServerTest_Comment:
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment:
comment1:
Name: Joe
Comment: This is a test comment
Page: =>RestfulServerTest_Page.page1
RestfulServerTest_Author:
Page: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage.page1
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor:
author1:
FirstName: Author 1
author2:
@ -49,18 +49,20 @@ RestfulServerTest_Author:
Firstname: Author 3
author4:
FirstName: Author 4
RelatedAuthors: =>RestfulServerTest_Author.author2,=>RestfulServerTest_Author.author3
RestfulServerTest_AuthorRating:
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: =>RestfulServerTest_Author.author1
SecretRelation: =>RestfulServerTest_Author.author1
Author: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
SecretRelation: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
rating2:
Rating: 5
Author: =>RestfulServerTest_Author.author1
SecretRelation: =>RestfulServerTest_Author.author1
RestfulServerTest_SecretThing:
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',
);
}
}