mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60235 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
c1440e0b02
commit
b89328e6cc
@ -37,7 +37,19 @@ abstract class DataFormatter extends Object {
|
||||
protected $customFields = null;
|
||||
|
||||
/**
|
||||
* Get a DataFormatter object suitable for handling the given file extension
|
||||
* Specifies the mimetype in which all strings
|
||||
* returned from the convert*() methods should be used,
|
||||
* e.g. "text/xml".
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $outputContentType = null;
|
||||
|
||||
/**
|
||||
* Get a DataFormatter object suitable for handling the given file extension.
|
||||
*
|
||||
* @string $extension
|
||||
* @return DataFormatter
|
||||
*/
|
||||
static function for_extension($extension) {
|
||||
$classes = ClassInfo::subclassesFor("DataFormatter");
|
||||
@ -55,6 +67,58 @@ abstract class DataFormatter extends Object {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatter for the first matching extension.
|
||||
*
|
||||
* @param array $extensions
|
||||
* @return DataFormatter
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @string $mimeType
|
||||
* @return DataFormatter
|
||||
*/
|
||||
static function for_mimetype($mimeType) {
|
||||
$classes = ClassInfo::subclassesFor("DataFormatter");
|
||||
array_shift($classes);
|
||||
$sortedClasses = array();
|
||||
foreach($classes as $class) {
|
||||
$sortedClasses[$class] = singleton($class)->stat('priority');
|
||||
}
|
||||
arsort($sortedClasses);
|
||||
foreach($sortedClasses as $className => $priority) {
|
||||
$formatter = singleton($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
|
||||
*/
|
||||
static function for_mimetypes($mimetypes) {
|
||||
foreach($mimetypes as $mimetype) {
|
||||
if($formatter = self::for_mimetype($mimetype)) return $formatter;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
*/
|
||||
@ -69,6 +133,10 @@ abstract class DataFormatter extends Object {
|
||||
return $this->customFields;
|
||||
}
|
||||
|
||||
public function getOutputContentType() {
|
||||
return $this->outputContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all fields on the object which should be shown
|
||||
* in the output. Can be customised through {@link self::setCustomFields()}.
|
||||
@ -104,6 +172,8 @@ abstract class DataFormatter extends Object {
|
||||
*/
|
||||
abstract function supportedExtensions();
|
||||
|
||||
abstract function supportedMimeTypes();
|
||||
|
||||
|
||||
/**
|
||||
* Convert a single data object to this format. Return a string.
|
||||
@ -114,5 +184,12 @@ abstract class DataFormatter extends Object {
|
||||
* Convert a data object set to this format. Return a string.
|
||||
*/
|
||||
abstract function convertDataObjectSet(DataObjectSet $set);
|
||||
|
||||
/**
|
||||
* @param string $strData HTTP Payload as string
|
||||
*/
|
||||
public function convertStringToArray($strData) {
|
||||
user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
|
||||
}
|
||||
|
||||
}
|
@ -6,8 +6,20 @@ class JSONDataFormatter extends DataFormatter {
|
||||
*/
|
||||
static $api_base = "api/v1/";
|
||||
|
||||
protected $outputContentType = 'application/json';
|
||||
|
||||
public function supportedExtensions() {
|
||||
return array('json', 'js');
|
||||
return array(
|
||||
'json',
|
||||
'js'
|
||||
);
|
||||
}
|
||||
|
||||
public function supportedMimeTypes() {
|
||||
return array(
|
||||
'application/json',
|
||||
'text/x-json'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,4 +92,9 @@ class JSONDataFormatter extends DataFormatter {
|
||||
}
|
||||
return "[\n" . implode(",\n", $jsonParts) . "\n]";
|
||||
}
|
||||
|
||||
public function convertStringToArray($strData) {
|
||||
return Convert::json2array($strData);
|
||||
}
|
||||
|
||||
}
|
@ -12,10 +12,10 @@
|
||||
* applications.
|
||||
*
|
||||
* - GET /api/v1/(ClassName)/(ID) - gets a database record
|
||||
* - GET /api/v1/(ClassName)/(ID)/(Relation) - get all of the records linked to this database record by the given reatlion (NOT IMPLEMENTED YET)
|
||||
* - GET /api/v1/(ClassName)?(Field)=(Val)&(Field)=(Val) - searches for matching database records (NOT IMPLEMENTED YET)
|
||||
* - GET /api/v1/(ClassName)/(ID)/(Relation) - get all of the records linked to this database record by the given reatlion
|
||||
* - GET /api/v1/(ClassName)?(Field)=(Val)&(Field)=(Val) - searches for matching database records
|
||||
*
|
||||
* - PUT /api/v1/(ClassName)/(ID) - updates a database record (NOT IMPLEMENTED YET)
|
||||
* - PUT /api/v1/(ClassName)/(ID) - updates a database record
|
||||
* - PUT /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, replacing the existing record(s) (NOT IMPLEMENTED YET)
|
||||
* - POST /api/v1/(ClassName)/(ID)/(Relation) - updates a relation, appending to the existing record(s) (NOT IMPLEMENTED YET)
|
||||
*
|
||||
@ -34,8 +34,10 @@
|
||||
* - &fields=<string>: Comma-separated list of fields on the output object (defaults to all database-columns)
|
||||
*
|
||||
* @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
|
||||
* @todo Implement PUT/POST/DELETE for relations
|
||||
* @todo Make SearchContext specification customizeable for each class
|
||||
* @todo Allow for range-searches (e.g. on Created column)
|
||||
* @todo Allow other authentication methods (currently only HTTP BasicAuth)
|
||||
*/
|
||||
class RestfulServer extends Controller {
|
||||
static $url_handlers = array(
|
||||
@ -52,7 +54,7 @@ class RestfulServer extends Controller {
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $default_extension = "xml";
|
||||
public static $default_extension = "xml";
|
||||
|
||||
/**
|
||||
* If no extension is given, resolve the request to this mimetype.
|
||||
@ -61,19 +63,6 @@ class RestfulServer extends Controller {
|
||||
*/
|
||||
protected static $default_mimetype = "text/xml";
|
||||
|
||||
/**
|
||||
* Maps common extensions to their mimetype representations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $mimetype_map = array(
|
||||
'xml' => 'text/xml',
|
||||
'json' => 'text/json',
|
||||
'js' => 'text/json',
|
||||
'xhtml' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
);
|
||||
|
||||
/*
|
||||
function handleItem($request) {
|
||||
return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
|
||||
@ -91,24 +80,25 @@ class RestfulServer extends Controller {
|
||||
function index() {
|
||||
ContentNegotiator::disable();
|
||||
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'];
|
||||
if(!isset($this->urlParams['ClassName'])) return $this->notFound();
|
||||
$className = $this->urlParams['ClassName'];
|
||||
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
|
||||
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
|
||||
|
||||
switch($requestMethod) {
|
||||
case 'GET':
|
||||
return $this->getHandler($className, $id, $relation);
|
||||
|
||||
case 'PUT':
|
||||
return $this->putHandler($className, $id, $relation);
|
||||
|
||||
case 'DELETE':
|
||||
return $this->deleteHandler($className, $id, $relation);
|
||||
|
||||
case 'POST':
|
||||
}
|
||||
// if api access is disabled, don't proceed
|
||||
if(!singleton($className)->stat('api_access')) return $this->permissionFailure();
|
||||
|
||||
// authenticate through HTTP BasicAuth
|
||||
$member = $this->authenticate();
|
||||
|
||||
// handle different HTTP verbs
|
||||
if($this->request->isGET()) return $this->getHandler($className, $id, $relation);
|
||||
if($this->request->isPOST()) return $this->postHandler($className, $id, $relation);
|
||||
if($this->request->isPUT()) return $this->putHandler($className, $id, $relation);
|
||||
if($this->request->isDELETE()) return $this->deleteHandler($className, $id, $relation);
|
||||
|
||||
// if no HTTP verb matches, return error
|
||||
return $this->methodNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,6 +127,8 @@ class RestfulServer extends Controller {
|
||||
* - static $api_access must be set. This enables the API on a class by class basis
|
||||
* - $obj->canView() must return true. This lets you implement record-level security
|
||||
*
|
||||
* @todo Access checking
|
||||
*
|
||||
* @param String $className
|
||||
* @param Int $id
|
||||
* @param String $relation
|
||||
@ -152,17 +144,13 @@ class RestfulServer extends Controller {
|
||||
'limit' => $this->request->getVar('limit')
|
||||
);
|
||||
|
||||
$formatter = $this->getDataFormatter();
|
||||
|
||||
$responseFormatter = $this->getResponseDataFormatter();
|
||||
if(!$responseFormatter) return $this->unsupportedMediaType();
|
||||
|
||||
if($id) {
|
||||
$obj = DataObject::get_by_id($className, $id);
|
||||
if(!$obj) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if(!$obj->stat('api_access') || !$obj->canView()) {
|
||||
return $this->permissionFailure();
|
||||
}
|
||||
if(!$obj) return $this->notFound();
|
||||
if(!$obj->canView()) return $this->permissionFailure();
|
||||
|
||||
if($relation) {
|
||||
if($relationClass = $obj->many_many($relation)) {
|
||||
@ -185,16 +173,15 @@ class RestfulServer extends Controller {
|
||||
}
|
||||
|
||||
} else {
|
||||
if(!singleton($className)->stat('api_access')) {
|
||||
return $this->permissionFailure();
|
||||
}
|
||||
$obj = $this->search($className, $this->request->getVars(), $sort, $limit);
|
||||
// show empty serialized result when no records are present
|
||||
if(!$obj) $obj = new DataObjectSet();
|
||||
}
|
||||
|
||||
if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj);
|
||||
else return $formatter->convertDataObject($obj);
|
||||
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
|
||||
|
||||
if($obj instanceof DataObjectSet) return $responseFormatter->convertDataObjectSet($obj);
|
||||
else return $responseFormatter->convertDataObject($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,50 +207,147 @@ class RestfulServer extends Controller {
|
||||
return singleton($className)->buildDataObjectSet($query->execute());
|
||||
}
|
||||
|
||||
protected function getDataFormatter() {
|
||||
/**
|
||||
* Returns a dataformatter instance based on the request
|
||||
* extension or mimetype. Falls back to {@link self::$default_extension}.
|
||||
*
|
||||
* @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers
|
||||
* @return DataFormatter
|
||||
*/
|
||||
protected function getDataFormatter($includeAcceptHeader = false) {
|
||||
$extension = $this->request->getExtension();
|
||||
$contentType = $this->request->getHeader('Content-Type');
|
||||
$accept = $this->request->getHeader('Accept');
|
||||
|
||||
// get formatter
|
||||
if(!empty($extension)) {
|
||||
$formatter = DataFormatter::for_extension($extension);
|
||||
}elseif($includeAcceptHeader && !empty($accept) && $accept != '*/*') {
|
||||
$formatter = DataFormatter::for_mimetypes(explode(',',$accept));
|
||||
} elseif(!empty($contentType)) {
|
||||
$formatter = DataFormatter::for_mimetype($contentType);
|
||||
} else {
|
||||
$formatter = DataFormatter::for_extension(self::$default_extension);
|
||||
}
|
||||
|
||||
// Determine mime-type from extension
|
||||
$contentType = isset(self::$mimetype_map[$extension]) ? self::$mimetype_map[$extension] : self::$default_mimetype;
|
||||
|
||||
if(!$extension) $extension = self::$default_extension;
|
||||
$formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType);
|
||||
// set custom fields
|
||||
if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
|
||||
|
||||
// set relation depth
|
||||
$relationDepth = $this->request->getVar('relationdepth');
|
||||
if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth;
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
protected function getRequestDataFormatter() {
|
||||
return $this->getDataFormatter(false);
|
||||
}
|
||||
|
||||
protected function getResponseDataFormatter() {
|
||||
return $this->getDataFormatter(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for object delete
|
||||
*/
|
||||
protected function deleteHandler($className, $id) {
|
||||
if($id) {
|
||||
$obj = DataObject::get_by_id($className, $id);
|
||||
if($obj->stat('api_access') && $obj->canDelete()) {
|
||||
$obj->delete();
|
||||
} else {
|
||||
return $this->permissionFailure();
|
||||
}
|
||||
}
|
||||
$obj = DataObject::get_by_id($className, $id);
|
||||
if(!$obj) return $this->notFound();
|
||||
if(!$obj->canDelete()) return $this->permissionFailure();
|
||||
|
||||
$obj->delete();
|
||||
|
||||
$this->getResponse()->setStatusCode(204); // No Content
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for object write
|
||||
*/
|
||||
protected function putHandler($className, $id) {
|
||||
return $this->permissionFailure();
|
||||
$obj = DataObject::get_by_id($className, $id);
|
||||
if(!$obj) return $this->notFound();
|
||||
if(!$obj->canEdit()) return $this->permissionFailure();
|
||||
|
||||
$reqFormatter = $this->getRequestDataFormatter();
|
||||
if(!$reqFormatter) return $this->unsupportedMediaType();
|
||||
|
||||
$responseFormatter = $this->getResponseDataFormatter();
|
||||
if(!$responseFormatter) return $this->unsupportedMediaType();
|
||||
|
||||
$obj = $this->updateDataObject($obj, $reqFormatter);
|
||||
|
||||
$this->getResponse()->setStatusCode(200); // Success
|
||||
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
||||
$this->getResponse()->addHeader('Location', $objHref);
|
||||
|
||||
return $responseFormatter->convertDataObject($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for object append / method call
|
||||
* Handler for object append / method call.
|
||||
*
|
||||
* @todo Posting to an existing URL (without a relation)
|
||||
* current resolves in creatig a new element,
|
||||
* rather than a "Conflict" message.
|
||||
*/
|
||||
protected function postHandler($className, $id) {
|
||||
return $this->permissionFailure();
|
||||
if(!singleton($className)->canCreate()) return $this->permissionFailure();
|
||||
$obj = new $className();
|
||||
|
||||
$reqFormatter = $this->getRequestDataFormatter();
|
||||
if(!$reqFormatter) return $this->unsupportedMediaType();
|
||||
|
||||
$responseFormatter = $this->getResponseDataFormatter();
|
||||
|
||||
$obj = $this->updateDataObject($obj, $reqFormatter);
|
||||
|
||||
$this->getResponse()->setStatusCode(201); // Created
|
||||
$this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
|
||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
||||
$this->getResponse()->addHeader('Location', $objHref);
|
||||
|
||||
return $responseFormatter->convertDataObject($obj);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Converts either the given HTTP Body into an array
|
||||
* (based on the DataFormatter instance), or returns
|
||||
* the POST variables.
|
||||
* Automatically filters out certain critical fields
|
||||
* that shouldn't be set by the client (e.g. ID).
|
||||
*
|
||||
* @param DataObject $obj
|
||||
* @param DataFormatter $formatter
|
||||
* @return DataObject The passed object
|
||||
*/
|
||||
protected function updateDataObject($obj, $formatter) {
|
||||
// if neither an http body nor POST data is present, return error
|
||||
$body = $this->request->getBody();
|
||||
if(!$body && !$this->request->postVars()) {
|
||||
$this->getResponse()->setStatusCode(204); // No Content
|
||||
return 'No Content';
|
||||
}
|
||||
|
||||
if(!empty($body)) {
|
||||
$data = $formatter->convertStringToArray($body);
|
||||
} else {
|
||||
// assume application/x-www-form-urlencoded which is automatically parsed by PHP
|
||||
$data = $this->request->postVars();
|
||||
}
|
||||
|
||||
// @todo Disallow editing of certain keys in database
|
||||
$data = array_diff_key($data, array('ID','Created'));
|
||||
|
||||
$obj->update($data);
|
||||
$obj->write();
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
protected function permissionFailure() {
|
||||
// return a 401
|
||||
$this->getResponse()->setStatusCode(403);
|
||||
@ -276,6 +360,33 @@ class RestfulServer extends Controller {
|
||||
return "That object wasn't found";
|
||||
}
|
||||
|
||||
protected function methodNotAllowed() {
|
||||
$this->getResponse()->setStatusCode(405);
|
||||
return "Method Not Allowed";
|
||||
}
|
||||
|
||||
protected function unsupportedMediaType() {
|
||||
$this->response->setStatusCode(415); // Unsupported Media Type
|
||||
return "Unsupported Media Type";
|
||||
}
|
||||
|
||||
protected function authenticate() {
|
||||
if(!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) return false;
|
||||
|
||||
if($member = Member::currentMember()) return $member;
|
||||
$member = MemberAuthenticator::authenticate(array(
|
||||
'Email' => $_SERVER['PHP_AUTH_USER'],
|
||||
'Password' => $_SERVER['PHP_AUTH_PW'],
|
||||
), null);
|
||||
|
||||
if($member) {
|
||||
$member->LogIn(false);
|
||||
return $member;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,8 +6,18 @@ class XMLDataFormatter extends DataFormatter {
|
||||
*/
|
||||
static $api_base = "api/v1/";
|
||||
|
||||
protected $outputContentType = 'text/xml';
|
||||
|
||||
public function supportedExtensions() {
|
||||
return array('xml');
|
||||
return array(
|
||||
'xml'
|
||||
);
|
||||
}
|
||||
|
||||
public function supportedMimeTypes() {
|
||||
return array(
|
||||
'text/xml',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,7 +58,7 @@ class XMLDataFormatter extends DataFormatter {
|
||||
}
|
||||
$json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n";
|
||||
}
|
||||
|
||||
|
||||
foreach($obj->has_many() as $relName => $relClass) {
|
||||
$json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
|
||||
$items = $obj->$relName();
|
||||
@ -94,4 +104,8 @@ class XMLDataFormatter extends DataFormatter {
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
public function convertStringToArray($strData) {
|
||||
return Convert::xml2array($strData);
|
||||
}
|
||||
}
|
@ -79,7 +79,7 @@ class Convert extends Object {
|
||||
* @return string JSON safe string
|
||||
*/
|
||||
static function raw2json($val) {
|
||||
if(function_exists('json_econde')) {
|
||||
if(function_exists('json_encode')) {
|
||||
return json_encode($val);
|
||||
} else {
|
||||
require_once(Director::baseFolder() . '/sapphire/misc/json/JSON.php');
|
||||
@ -152,8 +152,44 @@ class Convert extends Object {
|
||||
return self::raw2sql(self::js2raw($val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the PHP5.2 native json_decode function if available,
|
||||
* otherwise falls back to the Services_JSON class.
|
||||
*
|
||||
* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198
|
||||
*
|
||||
* @param string $val
|
||||
* @return mixed JSON safe string
|
||||
*/
|
||||
static function json2obj($val) {
|
||||
//if(function_exists('json_decode')) {
|
||||
// return json_decode($val);
|
||||
//} else {
|
||||
require_once(Director::baseFolder() . '/sapphire/misc/json/JSON.php');
|
||||
$json = new Services_JSON();
|
||||
return $json->decode($val);
|
||||
//}
|
||||
}
|
||||
|
||||
static function json2array($val) {
|
||||
$json = self::json2obj($val);
|
||||
$arr = array();
|
||||
foreach($json as $k => $v) {
|
||||
$arr[$k] = $v;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
|
||||
static function xml2array($val) {
|
||||
return preg_split( '/\s*(<[^>]+>)|\s\s*/', $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
|
||||
$xml = new SimpleXMLElement($val);
|
||||
$arr = array();
|
||||
foreach($xml->children() as $k => $v) {
|
||||
// @todo Convert recursively
|
||||
$arr[$k] = (string)$v;
|
||||
}
|
||||
return $arr;
|
||||
//return preg_split( '/\s*(<[^>]+>)|\s\s*/', $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
|
||||
}
|
||||
|
||||
static function array2json( $array ) {
|
||||
|
@ -98,10 +98,12 @@ class SSViewer extends Object {
|
||||
'rewriteHashlinks' => true,
|
||||
);
|
||||
|
||||
protected static $topLevel = null;
|
||||
public static function topLevel() {
|
||||
return SSViewer::$topLevel;
|
||||
}
|
||||
protected static $topLevel = array();
|
||||
public static function topLevel() {
|
||||
if(SSViewer::$topLevel) {
|
||||
return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to disable rewriting of <a href="#xxx"> links. This is useful in Ajax applications.
|
||||
@ -138,7 +140,7 @@ class SSViewer extends Object {
|
||||
*/
|
||||
|
||||
public function process($item) {
|
||||
SSViewer::$topLevel = $item;
|
||||
SSViewer::$topLevel[] = $item;
|
||||
|
||||
if(isset($this->chosenTemplates['main'])) {
|
||||
$template = $this->chosenTemplates['main'];
|
||||
@ -194,7 +196,7 @@ class SSViewer extends Object {
|
||||
$output = $val;
|
||||
$output = Requirements::includeInHTML($template, $output);
|
||||
|
||||
SSViewer::$topLevel = null;
|
||||
array_pop(SSViewer::$topLevel);
|
||||
|
||||
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template");
|
||||
|
||||
|
@ -88,7 +88,17 @@ class Director {
|
||||
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
||||
*/
|
||||
function direct($url) {
|
||||
$req = new HTTPRequest($_SERVER['REQUEST_METHOD'], $url, $_GET, array_merge((array)$_POST, (array)$_FILES));
|
||||
$req = new HTTPRequest(
|
||||
$_SERVER['REQUEST_METHOD'],
|
||||
$url,
|
||||
$_GET,
|
||||
array_merge((array)$_POST, (array)$_FILES),
|
||||
@file_get_contents('php://input')
|
||||
);
|
||||
|
||||
// @todo find better way to extract HTTP headers
|
||||
if(isset($_SERVER['HTTP_ACCEPT'])) $req->addHeader("Accept", $_SERVER['HTTP_ACCEPT']);
|
||||
if(isset($_SERVER['CONTENT_TYPE'])) $req->addHeader("Content-Type", $_SERVER['CONTENT_TYPE']);
|
||||
|
||||
// Load the session into the controller
|
||||
$session = new Session($_SESSION);
|
||||
@ -123,16 +133,19 @@ class Director {
|
||||
*
|
||||
* This method is the counterpart of Director::direct() that is used in functional testing. It will execute the URL given,
|
||||
*
|
||||
* @param $url The URL to visit
|
||||
* @param $postVars The $_POST & $_FILES variables
|
||||
* @param $session The {@link Session} object representing the current session. By passing the same object to multiple
|
||||
* @param string $url The URL to visit
|
||||
* @param array $postVars The $_POST & $_FILES variables
|
||||
* @param Session $session The {@link Session} object representing the current session. By passing the same object to multiple
|
||||
* calls of Director::test(), you can simulate a peristed session.
|
||||
* @param $httpMethod The HTTP method, such as GET or POST. It will default to POST if postVars is set, GET otherwise
|
||||
* @param string $httpMethod The HTTP method, such as GET or POST. It will default to POST if postVars is set, GET otherwise
|
||||
* @param string $body The HTTP body
|
||||
* @param array $headers HTTP headers with key-value pairs
|
||||
* @return HTTPResponse
|
||||
*
|
||||
* @uses getControllerForURL() The rule-lookup logic is handled by this.
|
||||
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
|
||||
*/
|
||||
function test($url, $postVars = null, $session = null, $httpMethod = null) {
|
||||
function test($url, $postVars = null, $session = null, $httpMethod = null, $body = null, $headers = null) {
|
||||
if(!$httpMethod) $httpMethod = $postVars ? "POST" : "GET";
|
||||
|
||||
$getVars = array();
|
||||
@ -142,8 +155,8 @@ class Director {
|
||||
}
|
||||
|
||||
if(!$session) $session = new Session(null);
|
||||
|
||||
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars);
|
||||
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
|
||||
if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v);
|
||||
$result = Director::handleRequest($req, $session);
|
||||
|
||||
return $result;
|
||||
|
@ -7,6 +7,9 @@
|
||||
* The intention is that a single HTTPRequest object can be passed from one object to another, each object calling
|
||||
* match() to get the information that they need out of the URL. This is generally handled by
|
||||
* {@link RequestHandlingData::handleRequest()}.
|
||||
*
|
||||
* @todo Accept X_HTTP_METHOD_OVERRIDE http header and $_REQUEST['_method'] to override request types (useful for webclients
|
||||
* not supporting PUT and DELETE)
|
||||
*/
|
||||
class HTTPRequest extends Object implements ArrayAccess {
|
||||
/**
|
||||
@ -20,14 +23,31 @@ class HTTPRequest extends Object implements ArrayAccess {
|
||||
protected $extension;
|
||||
|
||||
/**
|
||||
* The HTTP method
|
||||
* The HTTP method: GET/PUT/POST/DELETE/HEAD
|
||||
*/
|
||||
protected $httpMethod;
|
||||
|
||||
protected $getVars = array();
|
||||
|
||||
protected $postVars = array();
|
||||
|
||||
/**
|
||||
* HTTP Headers like "Content-Type: text/xml"
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = array();
|
||||
|
||||
/**
|
||||
* Raw HTTP body, used by PUT and POST requests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $body;
|
||||
|
||||
protected $allParams = array();
|
||||
|
||||
protected $latestParams = array();
|
||||
|
||||
protected $unshiftedButParsedParts = 0;
|
||||
@ -48,6 +68,14 @@ class HTTPRequest extends Object implements ArrayAccess {
|
||||
return $this->httpMethod == 'DELETE';
|
||||
}
|
||||
|
||||
function setBody($body) {
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
function getBody() {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
function getVars() {
|
||||
return $this->getVars;
|
||||
}
|
||||
@ -73,6 +101,42 @@ class HTTPRequest extends Object implements ArrayAccess {
|
||||
return $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HTTP header to the response, replacing any header of the same name.
|
||||
*
|
||||
* @param string $header Example: "Content-Type"
|
||||
* @param string $value Example: "text/xml"
|
||||
*/
|
||||
function addHeader($header, $value) {
|
||||
$this->headers[$header] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
function getHeaders() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing HTTP header
|
||||
*
|
||||
* @param string $header
|
||||
*/
|
||||
function getHeader($header) {
|
||||
return (isset($this->headers[$header])) ? $this->headers[$header] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing HTTP header by its name,
|
||||
* e.g. "Content-Type".
|
||||
*
|
||||
* @param string $header
|
||||
*/
|
||||
function removeHeader($header) {
|
||||
if(isset($this->headers[$header])) unset($this->headers[$header]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the existence of a key-value pair in the request to be checked using
|
||||
* array syntax, so isset($request['title']) will check for $_POST['title'] and $_GET['title]
|
||||
@ -109,7 +173,7 @@ class HTTPRequest extends Object implements ArrayAccess {
|
||||
/**
|
||||
* Construct a HTTPRequest from a URL relative to the site root.
|
||||
*/
|
||||
function __construct($httpMethod, $url, $getVars = array(), $postVars = array()) {
|
||||
function __construct($httpMethod, $url, $getVars = array(), $postVars = array(), $body = null) {
|
||||
$this->httpMethod = $httpMethod;
|
||||
|
||||
$url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $url);
|
||||
@ -123,6 +187,7 @@ class HTTPRequest extends Object implements ArrayAccess {
|
||||
|
||||
$this->getVars = (array)$getVars;
|
||||
$this->postVars = (array)$postVars;
|
||||
$this->body = $body;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -57,13 +57,25 @@ class HTTPResponse extends Object {
|
||||
);
|
||||
|
||||
protected $statusCode = 200;
|
||||
|
||||
/**
|
||||
* HTTP Headers like "Content-Type: text/xml"
|
||||
*
|
||||
* @see http://en.wikipedia.org/wiki/List_of_HTTP_headers
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = array();
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $body = null;
|
||||
|
||||
function setStatusCode($code) {
|
||||
if(isset(self::$status_codes[$code])) $this->statusCode = $code;
|
||||
else user_error("Unrecognised HTTP status code '$code'", E_USER_WARNING);
|
||||
}
|
||||
|
||||
function getStatusCode() {
|
||||
return $this->statusCode;
|
||||
}
|
||||
@ -71,19 +83,25 @@ class HTTPResponse extends Object {
|
||||
function setBody($body) {
|
||||
$this->body = $body;
|
||||
}
|
||||
|
||||
function getBody() {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HTTP header to the response, replacing any header of the same name
|
||||
* Add a HTTP header to the response, replacing any header of the same name.
|
||||
*
|
||||
* @param string $header Example: "Content-Type"
|
||||
* @param string $value Example: "text/xml"
|
||||
*/
|
||||
function addHeader($header, $value) {
|
||||
$this->headers[$header] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the HTTP header of the given name
|
||||
* Return the HTTP header of the given name.
|
||||
*
|
||||
* @param string $header
|
||||
* @returns string
|
||||
*/
|
||||
function getHeader($header) {
|
||||
@ -94,6 +112,23 @@ class HTTPResponse extends Object {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
function getHeaders() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an existing HTTP header by its name,
|
||||
* e.g. "Content-Type".
|
||||
*
|
||||
* @param unknown_type $header
|
||||
*/
|
||||
function removeHeader($header) {
|
||||
if(isset($this->headers[$header])) unset($this->headers[$header]);
|
||||
}
|
||||
|
||||
function redirect($dest, $code=302) {
|
||||
if(!in_array($code, self::$redirect_codes)) $code = 302;
|
||||
$this->statusCode = $code;
|
||||
|
@ -1776,7 +1776,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
||||
* Temporary hack to return an association name, based on class, toget around the mangle
|
||||
* of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
|
||||
*/
|
||||
public function getReverseAssociation($className) {
|
||||
public function getReverseAssociation($className) {
|
||||
if (is_array($this->has_many())) {
|
||||
$has_many = array_flip($this->has_many());
|
||||
if (array_key_exists($className, $has_many)) return $has_many[$className];
|
||||
|
@ -88,25 +88,6 @@ class SearchContext extends Object {
|
||||
$fields[] = $classes[0].'.ClassName AS RecordClassName';
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @refactor move to SQLQuery
|
||||
* @todo fix hack
|
||||
*/
|
||||
protected function applyBaseTable() {
|
||||
$classes = ClassInfo::dataClassesFor($this->modelClass);
|
||||
return $classes[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo only works for one level deep of inheritance
|
||||
* @todo fix hack
|
||||
* @deprecated - remove me!
|
||||
*/
|
||||
protected function applyBaseTableJoin($query) {
|
||||
$classes = ClassInfo::dataClassesFor($this->modelClass);
|
||||
if (count($classes) > 1) $query->leftJoin($classes[1], "{$classes[1]}.ID = {$classes[0]}.ID");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL object representing the search context for the given
|
||||
@ -132,7 +113,7 @@ class SearchContext extends Object {
|
||||
|
||||
$SQL_limit = Convert::raw2sql($limit);
|
||||
$query->limit($SQL_limit);
|
||||
|
||||
|
||||
$SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
|
||||
$query->orderby($SQL_sort);
|
||||
|
||||
@ -165,6 +146,8 @@ class SearchContext extends Object {
|
||||
|
||||
$query = $this->getQuery($searchParams, $sort, $limit);
|
||||
|
||||
$sql = $query->sql();
|
||||
|
||||
// use if a raw SQL query is needed
|
||||
$results = new DataObjectSet();
|
||||
foreach($query->execute() as $row) {
|
||||
|
@ -83,17 +83,6 @@ abstract class SearchFilter extends Object {
|
||||
return $candidateClass . "." . $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies multiple-table inheritance to straight joins on the data objects
|
||||
*
|
||||
* @todo Should this be applied in SQLQuery->from instead? !!!
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function applyJoin($query, $model, $component) {
|
||||
$query->leftJoin($component, "{$this->model}.ID = $component.{$model->getReverseAssociation($this->model)}ID");
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the relationship fields, and add the table
|
||||
* mappings to the query object state.
|
||||
@ -106,16 +95,22 @@ abstract class SearchFilter extends Object {
|
||||
if (is_array($this->relation)) {
|
||||
$model = singleton($this->model);
|
||||
foreach($this->relation as $rel) {
|
||||
if ($component = $model->has_one($rel)) {
|
||||
$model = singleton($component);
|
||||
$this->applyJoin($query, $model, $component);
|
||||
if ($component = $model->has_one($rel)) {
|
||||
$foreignKey = $model->getReverseAssociation($component);
|
||||
$query->leftJoin($component, "$component.ID = {$this->model}.{$foreignKey}ID");
|
||||
$this->model = $component;
|
||||
|
||||
} elseif ($component = $model->has_many($rel)) {
|
||||
$ancestry = $model->getClassAncestry();
|
||||
$model = singleton($component);
|
||||
$this->applyJoin($query, $model, $component);
|
||||
$foreignKey = $model->getReverseAssociation($ancestry[0]);
|
||||
$foreignKey = ($foreignKey) ? $foreignKey : $ancestry[0];
|
||||
$query->leftJoin($component, "$component.{$foreignKey}ID = {$this->model}.ID");
|
||||
|
||||
$this->model = $component;
|
||||
|
||||
} elseif ($component = $model->many_many($rel)) {
|
||||
Debug::dump("Many-Many traversals not implemented");
|
||||
throw new Exception("Many-Many traversals not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
class DataObjectDecoratorTest extends SapphireTest {
|
||||
static $fixture_file = 'sapphire/tests/DataObjectTest.yml';
|
||||
|
||||
function testOneToManyAssociationWithDecorator() {
|
||||
/*
|
||||
// Fails in RestfulServerTest
|
||||
// Error: Object::__call() Method 'RelatedObjects' not found in class 'RestfulServerTest_Comment'
|
||||
$contact = new DataObjectDecoratorTest_Member();
|
||||
$contact->Website = "http://www.example.com";
|
||||
|
||||
@ -11,12 +13,11 @@ class DataObjectDecoratorTest extends SapphireTest {
|
||||
$object->FieldOne = "Lorem ipsum dolor";
|
||||
$object->FieldTwo = "Random notes";
|
||||
|
||||
/* The following code doesn't currently work:
|
||||
$contact->RelatedObjects()->add($object);
|
||||
$contact->write();
|
||||
*/
|
||||
// The following code doesn't currently work:
|
||||
// $contact->RelatedObjects()->add($object);
|
||||
// $contact->write();
|
||||
|
||||
/* Instead we have to do the following */
|
||||
// Instead we have to do the following
|
||||
$contact->write();
|
||||
$object->ContactID = $contact->ID;
|
||||
$object->write();
|
||||
@ -29,6 +30,7 @@ class DataObjectDecoratorTest extends SapphireTest {
|
||||
$this->assertEquals("Lorem ipsum dolor", $contact->RelatedObjects()->First()->FieldOne);
|
||||
$this->assertEquals("Random notes", $contact->RelatedObjects()->First()->FieldTwo);
|
||||
$contact->delete();
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
@ -70,6 +72,5 @@ class DataObjectDecoratorTest_RelatedObject extends DataObject implements TestOn
|
||||
|
||||
}
|
||||
|
||||
DataObject::add_extension('DataObjectDecoratorTest_Member', 'DataObjectDecoratorTest_ContactRole');
|
||||
|
||||
//DataObject::add_extension('DataObjectDecoratorTest_Member', 'DataObjectDecoratorTest_ContactRole');
|
||||
?>
|
Loading…
x
Reference in New Issue
Block a user