mirror of
https://github.com/silverstripe/silverstripe-restfulserver
synced 2024-10-22 14:05:58 +02:00
Upgrade: begin SilverStripe 4 compatiblity update
This commit is contained in:
parent
e5a757d589
commit
6601b42c4b
@ -10,7 +10,7 @@ indent_style = space
|
|||||||
insert_final_newline = true
|
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
1
.gitattributes
vendored
@ -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
|
||||||
|
@ -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/*]
|
||||||
|
39
.travis.yml
39
.travis.yml
@ -1,28 +1,33 @@
|
|||||||
# See https://github.com/silverstripe/silverstripe-travis-support for setup details
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
language: php
|
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
15
.upgrade.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
mappings:
|
||||||
|
BasicRestfulAuthenticator: SilverStripe\RestfulServer\BasicRestfulAuthenticator
|
||||||
|
RestfulServer: SilverStripe\RestfulServer\RestfulServer
|
||||||
|
RestfulServerItem: SilverStripe\RestfulServer\RestfulServerItem
|
||||||
|
RestfulServerList: SilverStripe\RestfulServer\RestfulServerList
|
||||||
|
RestfulServerTest: SilverStripe\RestfulServer\Tests\RestfulServerTest
|
||||||
|
RestfulServerTestAuthor: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor
|
||||||
|
RestfulServerTestAuthorRating: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating
|
||||||
|
RestfulServerTestComment: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment
|
||||||
|
RestfulServerTestPage: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage
|
||||||
|
RestfulServerTestSecretThing: SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing
|
||||||
|
DataFormatter: SilverStripe\RestfulServer\DataFormatter\DataFormatter
|
||||||
|
FormEncodedDataFormatter: SilverStripe\RestfulServer\DataFormatter\FormEncodedDataFormatter
|
||||||
|
JSONDataFormatter: SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter
|
||||||
|
XMLDataFormatter: SilverStripe\RestfulServer\DataFormatter\XMLDataFormatter
|
68
README.md
68
README.md
@ -1,39 +1,63 @@
|
|||||||
# SilverStripe RestfulServer Module
|
# 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.
|
||||||
|
@ -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'
|
|
||||||
|
20
changelog.md
20
changelog.md
@ -1,20 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
||||||
|
|
||||||
## [1.0.5]
|
|
||||||
|
|
||||||
* Converted to PSR-2
|
|
||||||
* Added standard Scrutinizer config
|
|
||||||
* Added standard code of conduct
|
|
||||||
* Added standard editor config
|
|
||||||
* Added standard Travis config
|
|
||||||
* Added standard license
|
|
||||||
* Added standard git attributes
|
|
||||||
|
|
||||||
## [1.0.4]
|
|
||||||
|
|
||||||
* Changelog added.
|
|
||||||
* Include 3.2 and php 5.6 in tests
|
|
1
codecov.yml
Normal file
1
codecov.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
comment: false
|
@ -1,12 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "silverstripe/restfulserver",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -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
9
phpcs.xml.dist
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ruleset name="SilverStripe">
|
||||||
|
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
||||||
|
|
||||||
|
<rule ref="PSR2" >
|
||||||
|
<!-- Current exclusions -->
|
||||||
|
<exclude name="PSR1.Methods.CamelCapsMethodName" />
|
||||||
|
</rule>
|
||||||
|
</ruleset>
|
13
phpunit.xml.dist
Normal file
13
phpunit.xml.dist
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
|
||||||
|
<testsuite name="Default">
|
||||||
|
<directory>tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
<filter>
|
||||||
|
<whitelist addUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">code/</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory suffix=".php">tests/</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
@ -1,5 +1,12 @@
|
|||||||
<?php
|
<?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);
|
||||||
if ($member) {
|
$member = null;
|
||||||
$member->LogIn(false);
|
foreach ($authenticators as $authenticator) {
|
||||||
return $member;
|
$member = $authenticator->authenticate($data, $request);
|
||||||
|
if ($member) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return $member;
|
||||||
}
|
}
|
||||||
}
|
}
|
350
src/DataFormatter/DataFormatter.php
Normal file
350
src/DataFormatter/DataFormatter.php
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||||
|
|
||||||
|
use SilverStripe\Core\ClassInfo;
|
||||||
|
use SilverStripe\Core\Config\Configurable;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectInterface;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DataFormatter object handles transformation of data from SilverStripe model objects to a particular output
|
||||||
|
* format, and vice versa. This is most commonly used in developing RESTful APIs.
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage formatters
|
||||||
|
*/
|
||||||
|
abstract class DataFormatter
|
||||||
|
{
|
||||||
|
|
||||||
|
use Configurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set priority from 0-100.
|
||||||
|
* If multiple formatters for the same extension exist,
|
||||||
|
* we select the one with highest priority.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private static $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follow relations for the {@link DataObject} instances
|
||||||
|
* ($has_one, $has_many, $many_many).
|
||||||
|
* Set to "0" to disable relation output.
|
||||||
|
*
|
||||||
|
* @todo Support more than one nesting level
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $relationDepth = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows overriding of the fields which are rendered for the
|
||||||
|
* processed dataobjects. By default, this includes all
|
||||||
|
* fields in {@link DataObject::inheritedDatabaseFields()}.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $customFields = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows addition of fields
|
||||||
|
* (e.g. custom getters on a DataObject)
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $customAddFields = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to limit or add relations.
|
||||||
|
* Only use in combination with {@link $relationDepth}.
|
||||||
|
* By default, all relations will be shown.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $customRelations = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields which should be expicitly excluded from the export.
|
||||||
|
* Comes in handy for field-level permissions.
|
||||||
|
* Will overrule both {@link $customAddFields} and {@link $customFields}
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $removeFields = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the mimetype in which all strings
|
||||||
|
* returned from the convert*() methods should be used,
|
||||||
|
* e.g. "text/xml".
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $outputContentType = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to set totalSize properties on the output
|
||||||
|
* of {@link convertDataObjectSet()}, shows the
|
||||||
|
* total number of records without the "limit" and "offset"
|
||||||
|
* GET parameters. Useful to implement pagination.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $totalSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
|
||||||
|
* kills both requests (i.e. URIs) and XML (invalid character in a tag name)
|
||||||
|
* So we'll replace them with a hyphen (-), as it's also unambiguious
|
||||||
|
* in both cases (invalid in a php class name, and safe in an xml tag name)
|
||||||
|
*
|
||||||
|
* @param string $classname
|
||||||
|
* @return string 'escaped' class name
|
||||||
|
*/
|
||||||
|
protected function sanitiseClassName($className)
|
||||||
|
{
|
||||||
|
return str_replace('\\', '-', $className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a DataFormatter object suitable for handling the given file extension.
|
||||||
|
*
|
||||||
|
* @param string $extension
|
||||||
|
* @return DataFormatter
|
||||||
|
*/
|
||||||
|
public static function for_extension($extension)
|
||||||
|
{
|
||||||
|
$classes = ClassInfo::subclassesFor(DataFormatter::class);
|
||||||
|
array_shift($classes);
|
||||||
|
$sortedClasses = array();
|
||||||
|
foreach ($classes as $class) {
|
||||||
|
$sortedClasses[$class] = singleton($class)->stat('priority');
|
||||||
|
}
|
||||||
|
arsort($sortedClasses);
|
||||||
|
foreach ($sortedClasses as $className => $priority) {
|
||||||
|
$formatter = new $className();
|
||||||
|
if (in_array($extension, $formatter->supportedExtensions())) {
|
||||||
|
return $formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatter for the first matching extension.
|
||||||
|
*
|
||||||
|
* @param array $extensions
|
||||||
|
* @return DataFormatter
|
||||||
|
*/
|
||||||
|
public static function for_extensions($extensions)
|
||||||
|
{
|
||||||
|
foreach ($extensions as $extension) {
|
||||||
|
if ($formatter = self::for_extension($extension)) {
|
||||||
|
return $formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a DataFormatter object suitable for handling the given mimetype.
|
||||||
|
*
|
||||||
|
* @param string $mimeType
|
||||||
|
* @return DataFormatter
|
||||||
|
*/
|
||||||
|
public static function for_mimetype($mimeType)
|
||||||
|
{
|
||||||
|
$classes = ClassInfo::subclassesFor(DataFormatter::class);
|
||||||
|
array_shift($classes);
|
||||||
|
$sortedClasses = array();
|
||||||
|
foreach ($classes as $class) {
|
||||||
|
$sortedClasses[$class] = singleton($class)->stat('priority');
|
||||||
|
}
|
||||||
|
arsort($sortedClasses);
|
||||||
|
foreach ($sortedClasses as $className => $priority) {
|
||||||
|
$formatter = new $className();
|
||||||
|
if (in_array($mimeType, $formatter->supportedMimeTypes())) {
|
||||||
|
return $formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatter for the first matching mimetype.
|
||||||
|
* Useful for HTTP Accept headers which can contain
|
||||||
|
* multiple comma-separated mimetypes.
|
||||||
|
*
|
||||||
|
* @param array $mimetypes
|
||||||
|
* @return DataFormatter
|
||||||
|
*/
|
||||||
|
public static function for_mimetypes($mimetypes)
|
||||||
|
{
|
||||||
|
foreach ($mimetypes as $mimetype) {
|
||||||
|
if ($formatter = self::for_mimetype($mimetype)) {
|
||||||
|
return $formatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $fields
|
||||||
|
*/
|
||||||
|
public function setCustomFields($fields)
|
||||||
|
{
|
||||||
|
$this->customFields = $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getCustomFields()
|
||||||
|
{
|
||||||
|
return $this->customFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $fields
|
||||||
|
*/
|
||||||
|
public function setCustomAddFields($fields)
|
||||||
|
{
|
||||||
|
$this->customAddFields = $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $relations
|
||||||
|
*/
|
||||||
|
public function setCustomRelations($relations)
|
||||||
|
{
|
||||||
|
$this->customRelations = $relations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getCustomRelations()
|
||||||
|
{
|
||||||
|
return $this->customRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getCustomAddFields()
|
||||||
|
{
|
||||||
|
return $this->customAddFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $fields
|
||||||
|
*/
|
||||||
|
public function setRemoveFields($fields)
|
||||||
|
{
|
||||||
|
$this->removeFields = $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getRemoveFields()
|
||||||
|
{
|
||||||
|
return $this->removeFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOutputContentType()
|
||||||
|
{
|
||||||
|
return $this->outputContentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $size
|
||||||
|
*/
|
||||||
|
public function setTotalSize($size)
|
||||||
|
{
|
||||||
|
$this->totalSize = (int)$size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getTotalSize()
|
||||||
|
{
|
||||||
|
return $this->totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all fields on the object which should be shown
|
||||||
|
* in the output. Can be customised through {@link self::setCustomFields()}.
|
||||||
|
*
|
||||||
|
* @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
|
||||||
|
* @todo Field level permission checks
|
||||||
|
*
|
||||||
|
* @param DataObject $obj
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getFieldsForObj($obj)
|
||||||
|
{
|
||||||
|
$dbFields = array();
|
||||||
|
|
||||||
|
// if custom fields are specified, only select these
|
||||||
|
if (is_array($this->customFields)) {
|
||||||
|
foreach ($this->customFields as $fieldName) {
|
||||||
|
// @todo Possible security risk by making methods accessible - implement field-level security
|
||||||
|
if ($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
|
||||||
|
$dbFields[$fieldName] = $fieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// by default, all database fields are selected
|
||||||
|
$dbFields = DataObject::getSchema()->fieldSpecs(get_class($obj));
|
||||||
|
// $dbFields = $obj->inheritedDatabaseFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($this->customAddFields)) {
|
||||||
|
foreach ($this->customAddFields as $fieldName) {
|
||||||
|
// @todo Possible security risk by making methods accessible - implement field-level security
|
||||||
|
if ($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
|
||||||
|
$dbFields[$fieldName] = $fieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add default required fields
|
||||||
|
$dbFields = array_merge($dbFields, array('ID'=>'Int'));
|
||||||
|
|
||||||
|
if (is_array($this->removeFields)) {
|
||||||
|
$dbFields = array_diff_key($dbFields, array_combine($this->removeFields, $this->removeFields));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dbFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of the extensions that this data formatter supports
|
||||||
|
*/
|
||||||
|
abstract public function supportedExtensions();
|
||||||
|
|
||||||
|
abstract public function supportedMimeTypes();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a single data object to this format. Return a string.
|
||||||
|
*/
|
||||||
|
abstract public function convertDataObject(DataObjectInterface $do);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a data object set to this format. Return a string.
|
||||||
|
*/
|
||||||
|
abstract public function convertDataObjectSet(SS_List $set);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $strData HTTP Payload as string
|
||||||
|
*/
|
||||||
|
public function convertStringToArray($strData)
|
||||||
|
{
|
||||||
|
user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
46
src/DataFormatter/FormEncodedDataFormatter.php
Normal file
46
src/DataFormatter/FormEncodedDataFormatter.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts form encoded strings and converts them
|
||||||
|
* to a valid PHP array via {@link parse_str()}.
|
||||||
|
*
|
||||||
|
* Example when using cURL on commandline:
|
||||||
|
* <code>
|
||||||
|
* curl -d "Name=This is a new record" http://host/api/v1/(DataObject)
|
||||||
|
* curl -X PUT -d "Name=This is an updated record" http://host/api/v1/(DataObject)/1
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @todo Format response form encoded as well - currently uses XMLDataFormatter
|
||||||
|
*
|
||||||
|
* @author Cam Spiers <camspiers at gmail dot com>
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage formatters
|
||||||
|
*/
|
||||||
|
class FormEncodedDataFormatter extends XMLDataFormatter
|
||||||
|
{
|
||||||
|
|
||||||
|
public function supportedExtensions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportedMimeTypes()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertStringToArray($strData)
|
||||||
|
{
|
||||||
|
$postArray = array();
|
||||||
|
parse_str($strData, $postArray);
|
||||||
|
return $postArray;
|
||||||
|
//TODO: It would be nice to implement this function in Convert.php
|
||||||
|
//return Convert::querystr2array($strData);
|
||||||
|
}
|
||||||
|
}
|
170
src/DataFormatter/JSONDataFormatter.php
Normal file
170
src/DataFormatter/JSONDataFormatter.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObjectInterface;
|
||||||
|
use SilverStripe\Core\Convert;
|
||||||
|
use SilverStripe\View\ArrayData;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage formatters
|
||||||
|
*/
|
||||||
|
class JSONDataFormatter extends DataFormatter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @config
|
||||||
|
* @todo pass this from the API to the data formatter somehow
|
||||||
|
*/
|
||||||
|
private static $api_base = "api/v1/";
|
||||||
|
|
||||||
|
protected $outputContentType = 'application/json';
|
||||||
|
|
||||||
|
public function supportedExtensions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'json',
|
||||||
|
'js'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportedMimeTypes()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'application/json',
|
||||||
|
'text/x-json'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JSON representation of the given {@link DataObject}.
|
||||||
|
*
|
||||||
|
* @param DataObject $obj The object
|
||||||
|
* @param Array $fields If supplied, only fields in the list will be returned
|
||||||
|
* @param $relations Not used
|
||||||
|
* @return String JSON
|
||||||
|
*/
|
||||||
|
public function convertDataObject(DataObjectInterface $obj, $fields = null, $relations = null)
|
||||||
|
{
|
||||||
|
return Convert::array2json($this->convertDataObjectToJSONObject($obj, $fields, $relations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function to do the conversion of a single data object. It builds an empty object and dynamically
|
||||||
|
* adds the properties it needs to it. If it's done as a nested array, json_encode or equivalent won't use
|
||||||
|
* JSON object notation { ... }.
|
||||||
|
* @param DataObjectInterface $obj
|
||||||
|
* @param $fields
|
||||||
|
* @param $relations
|
||||||
|
* @return EmptyJSONObject
|
||||||
|
*/
|
||||||
|
public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields = null, $relations = null)
|
||||||
|
{
|
||||||
|
$className = get_class($obj);
|
||||||
|
$id = $obj->ID;
|
||||||
|
|
||||||
|
$serobj = ArrayData::array_to_object();
|
||||||
|
|
||||||
|
foreach ($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($fieldName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||||
|
$serobj->$fieldName = $fieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->relationDepth > 0) {
|
||||||
|
foreach ($obj->hasOne() as $relName => $relClass) {
|
||||||
|
if (!singleton($relClass)->stat('api_access')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($relName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $relName . 'ID';
|
||||||
|
if ($obj->$fieldName) {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$relClass/" . $obj->$fieldName);
|
||||||
|
} else {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName");
|
||||||
|
}
|
||||||
|
$serobj->$relName = ArrayData::array_to_object(array(
|
||||||
|
"className" => $relClass,
|
||||||
|
"href" => "$href.json",
|
||||||
|
"id" => $obj->$fieldName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($obj->hasMany() + $obj->manyMany() as $relName => $relClass) {
|
||||||
|
//remove dot notation from relation names
|
||||||
|
$parts = explode('.', $relClass);
|
||||||
|
$relClass = array_shift($parts);
|
||||||
|
|
||||||
|
if (!singleton($relClass)->stat('api_access')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($relName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$innerParts = array();
|
||||||
|
$items = $obj->$relName();
|
||||||
|
foreach ($items as $item) {
|
||||||
|
//$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName/$item->ID");
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||||
|
$innerParts[] = ArrayData::array_to_object(array(
|
||||||
|
"className" => $relClass,
|
||||||
|
"href" => "$href.json",
|
||||||
|
"id" => $item->ID
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$serobj->$relName = $innerParts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $serobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JSON representation of the given {@link SS_List}.
|
||||||
|
*
|
||||||
|
* @param SS_List $set
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObjectSet(SS_List $set, $fields = null)
|
||||||
|
{
|
||||||
|
$items = array();
|
||||||
|
foreach ($set as $do) {
|
||||||
|
if (!$do->canView()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items[] = $this->convertDataObjectToJSONObject($do, $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serobj = ArrayData::array_to_object(array(
|
||||||
|
"totalSize" => (is_numeric($this->totalSize)) ? $this->totalSize : null,
|
||||||
|
"items" => $items
|
||||||
|
));
|
||||||
|
|
||||||
|
return Convert::array2json($serobj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertStringToArray($strData)
|
||||||
|
{
|
||||||
|
return Convert::json2array($strData);
|
||||||
|
}
|
||||||
|
}
|
202
src/DataFormatter/XMLDataFormatter.php
Normal file
202
src/DataFormatter/XMLDataFormatter.php
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\DataFormatter;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObjectInterface;
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Core\Convert;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage formatters
|
||||||
|
*/
|
||||||
|
class XMLDataFormatter extends DataFormatter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @config
|
||||||
|
* @todo pass this from the API to the data formatter somehow
|
||||||
|
*/
|
||||||
|
private static $api_base = "api/v1/";
|
||||||
|
|
||||||
|
protected $outputContentType = 'text/xml';
|
||||||
|
|
||||||
|
public function supportedExtensions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'xml'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportedMimeTypes()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'text/xml',
|
||||||
|
'application/xml',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link DataObject}.
|
||||||
|
*
|
||||||
|
* @param DataObject $obj
|
||||||
|
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObject(DataObjectInterface $obj, $fields = null)
|
||||||
|
{
|
||||||
|
$response = Controller::curr()->getResponse();
|
||||||
|
if ($response) {
|
||||||
|
$response->addHeader("Content-Type", "text/xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . $this->convertDataObjectWithoutHeader($obj, $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertDataObjectWithoutHeader(DataObject $obj, $fields = null, $relations = null)
|
||||||
|
{
|
||||||
|
$className = $this->sanitiseClassName(get_class($obj));
|
||||||
|
$id = $obj->ID;
|
||||||
|
$objHref = Director::absoluteURL($this->config()->api_base . "$className/$obj->ID");
|
||||||
|
|
||||||
|
$xml = "<$className href=\"$objHref.xml\">\n";
|
||||||
|
foreach ($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($fieldName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fieldValue = $obj->obj($fieldName)->forTemplate();
|
||||||
|
if (!mb_check_encoding($fieldValue, 'utf-8')) {
|
||||||
|
$fieldValue = "(data is badly encoded)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toXML')) {
|
||||||
|
$xml .= $fieldValue->toXML();
|
||||||
|
} else {
|
||||||
|
if ('HTMLText' == $fieldType) {
|
||||||
|
// Escape HTML values using CDATA
|
||||||
|
$fieldValue = sprintf('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $fieldValue));
|
||||||
|
} else {
|
||||||
|
$fieldValue = Convert::raw2xml($fieldValue);
|
||||||
|
}
|
||||||
|
$xml .= "<$fieldName>$fieldValue</$fieldName>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->relationDepth > 0) {
|
||||||
|
foreach ($obj->hasOne() as $relName => $relClass) {
|
||||||
|
if (!singleton($relClass)->stat('api_access')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($relName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $relName . 'ID';
|
||||||
|
if ($obj->$fieldName) {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$relClass/" . $obj->$fieldName);
|
||||||
|
} else {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$className/$id/$relName");
|
||||||
|
}
|
||||||
|
$xml .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"" . $obj->$fieldName
|
||||||
|
. "\"></$relName>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($obj->hasMany() as $relName => $relClass) {
|
||||||
|
//remove dot notation from relation names
|
||||||
|
$parts = explode('.', $relClass);
|
||||||
|
$relClass = array_shift($parts);
|
||||||
|
if (!singleton($relClass)->stat('api_access')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// backslashes in FQCNs kills both URIs and XML
|
||||||
|
$relClass = $this->sanitiseClassName($relClass);
|
||||||
|
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($relName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
|
||||||
|
$items = $obj->$relName();
|
||||||
|
if ($items) {
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||||
|
$xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\"></$relClass>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$xml .= "</$relName>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($obj->manyMany() as $relName => $relClass) {
|
||||||
|
//remove dot notation from relation names
|
||||||
|
$parts = explode('.', $relClass);
|
||||||
|
$relClass = array_shift($parts);
|
||||||
|
if (!singleton($relClass)->stat('api_access')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// backslashes in FQCNs kills both URIs and XML
|
||||||
|
$relClass = $this->sanitiseClassName($relClass);
|
||||||
|
|
||||||
|
// Field filtering
|
||||||
|
if ($fields && !in_array($relName, $fields)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($this->customRelations && !in_array($relName, $this->customRelations)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n";
|
||||||
|
$items = $obj->$relName();
|
||||||
|
if ($items) {
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$href = Director::absoluteURL($this->config()->api_base . "$relClass/$item->ID");
|
||||||
|
$xml .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\"></$relClass>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$xml .= "</$relName>\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml .= "</$className>";
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an XML representation of the given {@link SS_List}.
|
||||||
|
*
|
||||||
|
* @param SS_List $set
|
||||||
|
* @return String XML
|
||||||
|
*/
|
||||||
|
public function convertDataObjectSet(SS_List $set, $fields = null)
|
||||||
|
{
|
||||||
|
Controller::curr()->getResponse()->addHeader("Content-Type", "text/xml");
|
||||||
|
$className = $this->sanitiseClassName(get_class($set));
|
||||||
|
|
||||||
|
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||||
|
$xml .= (is_numeric($this->totalSize)) ? "<$className totalSize=\"{$this->totalSize}\">\n" : "<$className>\n";
|
||||||
|
foreach ($set as $item) {
|
||||||
|
$xml .= $this->convertDataObjectWithoutHeader($item, $fields);
|
||||||
|
}
|
||||||
|
$xml .= "</$className>";
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convertStringToArray($strData)
|
||||||
|
{
|
||||||
|
return Convert::xml2array($strData);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,26 @@
|
|||||||
<?php
|
<?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
35
src/RestfulServerItem.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restful server handler for a single DataObject
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage api
|
||||||
|
*/
|
||||||
|
class RestfulServerItem
|
||||||
|
{
|
||||||
|
private static $url_handlers = array(
|
||||||
|
'$Relation' => 'handleRelation',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function __construct($item)
|
||||||
|
{
|
||||||
|
$this->item = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRelation($request)
|
||||||
|
{
|
||||||
|
$funcName = $request('Relation');
|
||||||
|
$relation = $this->item->$funcName();
|
||||||
|
|
||||||
|
if ($relation instanceof SS_List) {
|
||||||
|
return new RestfulServerList($relation);
|
||||||
|
} else {
|
||||||
|
return new RestfulServerItem($relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/RestfulServerList.php
Normal file
26
src/RestfulServerList.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restful server handler for a SS_List
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage api
|
||||||
|
*/
|
||||||
|
class RestfulServerList
|
||||||
|
{
|
||||||
|
private static $url_handlers = array(
|
||||||
|
'#ID' => 'handleItem',
|
||||||
|
);
|
||||||
|
|
||||||
|
public function __construct($list)
|
||||||
|
{
|
||||||
|
$this->list = $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleItem($request)
|
||||||
|
{
|
||||||
|
return new RestfulServerItem($this->list->getById($request->param('ID')));
|
||||||
|
}
|
||||||
|
}
|
575
tests/RestfulServerTest.php
Normal file
575
tests/RestfulServerTest.php
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Core\Convert;
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\RestfulServer\DataFormatter\JSONDataFormatter;
|
||||||
|
use Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @todo Test Relation getters
|
||||||
|
* @todo Test filter and limit through GET params
|
||||||
|
* @todo Test DELETE verb
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class RestfulServerTest extends SapphireTest
|
||||||
|
{
|
||||||
|
protected static $fixture_file = 'RestfulServerTest.yml';
|
||||||
|
|
||||||
|
protected $baseURI = 'http://www.fakesite.test';
|
||||||
|
|
||||||
|
protected static $extra_dataobjects = [
|
||||||
|
RestfulServerTestComment::class,
|
||||||
|
RestfulServerTestSecretThing::class,
|
||||||
|
RestfulServerTestPage::class,
|
||||||
|
RestfulServerTestAuthor::class,
|
||||||
|
RestfulServerTestAuthorRating::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function urlSafeClassname($classname)
|
||||||
|
{
|
||||||
|
return str_replace('\\', '-', $classname);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Director::config()->set('alternate_base_url', $this->baseURI);
|
||||||
|
Security::setCurrentUser(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccess()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
$page1 = $this->objFromFixture(RestfulServerTestPage::class, 'page1');
|
||||||
|
|
||||||
|
// normal GET should succeed with $api_access enabled
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||||
|
|
||||||
|
// even with logged in user a GET with $api_access disabled should fail
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestPage::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $page1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessBoolean()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertContains('<ID>', $response->getBody());
|
||||||
|
$this->assertContains('<Name>', $response->getBody());
|
||||||
|
$this->assertContains('<Comment>', $response->getBody());
|
||||||
|
$this->assertContains('<Page', $response->getBody());
|
||||||
|
$this->assertContains('<Author', $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthenticatedGET()
|
||||||
|
{
|
||||||
|
$thing1 = $this->objFromFixture(RestfulServerTestSecretThing::class, 'thing1');
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
// @todo create additional mock object with authenticated VIEW permissions
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $thing1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthenticatedPUT()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$data = array('Comment' => 'created');
|
||||||
|
|
||||||
|
$response = Director::test($url, $data, null, 'PUT');
|
||||||
|
$this->assertEquals(401, $response->getStatusCode()); // Permission failure
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
$response = Director::test($url, $data, null, 'PUT');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGETRelationshipsXML()
|
||||||
|
{
|
||||||
|
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||||
|
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||||
|
$rating2 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating2');
|
||||||
|
|
||||||
|
// @todo should be set up by fixtures, doesn't work for some reason...
|
||||||
|
$author1->Ratings()->add($rating1);
|
||||||
|
$author1->Ratings()->add($rating2);
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$responseArr = Convert::xml2array($response->getBody());
|
||||||
|
$xmlTagSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$ratingsArr = $responseArr['Ratings'][$xmlTagSafeClassName];
|
||||||
|
$this->assertEquals(2, count($ratingsArr));
|
||||||
|
$ratingIDs = array(
|
||||||
|
(int)$ratingsArr[0]['@attributes']['id'],
|
||||||
|
(int)$ratingsArr[1]['@attributes']['id']
|
||||||
|
);
|
||||||
|
$this->assertContains($rating1->ID, $ratingIDs);
|
||||||
|
$this->assertContains($rating2->ID, $ratingIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGETManyManyRelationshipsXML()
|
||||||
|
{
|
||||||
|
// author4 has related authors author2 and author3
|
||||||
|
$author2 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author2');
|
||||||
|
$author3 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author3');
|
||||||
|
$author4 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author4');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author4->ID . '/RelatedAuthors';
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$arr = Convert::xml2array($response->getBody());
|
||||||
|
$xmlSafeClassName = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$authorsArr = $arr[$xmlSafeClassName];
|
||||||
|
|
||||||
|
$this->assertEquals(2, count($authorsArr));
|
||||||
|
$ratingIDs = array(
|
||||||
|
(int)$authorsArr[0]['ID'],
|
||||||
|
(int)$authorsArr[1]['ID']
|
||||||
|
);
|
||||||
|
$this->assertContains($author2->ID, $ratingIDs);
|
||||||
|
$this->assertContains($author3->ID, $ratingIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPUTWithFormEncoded()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$body = 'Name=Updated Comment&Comment=updated';
|
||||||
|
$headers = array(
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
$response = Director::test($url, null, null, 'PUT', $body, $headers);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||||
|
// Assumption: XML is default output
|
||||||
|
$responseArr = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $responseArr['ID']);
|
||||||
|
$this->assertEquals('updated', $responseArr['Comment']);
|
||||||
|
$this->assertEquals('Updated Comment', $responseArr['Name']);
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPOSTWithFormEncoded()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname";
|
||||||
|
$body = 'Name=New Comment&Comment=created';
|
||||||
|
$headers = array(
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
$response = Director::test($url, null, null, 'POST', $body, $headers);
|
||||||
|
$this->assertEquals(201, $response->getStatusCode()); // Created
|
||||||
|
// Assumption: XML is default output
|
||||||
|
$responseArr = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertTrue($responseArr['ID'] > 0);
|
||||||
|
$this->assertNotEquals($responseArr['ID'], $comment1->ID);
|
||||||
|
$this->assertEquals('created', $responseArr['Comment']);
|
||||||
|
$this->assertEquals('New Comment', $responseArr['Name']);
|
||||||
|
$this->assertEquals(
|
||||||
|
Controller::join_links($url, $responseArr['ID'] . '.xml'),
|
||||||
|
$response->getHeader('Location')
|
||||||
|
);
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostWithoutBodyReturnsNoContent()
|
||||||
|
{
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
|
||||||
|
$url = "{$this->baseURI}/api/v1/" . RestfulServerTestComment::class;
|
||||||
|
$response = Director::test($url, null, null, 'POST');
|
||||||
|
|
||||||
|
$this->assertEquals('No Content', $response->getBody());
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPUTwithJSON()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
|
||||||
|
// by acceptance mimetype
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$body = '{"Comment":"updated"}';
|
||||||
|
$response = Director::test($url, null, null, 'PUT', $body, array(
|
||||||
|
'Content-Type'=>'application/json',
|
||||||
|
'Accept' => 'application/json'
|
||||||
|
));
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||||
|
$obj = Convert::json2obj($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $obj->ID);
|
||||||
|
$this->assertEquals('updated', $obj->Comment);
|
||||||
|
|
||||||
|
// by extension
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/{$comment1->ID}.json";
|
||||||
|
$body = '{"Comment":"updated"}';
|
||||||
|
$response = Director::test($url, null, null, 'PUT', $body);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||||
|
$this->assertEquals($url, $response->getHeader('Location'));
|
||||||
|
$obj = Convert::json2obj($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $obj->ID);
|
||||||
|
$this->assertEquals('updated', $obj->Comment);
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPUTwithXML()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
|
||||||
|
// by mimetype
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$body = '<RestfulServerTestComment><Comment>updated</Comment></RestfulServerTestComment>';
|
||||||
|
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml'));
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||||
|
$obj = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $obj['ID']);
|
||||||
|
$this->assertEquals('updated', $obj['Comment']);
|
||||||
|
|
||||||
|
// by extension
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/{$comment1->ID}.xml";
|
||||||
|
$body = '<RestfulServerTestComment><Comment>updated</Comment></RestfulServerTestComment>';
|
||||||
|
$response = Director::test($url, null, null, 'PUT', $body);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Updated
|
||||||
|
$this->assertEquals($url, $response->getHeader('Location'));
|
||||||
|
$obj = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $obj['ID']);
|
||||||
|
$this->assertEquals('updated', $obj['Comment']);
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHTTPAcceptAndContentType()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
|
||||||
|
$headers = array('Accept' => 'application/json');
|
||||||
|
$response = Director::test($url, null, null, 'GET', null, $headers);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode()); // Success
|
||||||
|
$obj = Convert::json2obj($response->getBody());
|
||||||
|
$this->assertEquals($comment1->ID, $obj->ID);
|
||||||
|
$this->assertEquals('application/json', $response->getHeader('Content-Type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotFound()
|
||||||
|
{
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/99";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMethodNotAllowed()
|
||||||
|
{
|
||||||
|
$comment1 = $this->objFromFixture(RestfulServerTestComment::class, 'comment1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $comment1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
|
||||||
|
$this->assertEquals(405, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConflictOnExistingResourceWhenUsingPost()
|
||||||
|
{
|
||||||
|
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'POST');
|
||||||
|
$this->assertEquals(409, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnsupportedMediaType()
|
||||||
|
{
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'user';
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestComment::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname";
|
||||||
|
$data = "Comment||\/||updated"; // weird format
|
||||||
|
$headers = array('Content-Type' => 'text/weirdformat');
|
||||||
|
$response = Director::test($url, null, null, 'POST', $data, $headers);
|
||||||
|
$this->assertEquals(415, $response->getStatusCode());
|
||||||
|
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testXMLValueFormatting()
|
||||||
|
{
|
||||||
|
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertContains('<ID>' . $rating1->ID . '</ID>', $response->getBody());
|
||||||
|
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessFieldRestrictions()
|
||||||
|
{
|
||||||
|
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||||
|
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertContains('<ID>', $response->getBody());
|
||||||
|
$this->assertContains('<Rating>', $response->getBody());
|
||||||
|
$this->assertContains('<Author', $response->getBody());
|
||||||
|
$this->assertNotContains('<SecretField>', $response->getBody());
|
||||||
|
$this->assertNotContains('<SecretRelation>', $response->getBody());
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertNotContains(
|
||||||
|
'<SecretField>',
|
||||||
|
$response->getBody(),
|
||||||
|
'"add_fields" URL parameter filters out disallowed fields from $api_access'
|
||||||
|
);
|
||||||
|
$this->assertNotContains(
|
||||||
|
'<SecretRelation>',
|
||||||
|
$response->getBody(),
|
||||||
|
'"add_fields" URL parameter filters out disallowed relations from $api_access'
|
||||||
|
);
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID . '?fields=SecretField,SecretRelation';
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertNotContains(
|
||||||
|
'<SecretField>',
|
||||||
|
$response->getBody(),
|
||||||
|
'"fields" URL parameter filters out disallowed fields from $api_access'
|
||||||
|
);
|
||||||
|
$this->assertNotContains(
|
||||||
|
'<SecretRelation>',
|
||||||
|
$response->getBody(),
|
||||||
|
'"fields" URL parameter filters out disallowed relations from $api_access'
|
||||||
|
);
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . '/Ratings';
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertContains(
|
||||||
|
'<Rating>',
|
||||||
|
$response->getBody(),
|
||||||
|
'Relation viewer shows fields allowed through $api_access'
|
||||||
|
);
|
||||||
|
$this->assertNotContains(
|
||||||
|
'<SecretField>',
|
||||||
|
$response->getBody(),
|
||||||
|
'Relation viewer on has-many filters out disallowed fields from $api_access'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessRelationRestrictionsInline()
|
||||||
|
{
|
||||||
|
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID;
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
|
||||||
|
$this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessRelationRestrictionsOnEndpoint()
|
||||||
|
{
|
||||||
|
$author1 = $this->objFromFixture(RestfulServerTestAuthor::class, 'author1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/ProfilePage";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/RelatedPages";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthor::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $author1->ID . "/PublishedPages";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessWithPUT()
|
||||||
|
{
|
||||||
|
$rating1 = $this->objFromFixture(RestfulServerTestAuthorRating::class, 'rating1');
|
||||||
|
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/" . $rating1->ID;
|
||||||
|
$data = array(
|
||||||
|
'Rating' => '42',
|
||||||
|
'WriteProtectedField' => 'haxx0red'
|
||||||
|
);
|
||||||
|
$response = Director::test($url, $data, null, 'PUT');
|
||||||
|
// Assumption: XML is default output
|
||||||
|
$responseArr = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals(42, $responseArr['Rating']);
|
||||||
|
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJSONDataFormatter()
|
||||||
|
{
|
||||||
|
$formatter = new JSONDataFormatter();
|
||||||
|
$editor = $this->objFromFixture(Member::class, 'editor');
|
||||||
|
$user = $this->objFromFixture(Member::class, 'user');
|
||||||
|
|
||||||
|
// The DataFormatter performs canView calls
|
||||||
|
// these are `Member`s so we need to be ADMIN types
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'{"FirstName":"Editor","Email":"editor@test.com"}',
|
||||||
|
$formatter->convertDataObject($editor, ["FirstName", "Email"]),
|
||||||
|
"Correct JSON formatting with field subset"
|
||||||
|
);
|
||||||
|
|
||||||
|
$set = Member::get()
|
||||||
|
->filter('ID', [$editor->ID, $user->ID])
|
||||||
|
->sort('"Email" ASC'); // for sorting for postgres
|
||||||
|
$this->assertEquals(
|
||||||
|
'{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' .
|
||||||
|
'{"FirstName":"User","Email":"user@test.com"}]}',
|
||||||
|
$formatter->convertDataObjectSet($set, ["FirstName", "Email"]),
|
||||||
|
"Correct JSON formatting on a dataobjectset with field filter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApiAccessWithPOST()
|
||||||
|
{
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestAuthorRating::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||||
|
$data = [
|
||||||
|
'Rating' => '42',
|
||||||
|
'WriteProtectedField' => 'haxx0red'
|
||||||
|
];
|
||||||
|
$response = Director::test($url, $data, null, 'POST');
|
||||||
|
// Assumption: XML is default output
|
||||||
|
$responseArr = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals(42, $responseArr['Rating']);
|
||||||
|
$this->assertNotEquals('haxx0red', $responseArr['WriteProtectedField']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCanViewRespectedInList()
|
||||||
|
{
|
||||||
|
// Default content type
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||||
|
|
||||||
|
// JSON content type
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname.json";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertNotContains('Unspeakable', $response->getBody());
|
||||||
|
$responseArray = Convert::json2array($response->getBody());
|
||||||
|
$this->assertSame(0, $responseArray['totalSize']);
|
||||||
|
|
||||||
|
// With authentication
|
||||||
|
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
||||||
|
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
||||||
|
$urlSafeClassname = $this->urlSafeClassname(RestfulServerTestSecretThing::class);
|
||||||
|
$url = "{$this->baseURI}/api/v1/$urlSafeClassname/";
|
||||||
|
$response = Director::test($url, null, null, 'GET');
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertContains('Unspeakable', $response->getBody());
|
||||||
|
// Assumption: default formatter is XML
|
||||||
|
$responseArray = Convert::xml2array($response->getBody());
|
||||||
|
$this->assertEquals(1, $responseArray['@attributes']['totalSize']);
|
||||||
|
unset($_SERVER['PHP_AUTH_USER']);
|
||||||
|
unset($_SERVER['PHP_AUTH_PW']);
|
||||||
|
}
|
||||||
|
}
|
66
tests/RestfulServerTest.yml
Normal file
66
tests/RestfulServerTest.yml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
SilverStripe\Security\Member:
|
||||||
|
editor:
|
||||||
|
FirstName: Editor
|
||||||
|
Email: editor@test.com
|
||||||
|
Password: editor
|
||||||
|
user:
|
||||||
|
FirstName: User
|
||||||
|
Email: user@test.com
|
||||||
|
Password: user
|
||||||
|
SilverStripe\Security\Group:
|
||||||
|
editorgroup:
|
||||||
|
Title: Editors
|
||||||
|
Code: editors
|
||||||
|
Members: =>SilverStripe\Security\Member.editor
|
||||||
|
usergroup:
|
||||||
|
Title: Users
|
||||||
|
Code: users
|
||||||
|
Members: =>SilverStripe\Security\Member.user
|
||||||
|
SilverStripe\Security\Permission:
|
||||||
|
perm1:
|
||||||
|
Code: CREATE_Comment
|
||||||
|
Group: =>SilverStripe\Security\Group.usergroup
|
||||||
|
perm3:
|
||||||
|
Code: EDIT_Comment
|
||||||
|
Group: =>SilverStripe\Security\Group.editorgroup
|
||||||
|
perm4:
|
||||||
|
Code: DELETE_Comment
|
||||||
|
Group: =>SilverStripe\Security\Group.editorgroup
|
||||||
|
perm5:
|
||||||
|
Code: CREATE_Comment
|
||||||
|
Group: =>SilverStripe\Security\Group.editorgroup
|
||||||
|
perm6:
|
||||||
|
Code: VIEW_SecretThing
|
||||||
|
Group: =>SilverStripe\Security\Group.editorgroup
|
||||||
|
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage:
|
||||||
|
page1:
|
||||||
|
Title: Testpage without API Access
|
||||||
|
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment:
|
||||||
|
comment1:
|
||||||
|
Name: Joe
|
||||||
|
Comment: This is a test comment
|
||||||
|
Page: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage.page1
|
||||||
|
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor:
|
||||||
|
author1:
|
||||||
|
FirstName: Author 1
|
||||||
|
author2:
|
||||||
|
FirstName: Author 2
|
||||||
|
author3:
|
||||||
|
Firstname: Author 3
|
||||||
|
author4:
|
||||||
|
FirstName: Author 4
|
||||||
|
RelatedAuthors: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author2,=>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author3
|
||||||
|
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating:
|
||||||
|
rating1:
|
||||||
|
Rating: 3
|
||||||
|
WriteProtectedField: Dont overwrite me
|
||||||
|
SecretField: Dont look at me!
|
||||||
|
Author: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||||
|
SecretRelation: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||||
|
rating2:
|
||||||
|
Rating: 5
|
||||||
|
Author: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||||
|
SecretRelation: =>SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor.author1
|
||||||
|
SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestSecretThing:
|
||||||
|
thing1:
|
||||||
|
Name: Unspeakable
|
35
tests/Stubs/RestfulServerTestAuthor.php
Normal file
35
tests/Stubs/RestfulServerTestAuthor.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||||
|
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthorRating;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
class RestfulServerTestAuthor extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $api_access = true;
|
||||||
|
|
||||||
|
private static $table_name = 'RestfulServerTestAuthor';
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Name' => 'Text',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $many_many = array(
|
||||||
|
'RelatedPages' => RestfulServerTestPage::class,
|
||||||
|
'RelatedAuthors' => RestfulServerTestAuthor::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
'PublishedPages' => RestfulServerTestPage::class,
|
||||||
|
'Ratings' => RestfulServerTestAuthorRating::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
public function canView($member = null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
49
tests/Stubs/RestfulServerTestAuthorRating.php
Normal file
49
tests/Stubs/RestfulServerTestAuthorRating.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||||
|
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
class RestfulServerTestAuthorRating extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $api_access = array(
|
||||||
|
'view' => array(
|
||||||
|
'Rating',
|
||||||
|
'WriteProtectedField',
|
||||||
|
'Author'
|
||||||
|
),
|
||||||
|
'edit' => array(
|
||||||
|
'Rating'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $table_name = 'RestfulServerTestAuthorRating';
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Rating' => 'Int',
|
||||||
|
'SecretField' => 'Text',
|
||||||
|
'WriteProtectedField' => 'Text',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Author' => RestfulServerTestAuthor::class,
|
||||||
|
'SecretRelation' => RestfulServerTestAuthor::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
public function canView($member = null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canEdit($member = null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canCreate($member = null, $context = array())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
61
tests/Stubs/RestfulServerTestComment.php
Normal file
61
tests/Stubs/RestfulServerTestComment.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||||
|
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestPage;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||||
|
use SilverStripe\Security\Permission;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Security\PermissionProvider;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everybody can view comments, logged in members in the "users" group can create comments,
|
||||||
|
* but only "editors" can edit or delete them.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class RestfulServerTestComment extends DataObject implements PermissionProvider, TestOnly
|
||||||
|
{
|
||||||
|
private static $api_access = true;
|
||||||
|
|
||||||
|
private static $table_name = 'RestfulServerTestComment';
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
"Name" => "Varchar(255)",
|
||||||
|
"Comment" => "Text"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Page' => RestfulServerTestPage::class,
|
||||||
|
'Author' => RestfulServerTestAuthor::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
public function providePermissions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'EDIT_Comment' => 'Edit Comment Objects',
|
||||||
|
'CREATE_Comment' => 'Create Comment Objects',
|
||||||
|
'DELETE_Comment' => 'Delete Comment Objects',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canView($member = null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canEdit($member = null)
|
||||||
|
{
|
||||||
|
return Permission::checkMember($member, 'EDIT_Comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canDelete($member = null)
|
||||||
|
{
|
||||||
|
return Permission::checkMember($member, 'DELETE_Comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canCreate($member = null, $context = array())
|
||||||
|
{
|
||||||
|
return Permission::checkMember($member, 'CREATE_Comment');
|
||||||
|
}
|
||||||
|
}
|
32
tests/Stubs/RestfulServerTestPage.php
Normal file
32
tests/Stubs/RestfulServerTestPage.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||||
|
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestAuthor;
|
||||||
|
use SilverStripe\RestfulServer\Tests\Stubs\RestfulServerTestComment;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
class RestfulServerTestPage extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $api_access = false;
|
||||||
|
|
||||||
|
private static $table_name = 'RestfulServerTestPage';
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Text',
|
||||||
|
'Content' => 'HTMLText',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Author' => RestfulServerTestAuthor::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_many = array(
|
||||||
|
'TestComments' => RestfulServerTestComment::class
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $belongs_many_many = array(
|
||||||
|
'RelatedAuthors' => RestfulServerTestAuthor::class,
|
||||||
|
);
|
||||||
|
}
|
31
tests/Stubs/RestfulServerTestSecretThing.php
Normal file
31
tests/Stubs/RestfulServerTestSecretThing.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\RestfulServer\Tests\Stubs;
|
||||||
|
|
||||||
|
use SilverStripe\Security\Permission;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\Security\PermissionProvider;
|
||||||
|
|
||||||
|
class RestfulServerTestSecretThing extends DataObject implements TestOnly, PermissionProvider
|
||||||
|
{
|
||||||
|
private static $api_access = true;
|
||||||
|
|
||||||
|
private static $table_name = 'RestfulServerTestSecretThing';
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
"Name" => "Varchar(255)",
|
||||||
|
);
|
||||||
|
|
||||||
|
public function canView($member = null)
|
||||||
|
{
|
||||||
|
return Permission::checkMember($member, 'VIEW_SecretThing');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function providePermissions()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'VIEW_SecretThing' => 'View Secret Things',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ruleset name="SilverStripe">
|
|
||||||
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
|
|
||||||
|
|
||||||
<!-- exclude SCSS-generated CSS files -->
|
|
||||||
<exclude-pattern>*/css/*</exclude-pattern>
|
|
||||||
<exclude-pattern>css/*</exclude-pattern>
|
|
||||||
|
|
||||||
<!-- exclude thirdparty content -->
|
|
||||||
<exclude-pattern>thirdparty/*</exclude-pattern>
|
|
||||||
|
|
||||||
<rule ref="Generic.Files.LineEndings.InvalidEOLChar">
|
|
||||||
<severity>8</severity>
|
|
||||||
</rule>
|
|
||||||
<rule ref="Generic.Files.LineEndings">
|
|
||||||
<properties>
|
|
||||||
<property name="eolChar" value="\n" />
|
|
||||||
</properties>
|
|
||||||
</rule>
|
|
||||||
<rule ref="Generic.Files.LineLength.TooLong">
|
|
||||||
<severity>7</severity>
|
|
||||||
</rule>
|
|
||||||
<rule ref="Generic.Files.LineLength.MaxExceeded">
|
|
||||||
<severity>8</severity>
|
|
||||||
</rule>
|
|
||||||
<rule ref="Generic.Files.LineLength">
|
|
||||||
<properties>
|
|
||||||
<property name="lineLimit" value="120"/>
|
|
||||||
<property name="absoluteLineLimit" value="120"/>
|
|
||||||
</properties>
|
|
||||||
</rule>
|
|
||||||
</ruleset>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
global $project;
|
|
||||||
$project = 'mysite';
|
|
||||||
|
|
||||||
global $database;
|
|
||||||
$database = '';
|
|
||||||
|
|
||||||
require_once('conf/ConfigureFromEnv.php');
|
|
||||||
|
|
||||||
global $databaseConfig;
|
|
||||||
$databaseConfig['memory'] = true;
|
|
||||||
$databaseConfig['path'] = dirname(dirname(__FILE__)) .'/assets/';
|
|
||||||
|
|
||||||
MySQLDatabase::set_connection_charset('utf8');
|
|
||||||
|
|
||||||
// Set the current theme. More themes can be downloaded from
|
|
||||||
// http://www.silverstripe.org/themes/
|
|
||||||
SSViewer::set_theme('simple');
|
|
||||||
|
|
||||||
// Enable nested URLs for this site (e.g. page/sub-page/)
|
|
||||||
if (class_exists('SiteTree')) {
|
|
||||||
SiteTree::enable_nested_urls();
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
ob_start();
|
|
||||||
|
|
||||||
define('SS_ENVIRONMENT_TYPE', 'dev');
|
|
||||||
|
|
||||||
/* Database connection */
|
|
||||||
$db = getenv('TESTDB');
|
|
||||||
switch ($db) {
|
|
||||||
case "PGSQL";
|
|
||||||
define('SS_DATABASE_CLASS', 'PostgreSQLDatabase');
|
|
||||||
define('SS_DATABASE_USERNAME', 'postgres');
|
|
||||||
define('SS_DATABASE_PASSWORD', '');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "MYSQL":
|
|
||||||
define('SS_DATABASE_CLASS', 'MySQLDatabase');
|
|
||||||
define('SS_DATABASE_USERNAME', 'root');
|
|
||||||
define('SS_DATABASE_PASSWORD', '');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
define('SS_DATABASE_CLASS', 'SQLitePDODatabase');
|
|
||||||
define('SS_DATABASE_USERNAME', 'root');
|
|
||||||
define('SS_DATABASE_PASSWORD', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
echo SS_DATABASE_CLASS;
|
|
||||||
|
|
||||||
define('SS_DATABASE_SERVER', 'localhost');
|
|
||||||
define('SS_DATABASE_CHOOSE_NAME', true);
|
|
||||||
|
|
||||||
|
|
||||||
/* Configure a default username and password to access the CMS on all sites in this environment. */
|
|
||||||
define('SS_DEFAULT_ADMIN_USERNAME', 'username');
|
|
||||||
define('SS_DEFAULT_ADMIN_PASSWORD', 'password');
|
|
||||||
|
|
||||||
$_FILE_TO_URL_MAPPING[dirname(__FILE__)] = 'http://localhost';
|
|
@ -1,23 +0,0 @@
|
|||||||
### USAGE: before_script <base-folder>
|
|
||||||
|
|
||||||
BUILD_DIR=$1
|
|
||||||
|
|
||||||
# Fetch all dependencies
|
|
||||||
# TODO Replace with different composer.json variations
|
|
||||||
echo "Checking out installer@$CORE_RELEASE"
|
|
||||||
git clone --depth=100 --quiet --branch $CORE_RELEASE git://github.com/silverstripe/silverstripe-installer.git $BUILD_DIR
|
|
||||||
echo "Checking out framework@$CORE_RELEASE"
|
|
||||||
git clone --depth=100 --quiet --branch $CORE_RELEASE git://github.com/silverstripe/sapphire.git $BUILD_DIR/framework
|
|
||||||
echo "Checking out sqlite3"
|
|
||||||
git clone --depth=100 --quiet git://github.com/silverstripe-labs/silverstripe-sqlite3.git $BUILD_DIR/sqlite3
|
|
||||||
echo "Checking out postgresql"
|
|
||||||
git clone --depth=100 --quiet git://github.com/silverstripe/silverstripe-postgresql.git $BUILD_DIR/postgresql
|
|
||||||
|
|
||||||
# Copy setup files
|
|
||||||
cp ./tests/travis/_ss_environment.php $BUILD_DIR
|
|
||||||
cp ./tests/travis/_config.php $BUILD_DIR/mysite
|
|
||||||
|
|
||||||
# Copy actual project code into build directory (checked out by travis)
|
|
||||||
cp -r . $BUILD_DIR/restfulserver
|
|
||||||
|
|
||||||
cd $BUILD_DIR
|
|
@ -1,651 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @todo Test Relation getters
|
|
||||||
* @todo Test filter and limit through GET params
|
|
||||||
* @todo Test DELETE verb
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class RestfulServerTest extends SapphireTest
|
|
||||||
{
|
|
||||||
public static $fixture_file = 'RestfulServerTest.yml';
|
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
|
||||||
'RestfulServerTest_Comment',
|
|
||||||
'RestfulServerTest_SecretThing',
|
|
||||||
'RestfulServerTest_Page',
|
|
||||||
'RestfulServerTest_Author',
|
|
||||||
'RestfulServerTest_AuthorRating',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function testApiAccess()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
$page1 = $this->objFromFixture('RestfulServerTest_Page', 'page1');
|
|
||||||
|
|
||||||
// normal GET should succeed with $api_access enabled
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
|
||||||
|
|
||||||
// even with logged in user a GET with $api_access disabled should fail
|
|
||||||
$url = "/api/v1/RestfulServerTest_Page/" . $page1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 401);
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessBoolean()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertContains('<ID>', $response->getBody());
|
|
||||||
$this->assertContains('<Name>', $response->getBody());
|
|
||||||
$this->assertContains('<Comment>', $response->getBody());
|
|
||||||
$this->assertContains('<Page', $response->getBody());
|
|
||||||
$this->assertContains('<Author', $response->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAuthenticatedGET()
|
|
||||||
{
|
|
||||||
$thing1 = $this->objFromFixture('RestfulServerTest_SecretThing', 'thing1');
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
// @todo create additional mock object with authenticated VIEW permissions
|
|
||||||
$url = "/api/v1/RestfulServerTest_SecretThing/" . $thing1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 401);
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAuthenticatedPUT()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$data = array('Comment' => 'created');
|
|
||||||
|
|
||||||
$response = Director::test($url, $data, null, 'PUT');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 401); // Permission failure
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
$response = Director::test($url, $data, null, 'PUT');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGETRelationshipsXML()
|
|
||||||
{
|
|
||||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
|
||||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
|
||||||
$rating2 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating2');
|
|
||||||
|
|
||||||
// @todo should be set up by fixtures, doesn't work for some reason...
|
|
||||||
$author1->Ratings()->add($rating1);
|
|
||||||
$author1->Ratings()->add($rating2);
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
|
|
||||||
$responseArr = Convert::xml2array($response->getBody());
|
|
||||||
$ratingsArr = $responseArr['Ratings']['RestfulServerTest_AuthorRating'];
|
|
||||||
$this->assertEquals(count($ratingsArr), 2);
|
|
||||||
$ratingIDs = array(
|
|
||||||
(int)$ratingsArr[0]['@attributes']['id'],
|
|
||||||
(int)$ratingsArr[1]['@attributes']['id']
|
|
||||||
);
|
|
||||||
$this->assertContains($rating1->ID, $ratingIDs);
|
|
||||||
$this->assertContains($rating2->ID, $ratingIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGETManyManyRelationshipsXML()
|
|
||||||
{
|
|
||||||
// author4 has related authors author2 and author3
|
|
||||||
$author2 = $this->objFromFixture('RestfulServerTest_Author', 'author2');
|
|
||||||
$author3 = $this->objFromFixture('RestfulServerTest_Author', 'author3');
|
|
||||||
$author4 = $this->objFromFixture('RestfulServerTest_Author', 'author4');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors';
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
|
||||||
$arr = Convert::xml2array($response->getBody());
|
|
||||||
$authorsArr = $arr['RestfulServerTest_Author'];
|
|
||||||
|
|
||||||
$this->assertEquals(count($authorsArr), 2);
|
|
||||||
$ratingIDs = array(
|
|
||||||
(int)$authorsArr[0]['ID'],
|
|
||||||
(int)$authorsArr[1]['ID']
|
|
||||||
);
|
|
||||||
$this->assertContains($author2->ID, $ratingIDs);
|
|
||||||
$this->assertContains($author3->ID, $ratingIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPUTWithFormEncoded()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$body = 'Name=Updated Comment&Comment=updated';
|
|
||||||
$headers = array(
|
|
||||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
|
||||||
);
|
|
||||||
$response = Director::test($url, null, null, 'PUT', $body, $headers);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
|
||||||
// Assumption: XML is default output
|
|
||||||
$responseArr = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals($responseArr['ID'], $comment1->ID);
|
|
||||||
$this->assertEquals($responseArr['Comment'], 'updated');
|
|
||||||
$this->assertEquals($responseArr['Name'], 'Updated Comment');
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPOSTWithFormEncoded()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment";
|
|
||||||
$body = 'Name=New Comment&Comment=created';
|
|
||||||
$headers = array(
|
|
||||||
'Content-Type' => 'application/x-www-form-urlencoded'
|
|
||||||
);
|
|
||||||
$response = Director::test($url, null, null, 'POST', $body, $headers);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 201); // Created
|
|
||||||
// Assumption: XML is default output
|
|
||||||
$responseArr = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertTrue($responseArr['ID'] > 0);
|
|
||||||
$this->assertNotEquals($responseArr['ID'], $comment1->ID);
|
|
||||||
$this->assertEquals($responseArr['Comment'], 'created');
|
|
||||||
$this->assertEquals($responseArr['Name'], 'New Comment');
|
|
||||||
$this->assertEquals(
|
|
||||||
$response->getHeader('Location'),
|
|
||||||
Controller::join_links(Director::absoluteBaseURL(), $url, $responseArr['ID'])
|
|
||||||
);
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPostWithoutBodyReturnsNoContent()
|
|
||||||
{
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
|
|
||||||
$url = '/api/v1/RestfulServerTest_Comment';
|
|
||||||
$response = Director::test($url, null, null, 'POST');
|
|
||||||
|
|
||||||
$this->assertEquals('No Content', $response->getBody());
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPUTwithJSON()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
|
|
||||||
// by mimetype
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$body = '{"Comment":"updated"}';
|
|
||||||
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'application/json'));
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
|
||||||
$obj = Convert::json2obj($response->getBody());
|
|
||||||
$this->assertEquals($obj->ID, $comment1->ID);
|
|
||||||
$this->assertEquals($obj->Comment, 'updated');
|
|
||||||
|
|
||||||
// by extension
|
|
||||||
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.json", $comment1->ID);
|
|
||||||
$body = '{"Comment":"updated"}';
|
|
||||||
$response = Director::test($url, null, null, 'PUT', $body);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
|
||||||
$this->assertEquals(
|
|
||||||
$response->getHeader('Location'),
|
|
||||||
Controller::join_links(Director::absoluteBaseURL(), $url)
|
|
||||||
);
|
|
||||||
$obj = Convert::json2obj($response->getBody());
|
|
||||||
$this->assertEquals($obj->ID, $comment1->ID);
|
|
||||||
$this->assertEquals($obj->Comment, 'updated');
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPUTwithXML()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
|
|
||||||
// by mimetype
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
|
|
||||||
$response = Director::test($url, null, null, 'PUT', $body, array('Content-Type'=>'text/xml'));
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
|
||||||
$obj = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals($obj['ID'], $comment1->ID);
|
|
||||||
$this->assertEquals($obj['Comment'], 'updated');
|
|
||||||
|
|
||||||
// by extension
|
|
||||||
$url = sprintf("/api/v1/RestfulServerTest_Comment/%d.xml", $comment1->ID);
|
|
||||||
$body = '<RestfulServerTest_Comment><Comment>updated</Comment></RestfulServerTest_Comment>';
|
|
||||||
$response = Director::test($url, null, null, 'PUT', $body);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Updated
|
|
||||||
$this->assertEquals(
|
|
||||||
$response->getHeader('Location'),
|
|
||||||
Controller::join_links(Director::absoluteBaseURL(), $url)
|
|
||||||
);
|
|
||||||
$obj = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals($obj['ID'], $comment1->ID);
|
|
||||||
$this->assertEquals($obj['Comment'], 'updated');
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testHTTPAcceptAndContentType()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
|
|
||||||
$headers = array('Accept' => 'application/json');
|
|
||||||
$response = Director::test($url, null, null, 'GET', null, $headers);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200); // Success
|
|
||||||
$obj = Convert::json2obj($response->getBody());
|
|
||||||
$this->assertEquals($obj->ID, $comment1->ID);
|
|
||||||
$this->assertEquals($response->getHeader('Content-Type'), 'application/json');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNotFound()
|
|
||||||
{
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/99";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 404);
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testMethodNotAllowed()
|
|
||||||
{
|
|
||||||
$comment1 = $this->objFromFixture('RestfulServerTest_Comment', 'comment1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment/" . $comment1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'UNKNOWNHTTPMETHOD');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testConflictOnExistingResourceWhenUsingPost()
|
|
||||||
{
|
|
||||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'POST');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUnsupportedMediaType()
|
|
||||||
{
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'user@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'user';
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Comment";
|
|
||||||
$data = "Comment||\/||updated"; // weird format
|
|
||||||
$headers = array('Content-Type' => 'text/weirdformat');
|
|
||||||
$response = Director::test($url, null, null, 'POST', $data, $headers);
|
|
||||||
$this->assertEquals($response->getStatusCode(), 415);
|
|
||||||
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testXMLValueFormatting()
|
|
||||||
{
|
|
||||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertContains('<ID>' . $rating1->ID . '</ID>', $response->getBody());
|
|
||||||
$this->assertContains('<Rating>' . $rating1->Rating . '</Rating>', $response->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessFieldRestrictions()
|
|
||||||
{
|
|
||||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
|
||||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertContains('<ID>', $response->getBody());
|
|
||||||
$this->assertContains('<Rating>', $response->getBody());
|
|
||||||
$this->assertContains('<Author', $response->getBody());
|
|
||||||
$this->assertNotContains('<SecretField>', $response->getBody());
|
|
||||||
$this->assertNotContains('<SecretRelation>', $response->getBody());
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?add_fields=SecretField,SecretRelation';
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
|
||||||
'"add_fields" URL parameter filters out disallowed fields from $api_access'
|
|
||||||
);
|
|
||||||
$this->assertNotContains('<SecretRelation>', $response->getBody(),
|
|
||||||
'"add_fields" URL parameter filters out disallowed relations from $api_access'
|
|
||||||
);
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID . '?fields=SecretField,SecretRelation';
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
|
||||||
'"fields" URL parameter filters out disallowed fields from $api_access'
|
|
||||||
);
|
|
||||||
$this->assertNotContains('<SecretRelation>', $response->getBody(),
|
|
||||||
'"fields" URL parameter filters out disallowed relations from $api_access'
|
|
||||||
);
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . '/Ratings';
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertContains('<Rating>', $response->getBody(),
|
|
||||||
'Relation viewer shows fields allowed through $api_access'
|
|
||||||
);
|
|
||||||
$this->assertNotContains('<SecretField>', $response->getBody(),
|
|
||||||
'Relation viewer on has-many filters out disallowed fields from $api_access'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessRelationRestrictionsInline()
|
|
||||||
{
|
|
||||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID;
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertNotContains('<RelatedPages', $response->getBody(), 'Restricts many-many with api_access=false');
|
|
||||||
$this->assertNotContains('<PublishedPages', $response->getBody(), 'Restricts has-many with api_access=false');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessRelationRestrictionsOnEndpoint()
|
|
||||||
{
|
|
||||||
$author1 = $this->objFromFixture('RestfulServerTest_Author', 'author1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/ProfilePage";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-one with api_access=false');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/RelatedPages";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts many-many with api_access=false');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_Author/" . $author1->ID . "/PublishedPages";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals(404, $response->getStatusCode(), 'Restricts has-many with api_access=false');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessWithPUT()
|
|
||||||
{
|
|
||||||
$rating1 = $this->objFromFixture('RestfulServerTest_AuthorRating', 'rating1');
|
|
||||||
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating/" . $rating1->ID;
|
|
||||||
$data = array(
|
|
||||||
'Rating' => '42',
|
|
||||||
'WriteProtectedField' => 'haxx0red'
|
|
||||||
);
|
|
||||||
$response = Director::test($url, $data, null, 'PUT');
|
|
||||||
// Assumption: XML is default output
|
|
||||||
$responseArr = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals($responseArr['Rating'], 42);
|
|
||||||
$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testJSONDataFormatter()
|
|
||||||
{
|
|
||||||
$formatter = new JSONDataFormatter();
|
|
||||||
$editor = $this->objFromFixture('Member', 'editor');
|
|
||||||
$user = $this->objFromFixture('Member', 'user');
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
$formatter->convertDataObject($editor, array("FirstName", "Email")),
|
|
||||||
'{"FirstName":"Editor","Email":"editor@test.com"}',
|
|
||||||
"Correct JSON formatting with field subset");
|
|
||||||
|
|
||||||
$set = DataObject::get(
|
|
||||||
"Member",
|
|
||||||
sprintf('"Member"."ID" IN (%s)', implode(',', array($editor->ID, $user->ID))),
|
|
||||||
'"Email" ASC' // for sorting for postgres
|
|
||||||
);
|
|
||||||
$this->assertEquals(
|
|
||||||
$formatter->convertDataObjectSet($set, array("FirstName", "Email")),
|
|
||||||
'{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},' .
|
|
||||||
'{"FirstName":"User","Email":"user@test.com"}]}',
|
|
||||||
"Correct JSON formatting on a dataobjectset with field filter");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testApiAccessWithPOST()
|
|
||||||
{
|
|
||||||
$url = "/api/v1/RestfulServerTest_AuthorRating";
|
|
||||||
$data = array(
|
|
||||||
'Rating' => '42',
|
|
||||||
'WriteProtectedField' => 'haxx0red'
|
|
||||||
);
|
|
||||||
$response = Director::test($url, $data, null, 'POST');
|
|
||||||
// Assumption: XML is default output
|
|
||||||
$responseArr = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals($responseArr['Rating'], 42);
|
|
||||||
$this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCanViewRespectedInList()
|
|
||||||
{
|
|
||||||
// Default content type
|
|
||||||
$url = "/api/v1/RestfulServerTest_SecretThing/";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
|
||||||
|
|
||||||
// JSON content type
|
|
||||||
$url = "/api/v1/RestfulServerTest_SecretThing.json";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
$this->assertNotContains('Unspeakable', $response->getBody());
|
|
||||||
$responseArray = Convert::json2array($response->getBody());
|
|
||||||
$this->assertSame(0, $responseArray['totalSize']);
|
|
||||||
|
|
||||||
// With authentication
|
|
||||||
$_SERVER['PHP_AUTH_USER'] = 'editor@test.com';
|
|
||||||
$_SERVER['PHP_AUTH_PW'] = 'editor';
|
|
||||||
$url = "/api/v1/RestfulServerTest_SecretThing/";
|
|
||||||
$response = Director::test($url, null, null, 'GET');
|
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
|
||||||
$this->assertContains('Unspeakable', $response->getBody());
|
|
||||||
// Assumption: default formatter is XML
|
|
||||||
$responseArray = Convert::xml2array($response->getBody());
|
|
||||||
$this->assertEquals(1, $responseArray['@attributes']['totalSize']);
|
|
||||||
unset($_SERVER['PHP_AUTH_USER']);
|
|
||||||
unset($_SERVER['PHP_AUTH_PW']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Everybody can view comments, logged in members in the "users" group can create comments,
|
|
||||||
* but only "editors" can edit or delete them.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class RestfulServerTest_Comment extends DataObject implements PermissionProvider,TestOnly
|
|
||||||
{
|
|
||||||
public static $api_access = true;
|
|
||||||
|
|
||||||
public static $db = array(
|
|
||||||
"Name" => "Varchar(255)",
|
|
||||||
"Comment" => "Text"
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $has_one = array(
|
|
||||||
'Page' => 'RestfulServerTest_Page',
|
|
||||||
'Author' => 'RestfulServerTest_Author',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function providePermissions()
|
|
||||||
{
|
|
||||||
return array(
|
|
||||||
'EDIT_Comment' => 'Edit Comment Objects',
|
|
||||||
'CREATE_Comment' => 'Create Comment Objects',
|
|
||||||
'DELETE_Comment' => 'Delete Comment Objects',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canView($member = null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canEdit($member = null)
|
|
||||||
{
|
|
||||||
return Permission::checkMember($member, 'EDIT_Comment');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canDelete($member = null)
|
|
||||||
{
|
|
||||||
return Permission::checkMember($member, 'DELETE_Comment');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canCreate($member = null)
|
|
||||||
{
|
|
||||||
return Permission::checkMember($member, 'CREATE_Comment');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestfulServerTest_SecretThing extends DataObject implements TestOnly,PermissionProvider
|
|
||||||
{
|
|
||||||
public static $api_access = true;
|
|
||||||
|
|
||||||
public static $db = array(
|
|
||||||
"Name" => "Varchar(255)",
|
|
||||||
);
|
|
||||||
|
|
||||||
public function canView($member = null)
|
|
||||||
{
|
|
||||||
return Permission::checkMember($member, 'VIEW_SecretThing');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function providePermissions()
|
|
||||||
{
|
|
||||||
return array(
|
|
||||||
'VIEW_SecretThing' => 'View Secret Things',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestfulServerTest_Page extends DataObject implements TestOnly
|
|
||||||
{
|
|
||||||
public static $api_access = false;
|
|
||||||
|
|
||||||
public static $db = array(
|
|
||||||
'Title' => 'Text',
|
|
||||||
'Content' => 'HTMLText',
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $has_one = array(
|
|
||||||
'Author' => 'RestfulServerTest_Author',
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $has_many = array(
|
|
||||||
'TestComments' => 'RestfulServerTest_Comment'
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $belongs_many_many = array(
|
|
||||||
'RelatedAuthors' => 'RestfulServerTest_Author',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestfulServerTest_Author extends DataObject implements TestOnly
|
|
||||||
{
|
|
||||||
public static $api_access = true;
|
|
||||||
|
|
||||||
public static $db = array(
|
|
||||||
'Name' => 'Text',
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $many_many = array(
|
|
||||||
'RelatedPages' => 'RestfulServerTest_Page',
|
|
||||||
'RelatedAuthors' => 'RestfulServerTest_Author',
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $has_many = array(
|
|
||||||
'PublishedPages' => 'RestfulServerTest_Page',
|
|
||||||
'Ratings' => 'RestfulServerTest_AuthorRating',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function canView($member = null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestfulServerTest_AuthorRating extends DataObject implements TestOnly
|
|
||||||
{
|
|
||||||
public static $api_access = array(
|
|
||||||
'view' => array(
|
|
||||||
'Rating',
|
|
||||||
'WriteProtectedField',
|
|
||||||
'Author'
|
|
||||||
),
|
|
||||||
'edit' => array(
|
|
||||||
'Rating'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $db = array(
|
|
||||||
'Rating' => 'Int',
|
|
||||||
'SecretField' => 'Text',
|
|
||||||
'WriteProtectedField' => 'Text',
|
|
||||||
);
|
|
||||||
|
|
||||||
public static $has_one = array(
|
|
||||||
'Author' => 'RestfulServerTest_Author',
|
|
||||||
'SecretRelation' => 'RestfulServerTest_Author',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function canView($member = null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canEdit($member = null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canCreate($member = null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
Member:
|
|
||||||
editor:
|
|
||||||
FirstName: Editor
|
|
||||||
Email: editor@test.com
|
|
||||||
Password: editor
|
|
||||||
user:
|
|
||||||
FirstName: User
|
|
||||||
Email: user@test.com
|
|
||||||
Password: user
|
|
||||||
Group:
|
|
||||||
editorgroup:
|
|
||||||
Title: Editors
|
|
||||||
Code: editors
|
|
||||||
Members: =>Member.editor
|
|
||||||
usergroup:
|
|
||||||
Title: Users
|
|
||||||
Code: users
|
|
||||||
Members: =>Member.user
|
|
||||||
Permission:
|
|
||||||
perm1:
|
|
||||||
Code: CREATE_Comment
|
|
||||||
Group: =>Group.usergroup
|
|
||||||
perm3:
|
|
||||||
Code: EDIT_Comment
|
|
||||||
Group: =>Group.editorgroup
|
|
||||||
perm4:
|
|
||||||
Code: DELETE_Comment
|
|
||||||
Group: =>Group.editorgroup
|
|
||||||
perm5:
|
|
||||||
Code: CREATE_Comment
|
|
||||||
Group: =>Group.editorgroup
|
|
||||||
perm6:
|
|
||||||
Code: VIEW_SecretThing
|
|
||||||
Group: =>Group.editorgroup
|
|
||||||
RestfulServerTest_Page:
|
|
||||||
page1:
|
|
||||||
Title: Testpage without API Access
|
|
||||||
RestfulServerTest_Comment:
|
|
||||||
comment1:
|
|
||||||
Name: Joe
|
|
||||||
Comment: This is a test comment
|
|
||||||
Page: =>RestfulServerTest_Page.page1
|
|
||||||
RestfulServerTest_Author:
|
|
||||||
author1:
|
|
||||||
FirstName: Author 1
|
|
||||||
author2:
|
|
||||||
FirstName: Author 2
|
|
||||||
author3:
|
|
||||||
Firstname: Author 3
|
|
||||||
author4:
|
|
||||||
FirstName: Author 4
|
|
||||||
RelatedAuthors: =>RestfulServerTest_Author.author2,=>RestfulServerTest_Author.author3
|
|
||||||
RestfulServerTest_AuthorRating:
|
|
||||||
rating1:
|
|
||||||
Rating: 3
|
|
||||||
WriteProtectedField: Dont overwrite me
|
|
||||||
SecretField: Dont look at me!
|
|
||||||
Author: =>RestfulServerTest_Author.author1
|
|
||||||
SecretRelation: =>RestfulServerTest_Author.author1
|
|
||||||
rating2:
|
|
||||||
Rating: 5
|
|
||||||
Author: =>RestfulServerTest_Author.author1
|
|
||||||
SecretRelation: =>RestfulServerTest_Author.author1
|
|
||||||
RestfulServerTest_SecretThing:
|
|
||||||
thing1:
|
|
||||||
Name: Unspeakable
|
|
Loading…
Reference in New Issue
Block a user