(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:
Ingo Schommer 2008-08-09 07:03:24 +00:00
parent c1440e0b02
commit b89328e6cc
13 changed files with 481 additions and 132 deletions

View File

@ -37,7 +37,19 @@ abstract class DataFormatter extends Object {
protected $customFields = null; 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) { static function for_extension($extension) {
$classes = ClassInfo::subclassesFor("DataFormatter"); $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 * @param array $fields
*/ */
@ -69,6 +133,10 @@ abstract class DataFormatter extends Object {
return $this->customFields; return $this->customFields;
} }
public function getOutputContentType() {
return $this->outputContentType;
}
/** /**
* Returns all fields on the object which should be shown * Returns all fields on the object which should be shown
* in the output. Can be customised through {@link self::setCustomFields()}. * in the output. Can be customised through {@link self::setCustomFields()}.
@ -104,6 +172,8 @@ abstract class DataFormatter extends Object {
*/ */
abstract function supportedExtensions(); abstract function supportedExtensions();
abstract function supportedMimeTypes();
/** /**
* Convert a single data object to this format. Return a string. * 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. * Convert a data object set to this format. Return a string.
*/ */
abstract function convertDataObjectSet(DataObjectSet $set); 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);
}
} }

View File

@ -6,8 +6,20 @@ class JSONDataFormatter extends DataFormatter {
*/ */
static $api_base = "api/v1/"; static $api_base = "api/v1/";
protected $outputContentType = 'application/json';
public function supportedExtensions() { 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]"; return "[\n" . implode(",\n", $jsonParts) . "\n]";
} }
public function convertStringToArray($strData) {
return Convert::json2array($strData);
}
} }

View File

@ -12,10 +12,10 @@
* applications. * applications.
* *
* - GET /api/v1/(ClassName)/(ID) - gets a database record * - 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)/(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 (NOT IMPLEMENTED YET) * - 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) * - 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) * - 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) * - &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 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 Make SearchContext specification customizeable for each class
* @todo Allow for range-searches (e.g. on Created column) * @todo Allow for range-searches (e.g. on Created column)
* @todo Allow other authentication methods (currently only HTTP BasicAuth)
*/ */
class RestfulServer extends Controller { class RestfulServer extends Controller {
static $url_handlers = array( static $url_handlers = array(
@ -52,7 +54,7 @@ class RestfulServer extends Controller {
* *
* @var string * @var string
*/ */
protected static $default_extension = "xml"; public 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.
@ -61,19 +63,6 @@ class RestfulServer extends Controller {
*/ */
protected static $default_mimetype = "text/xml"; 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) { function handleItem($request) {
return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID"))); return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
@ -91,24 +80,25 @@ class RestfulServer extends Controller {
function index() { function index() {
ContentNegotiator::disable(); ContentNegotiator::disable();
$requestMethod = $_SERVER['REQUEST_METHOD'];
if(!isset($this->urlParams['ClassName'])) return $this->notFound(); if(!isset($this->urlParams['ClassName'])) return $this->notFound();
$className = $this->urlParams['ClassName']; $className = $this->urlParams['ClassName'];
$id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null; $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
$relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null; $relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
switch($requestMethod) { // if api access is disabled, don't proceed
case 'GET': if(!singleton($className)->stat('api_access')) return $this->permissionFailure();
return $this->getHandler($className, $id, $relation);
// authenticate through HTTP BasicAuth
case 'PUT': $member = $this->authenticate();
return $this->putHandler($className, $id, $relation);
// handle different HTTP verbs
case 'DELETE': if($this->request->isGET()) return $this->getHandler($className, $id, $relation);
return $this->deleteHandler($className, $id, $relation); if($this->request->isPOST()) return $this->postHandler($className, $id, $relation);
if($this->request->isPUT()) return $this->putHandler($className, $id, $relation);
case 'POST': 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 * - 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 * - $obj->canView() must return true. This lets you implement record-level security
* *
* @todo Access checking
*
* @param String $className * @param String $className
* @param Int $id * @param Int $id
* @param String $relation * @param String $relation
@ -152,17 +144,13 @@ class RestfulServer extends Controller {
'limit' => $this->request->getVar('limit') 'limit' => $this->request->getVar('limit')
); );
$formatter = $this->getDataFormatter(); $responseFormatter = $this->getResponseDataFormatter();
if(!$responseFormatter) return $this->unsupportedMediaType();
if($id) { if($id) {
$obj = DataObject::get_by_id($className, $id); $obj = DataObject::get_by_id($className, $id);
if(!$obj) { if(!$obj) return $this->notFound();
return $this->notFound(); if(!$obj->canView()) return $this->permissionFailure();
}
if(!$obj->stat('api_access') || !$obj->canView()) {
return $this->permissionFailure();
}
if($relation) { if($relation) {
if($relationClass = $obj->many_many($relation)) { if($relationClass = $obj->many_many($relation)) {
@ -185,16 +173,15 @@ class RestfulServer extends Controller {
} }
} else { } else {
if(!singleton($className)->stat('api_access')) {
return $this->permissionFailure();
}
$obj = $this->search($className, $this->request->getVars(), $sort, $limit); $obj = $this->search($className, $this->request->getVars(), $sort, $limit);
// show empty serialized result when no records are present // show empty serialized result when no records are present
if(!$obj) $obj = new DataObjectSet(); if(!$obj) $obj = new DataObjectSet();
} }
if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj); $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
else return $formatter->convertDataObject($obj);
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()); 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(); $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 // set custom fields
$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);
if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields)); if($customFields = $this->request->getVar('fields')) $formatter->setCustomFields(explode(',',$customFields));
// set relation depth
$relationDepth = $this->request->getVar('relationdepth'); $relationDepth = $this->request->getVar('relationdepth');
if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth; if(is_numeric($relationDepth)) $formatter->relationDepth = (int)$relationDepth;
return $formatter; return $formatter;
} }
protected function getRequestDataFormatter() {
return $this->getDataFormatter(false);
}
protected function getResponseDataFormatter() {
return $this->getDataFormatter(true);
}
/** /**
* Handler for object delete * Handler for object delete
*/ */
protected function deleteHandler($className, $id) { protected function deleteHandler($className, $id) {
if($id) { $obj = DataObject::get_by_id($className, $id);
$obj = DataObject::get_by_id($className, $id); if(!$obj) return $this->notFound();
if($obj->stat('api_access') && $obj->canDelete()) { if(!$obj->canDelete()) return $this->permissionFailure();
$obj->delete();
} else { $obj->delete();
return $this->permissionFailure();
} $this->getResponse()->setStatusCode(204); // No Content
} return true;
} }
/** /**
* Handler for object write * Handler for object write
*/ */
protected function putHandler($className, $id) { 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) { 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() { protected function permissionFailure() {
// return a 401 // return a 401
$this->getResponse()->setStatusCode(403); $this->getResponse()->setStatusCode(403);
@ -276,6 +360,33 @@ class RestfulServer extends Controller {
return "That object wasn't found"; 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;
}
}
} }
/** /**

View File

@ -6,8 +6,18 @@ class XMLDataFormatter extends DataFormatter {
*/ */
static $api_base = "api/v1/"; static $api_base = "api/v1/";
protected $outputContentType = 'text/xml';
public function supportedExtensions() { 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"; $json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n";
} }
foreach($obj->has_many() as $relName => $relClass) { foreach($obj->has_many() as $relName => $relClass) {
$json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n"; $json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n";
$items = $obj->$relName(); $items = $obj->$relName();
@ -94,4 +104,8 @@ class XMLDataFormatter extends DataFormatter {
return $xml; return $xml;
} }
public function convertStringToArray($strData) {
return Convert::xml2array($strData);
}
} }

View File

@ -79,7 +79,7 @@ class Convert extends Object {
* @return string JSON safe string * @return string JSON safe string
*/ */
static function raw2json($val) { static function raw2json($val) {
if(function_exists('json_econde')) { if(function_exists('json_encode')) {
return json_encode($val); return json_encode($val);
} else { } else {
require_once(Director::baseFolder() . '/sapphire/misc/json/JSON.php'); require_once(Director::baseFolder() . '/sapphire/misc/json/JSON.php');
@ -152,8 +152,44 @@ class Convert extends Object {
return self::raw2sql(self::js2raw($val)); 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) { 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 ) { static function array2json( $array ) {

View File

@ -98,10 +98,12 @@ class SSViewer extends Object {
'rewriteHashlinks' => true, 'rewriteHashlinks' => true,
); );
protected static $topLevel = null; protected static $topLevel = array();
public static function topLevel() { public static function topLevel() {
return SSViewer::$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. * 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) { public function process($item) {
SSViewer::$topLevel = $item; SSViewer::$topLevel[] = $item;
if(isset($this->chosenTemplates['main'])) { if(isset($this->chosenTemplates['main'])) {
$template = $this->chosenTemplates['main']; $template = $this->chosenTemplates['main'];
@ -194,7 +196,7 @@ class SSViewer extends Object {
$output = $val; $output = $val;
$output = Requirements::includeInHTML($template, $output); $output = Requirements::includeInHTML($template, $output);
SSViewer::$topLevel = null; array_pop(SSViewer::$topLevel);
if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template"); if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template");

View File

@ -88,7 +88,17 @@ class Director {
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call. * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
*/ */
function direct($url) { 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 // Load the session into the controller
$session = new Session($_SESSION); $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, * 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 string $url The URL to visit
* @param $postVars The $_POST & $_FILES variables * @param array $postVars The $_POST & $_FILES variables
* @param $session The {@link Session} object representing the current session. By passing the same object to multiple * @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. * 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 getControllerForURL() The rule-lookup logic is handled by this.
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call. * @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"; if(!$httpMethod) $httpMethod = $postVars ? "POST" : "GET";
$getVars = array(); $getVars = array();
@ -142,8 +155,8 @@ class Director {
} }
if(!$session) $session = new Session(null); if(!$session) $session = new Session(null);
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
$req = new HTTPRequest($httpMethod, $url, $getVars, $postVars); if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v);
$result = Director::handleRequest($req, $session); $result = Director::handleRequest($req, $session);
return $result; return $result;

View File

@ -7,6 +7,9 @@
* The intention is that a single HTTPRequest object can be passed from one object to another, each object calling * 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 * match() to get the information that they need out of the URL. This is generally handled by
* {@link RequestHandlingData::handleRequest()}. * {@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 { class HTTPRequest extends Object implements ArrayAccess {
/** /**
@ -20,14 +23,31 @@ class HTTPRequest extends Object implements ArrayAccess {
protected $extension; protected $extension;
/** /**
* The HTTP method * The HTTP method: GET/PUT/POST/DELETE/HEAD
*/ */
protected $httpMethod; protected $httpMethod;
protected $getVars = array(); protected $getVars = array();
protected $postVars = 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 $allParams = array();
protected $latestParams = array(); protected $latestParams = array();
protected $unshiftedButParsedParts = 0; protected $unshiftedButParsedParts = 0;
@ -48,6 +68,14 @@ class HTTPRequest extends Object implements ArrayAccess {
return $this->httpMethod == 'DELETE'; return $this->httpMethod == 'DELETE';
} }
function setBody($body) {
$this->body = $body;
}
function getBody() {
return $this->body;
}
function getVars() { function getVars() {
return $this->getVars; return $this->getVars;
} }
@ -73,6 +101,42 @@ class HTTPRequest extends Object implements ArrayAccess {
return $this->extension; 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 * 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] * 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. * 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; $this->httpMethod = $httpMethod;
$url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $url); $url = preg_replace(array('/\/+/','/^\//', '/\/$/'),array('/','',''), $url);
@ -123,6 +187,7 @@ class HTTPRequest extends Object implements ArrayAccess {
$this->getVars = (array)$getVars; $this->getVars = (array)$getVars;
$this->postVars = (array)$postVars; $this->postVars = (array)$postVars;
$this->body = $body;
parent::__construct(); parent::__construct();
} }

View File

@ -57,13 +57,25 @@ class HTTPResponse extends Object {
); );
protected $statusCode = 200; 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(); protected $headers = array();
/**
* @var string
*/
protected $body = null; protected $body = null;
function setStatusCode($code) { function setStatusCode($code) {
if(isset(self::$status_codes[$code])) $this->statusCode = $code; if(isset(self::$status_codes[$code])) $this->statusCode = $code;
else user_error("Unrecognised HTTP status code '$code'", E_USER_WARNING); else user_error("Unrecognised HTTP status code '$code'", E_USER_WARNING);
} }
function getStatusCode() { function getStatusCode() {
return $this->statusCode; return $this->statusCode;
} }
@ -71,19 +83,25 @@ class HTTPResponse extends Object {
function setBody($body) { function setBody($body) {
$this->body = $body; $this->body = $body;
} }
function getBody() { function getBody() {
return $this->body; 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) { function addHeader($header, $value) {
$this->headers[$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 * @returns string
*/ */
function getHeader($header) { 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) { function redirect($dest, $code=302) {
if(!in_array($code, self::$redirect_codes)) $code = 302; if(!in_array($code, self::$redirect_codes)) $code = 302;
$this->statusCode = $code; $this->statusCode = $code;

View File

@ -1776,7 +1776,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Temporary hack to return an association name, based on class, toget around the mangle * 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. * 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())) { if (is_array($this->has_many())) {
$has_many = array_flip($this->has_many()); $has_many = array_flip($this->has_many());
if (array_key_exists($className, $has_many)) return $has_many[$className]; if (array_key_exists($className, $has_many)) return $has_many[$className];

View File

@ -88,25 +88,6 @@ class SearchContext extends Object {
$fields[] = $classes[0].'.ClassName AS RecordClassName'; $fields[] = $classes[0].'.ClassName AS RecordClassName';
return $fields; 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 * Returns a SQL object representing the search context for the given
@ -132,7 +113,7 @@ class SearchContext extends Object {
$SQL_limit = Convert::raw2sql($limit); $SQL_limit = Convert::raw2sql($limit);
$query->limit($SQL_limit); $query->limit($SQL_limit);
$SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort'); $SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
$query->orderby($SQL_sort); $query->orderby($SQL_sort);
@ -165,6 +146,8 @@ class SearchContext extends Object {
$query = $this->getQuery($searchParams, $sort, $limit); $query = $this->getQuery($searchParams, $sort, $limit);
$sql = $query->sql();
// use if a raw SQL query is needed // use if a raw SQL query is needed
$results = new DataObjectSet(); $results = new DataObjectSet();
foreach($query->execute() as $row) { foreach($query->execute() as $row) {

View File

@ -83,17 +83,6 @@ abstract class SearchFilter extends Object {
return $candidateClass . "." . $this->name; 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 * Traverse the relationship fields, and add the table
* mappings to the query object state. * mappings to the query object state.
@ -106,16 +95,22 @@ abstract class SearchFilter extends Object {
if (is_array($this->relation)) { if (is_array($this->relation)) {
$model = singleton($this->model); $model = singleton($this->model);
foreach($this->relation as $rel) { foreach($this->relation as $rel) {
if ($component = $model->has_one($rel)) { if ($component = $model->has_one($rel)) {
$model = singleton($component); $foreignKey = $model->getReverseAssociation($component);
$this->applyJoin($query, $model, $component); $query->leftJoin($component, "$component.ID = {$this->model}.{$foreignKey}ID");
$this->model = $component; $this->model = $component;
} elseif ($component = $model->has_many($rel)) { } elseif ($component = $model->has_many($rel)) {
$ancestry = $model->getClassAncestry();
$model = singleton($component); $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; $this->model = $component;
} elseif ($component = $model->many_many($rel)) { } elseif ($component = $model->many_many($rel)) {
Debug::dump("Many-Many traversals not implemented"); throw new Exception("Many-Many traversals not implemented");
} }
} }
} }

View File

@ -1,9 +1,11 @@
<?php <?php
class DataObjectDecoratorTest extends SapphireTest { class DataObjectDecoratorTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/DataObjectTest.yml'; static $fixture_file = 'sapphire/tests/DataObjectTest.yml';
function testOneToManyAssociationWithDecorator() { function testOneToManyAssociationWithDecorator() {
/*
// Fails in RestfulServerTest
// Error: Object::__call() Method 'RelatedObjects' not found in class 'RestfulServerTest_Comment'
$contact = new DataObjectDecoratorTest_Member(); $contact = new DataObjectDecoratorTest_Member();
$contact->Website = "http://www.example.com"; $contact->Website = "http://www.example.com";
@ -11,12 +13,11 @@ class DataObjectDecoratorTest extends SapphireTest {
$object->FieldOne = "Lorem ipsum dolor"; $object->FieldOne = "Lorem ipsum dolor";
$object->FieldTwo = "Random notes"; $object->FieldTwo = "Random notes";
/* The following code doesn't currently work: // The following code doesn't currently work:
$contact->RelatedObjects()->add($object); // $contact->RelatedObjects()->add($object);
$contact->write(); // $contact->write();
*/
/* Instead we have to do the following */ // Instead we have to do the following
$contact->write(); $contact->write();
$object->ContactID = $contact->ID; $object->ContactID = $contact->ID;
$object->write(); $object->write();
@ -29,6 +30,7 @@ class DataObjectDecoratorTest extends SapphireTest {
$this->assertEquals("Lorem ipsum dolor", $contact->RelatedObjects()->First()->FieldOne); $this->assertEquals("Lorem ipsum dolor", $contact->RelatedObjects()->First()->FieldOne);
$this->assertEquals("Random notes", $contact->RelatedObjects()->First()->FieldTwo); $this->assertEquals("Random notes", $contact->RelatedObjects()->First()->FieldTwo);
$contact->delete(); $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');
?> ?>