mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
BUGFIX: Add casting support to global and iterator variable injection
This commit is contained in:
parent
fb246bdd08
commit
2c65d3a398
@ -85,7 +85,29 @@ SS
|
|||||||
$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'), 'Escapes before injections are correctly unescaped');
|
$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'), 'Escapes before injections are correctly unescaped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function testGlobalVariableCalls() {
|
function testGlobalVariableCalls() {
|
||||||
|
$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
|
||||||
|
$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
|
||||||
|
$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGlobalVariableCallsWithArguments() {
|
||||||
|
$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
|
||||||
|
$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
|
||||||
|
$this->assertEquals('zFoo:Bar:Bazz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
|
||||||
|
$this->assertEquals('zreferencez', $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGlobalVariablesAreEscaped() {
|
||||||
|
$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
|
||||||
|
$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
|
||||||
|
|
||||||
|
$this->assertEquals('z<div></div>z', $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
|
||||||
|
$this->assertEquals('z<div></div>z', $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCoreGlobalVariableCalls() {
|
||||||
$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
|
$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
|
||||||
$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'), 'Upper-case %AbsoluteBaseURL can be called from within template');
|
$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'), 'Upper-case %AbsoluteBaseURL can be called from within template');
|
||||||
|
|
||||||
@ -108,9 +130,7 @@ SS
|
|||||||
|
|
||||||
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'), 'SecurityToken template functions result correct result');
|
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'), 'SecurityToken template functions result correct result');
|
||||||
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'), 'SecurityToken template functions result correct result');
|
$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'), 'SecurityToken template functions result correct result');
|
||||||
}
|
|
||||||
|
|
||||||
function testGlobalVariableCallsWithArguments() {
|
|
||||||
$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
|
$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
|
||||||
$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
|
$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result');
|
||||||
}
|
}
|
||||||
@ -967,3 +987,37 @@ class SSViewerTest_Page extends SiteTree {
|
|||||||
return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
|
return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
|
||||||
|
|
||||||
|
public static function get_exposed_variables() {
|
||||||
|
return array(
|
||||||
|
'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html'),
|
||||||
|
'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html', 'casting' => 'Varchar'),
|
||||||
|
|
||||||
|
'SSViewerTest_GlobalAutomatic',
|
||||||
|
'SSViewerTest_GlobalReferencedByString' => 'get_reference',
|
||||||
|
'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
|
||||||
|
|
||||||
|
'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix')
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get_html() {
|
||||||
|
return '<div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
static function SSViewerTest_GlobalAutomatic() {
|
||||||
|
return 'automatic';
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get_reference() {
|
||||||
|
return 'reference';
|
||||||
|
}
|
||||||
|
|
||||||
|
static function get_argmix() {
|
||||||
|
return 'z' . implode(':', func_get_args()) . 'z';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -322,28 +322,87 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
|
|||||||
|
|
||||||
protected function createCallableArray(&$extraArray, $interfaceToQuery, $createObject = false) {
|
protected function createCallableArray(&$extraArray, $interfaceToQuery, $createObject = false) {
|
||||||
$implementers = ClassInfo::implementorsOf($interfaceToQuery);
|
$implementers = ClassInfo::implementorsOf($interfaceToQuery);
|
||||||
if ($implementers && count($implementers) > 0) {
|
if($implementers) foreach($implementers as $implementer) {
|
||||||
foreach($implementers as $implementer) {
|
|
||||||
if ($createObject) $implementer = new $implementer(); //create a new instance of the object for method calls
|
|
||||||
$exposedVariables = $implementer::get_exposed_variables(); //get the exposed variables
|
|
||||||
|
|
||||||
foreach($exposedVariables as $varName => $methodName) {
|
// Create a new instance of the object for method calls
|
||||||
if (!$varName || is_numeric($varName)) $varName = $methodName; //array has just a single value, use it for both key and value
|
if ($createObject) $implementer = new $implementer();
|
||||||
|
|
||||||
//e.g. "array(Director, absoluteBaseURL)" means call "Director::absoluteBaseURL()"
|
// Get the exposed variables
|
||||||
$extraArray[$varName] = array($implementer, $methodName);
|
$exposedVariables = $implementer::get_exposed_variables();
|
||||||
$firstCharacter = substr($varName, 0, 1);
|
|
||||||
|
|
||||||
if ((strtoupper($firstCharacter) === $firstCharacter)) { //is uppercase, so save the lowercase version, too
|
foreach($exposedVariables as $varName => $details) {
|
||||||
$extraArray[lcfirst($varName)] = array($implementer, $methodName); //callable array
|
if (!is_array($details)) $details = array('method' => $details, 'casting' => Object::get_static('ViewableData', 'default_cast'));
|
||||||
} else { //is lowercase, save a version so it also works uppercase
|
|
||||||
$extraArray[ucfirst($varName)] = array($implementer, $methodName);
|
// If just a value (and not a key => value pair), use it for both key and value
|
||||||
}
|
if (is_numeric($varName)) $varName = $details['method'];
|
||||||
}
|
|
||||||
|
// Add in a reference to the implementing class (might be a string class name or an instance)
|
||||||
|
$details['implementer'] = $implementer;
|
||||||
|
|
||||||
|
// And a callable array
|
||||||
|
if (isset($details['method'])) $details['callable'] = array($implementer, $details['method']);
|
||||||
|
|
||||||
|
// Save with both uppercase & lowercase first letter, so either works
|
||||||
|
$extraArray[lcfirst($varName)] = $details;
|
||||||
|
$extraArray[ucfirst($varName)] = $details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInjectedValue($property, $params, $cast = true) {
|
||||||
|
// Check if the method to-be-called exists on the target object, and if so don't check global objects
|
||||||
|
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
|
||||||
|
if (isset($on->$property) || method_exists($on, $property)) return null;
|
||||||
|
|
||||||
|
// Find the source of the value
|
||||||
|
$source = null;
|
||||||
|
|
||||||
|
// Check for a presenter-specific override
|
||||||
|
if (array_key_exists($property, $this->extras)) {
|
||||||
|
$source = array('value' => $this->extras[$property]);
|
||||||
|
}
|
||||||
|
// Then for iterator-specific overrides
|
||||||
|
else if (array_key_exists($property, self::$iteratorProperties)) {
|
||||||
|
$source = self::$iteratorProperties[$property];
|
||||||
|
|
||||||
|
if ($this->itemIterator) {
|
||||||
|
// Set the current iterator position and total (the object instance is the first item in the callable array)
|
||||||
|
$source['implementer']->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
|
||||||
|
} else {
|
||||||
|
// If we don't actually have an iterator at the moment, act like a list of length 1
|
||||||
|
$source['implementer']->iteratorProperties(0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And finally for global overrides
|
||||||
|
else if (array_key_exists($property, self::$globalProperties)) {
|
||||||
|
$source = self::$globalProperties[$property]; //get the method call
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($source) {
|
||||||
|
$res = array();
|
||||||
|
|
||||||
|
// Look up the value - either from a callable, or from a directly provided value
|
||||||
|
if (isset($source['callable'])) $res['value'] = call_user_func_array($source['callable'], $params);
|
||||||
|
elseif (isset($source['value'])) $res['value'] = $source['value'];
|
||||||
|
else throw new InvalidArgumentException("Injected property $property does't have a value or callable value source provided");
|
||||||
|
|
||||||
|
// If we want to provide a casted object, look up what type object to use
|
||||||
|
if ($cast) {
|
||||||
|
// Get the object to cast as
|
||||||
|
$casting = isset($source['casting']) ? $source['casting'] : null;
|
||||||
|
// If not provided, use default
|
||||||
|
if (!$casting) $casting = Object::get_static('ViewableData', 'default_cast');
|
||||||
|
|
||||||
|
$obj = new $casting($property);
|
||||||
|
$obj->setValue($res['value']);
|
||||||
|
|
||||||
|
$res['obj'] = $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function __call($name, $arguments) {
|
function __call($name, $arguments) {
|
||||||
//extract the method name and parameters
|
//extract the method name and parameters
|
||||||
$property = $arguments[0]; //the name of the function being called
|
$property = $arguments[0]; //the name of the function being called
|
||||||
@ -351,59 +410,26 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
|
|||||||
if (isset($arguments[1]) && $arguments[1] != null) $params = $arguments[1]; //the function parameters in an array
|
if (isset($arguments[1]) && $arguments[1] != null) $params = $arguments[1]; //the function parameters in an array
|
||||||
else $params = array();
|
else $params = array();
|
||||||
|
|
||||||
//check if the method to-be-called exists on the target object
|
$hasInjected = $res = null;
|
||||||
$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
|
|
||||||
if (method_exists($on, $property)) { //return the result immediately without trying global functions
|
if ($name == 'hasValue') {
|
||||||
|
if ($val = $this->getInjectedValue($property, $params, false)) {
|
||||||
|
$hasInjected = true; $res = (bool)$val['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // XML_val
|
||||||
|
if ($val = $this->getInjectedValue($property, $params)) {
|
||||||
|
$hasInjected = true; $obj = $val['obj']; $res = $obj->forTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasInjected) {
|
||||||
|
$this->resetLocalScope();
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
else {
|
||||||
return parent::__call($name, $arguments);
|
return parent::__call($name, $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We create a specific object instance, so that we can determine "unset" from "null" and "false"
|
|
||||||
static $nomatch = null;
|
|
||||||
if ($nomatch === null) $nomatch = new stdClass();
|
|
||||||
|
|
||||||
// Start off with no match
|
|
||||||
$value = $nomatch;
|
|
||||||
|
|
||||||
// Check for a presenter-specific override
|
|
||||||
if (array_key_exists($property, $this->extras)) {
|
|
||||||
$value = $this->extras[$property];
|
|
||||||
}
|
|
||||||
// Then for iterator-specific overrides
|
|
||||||
else if (array_key_exists($property, self::$iteratorProperties)) {
|
|
||||||
$value = self::$iteratorProperties[$property];
|
|
||||||
|
|
||||||
if ($this->itemIterator) {
|
|
||||||
// Set the current iterator position and total (the object instance is the first item in the callable array)
|
|
||||||
$value[0]->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
|
|
||||||
} else {
|
|
||||||
// If we don't actually have an iterator at the moment, act like a list of length 1
|
|
||||||
$value[0]->iteratorProperties(0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// And finally for global overrides
|
|
||||||
else if (array_key_exists($property, self::$globalProperties)) {
|
|
||||||
$value = self::$globalProperties[$property]; //get the method call
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value !== $nomatch) {
|
|
||||||
$this->resetLocalScope(); //if we are inside a chain (e.g. $A.B.C.Up.E) break out to the beginning of it
|
|
||||||
|
|
||||||
//only call callable functions
|
|
||||||
if (is_callable($value)) {
|
|
||||||
//$value = call_user_func_array($value, array_slice($arguments, 1));
|
|
||||||
$value = call_user_func_array($value, $params);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($name) {
|
|
||||||
case 'hasValue':
|
|
||||||
return (bool)$value;
|
|
||||||
default: //XML_val
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$callResult = parent::__call($name, $arguments);
|
|
||||||
return $callResult;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user