BUGFIX: Add casting support to global and iterator variable injection

This commit is contained in:
Hamish Friedlander 2012-02-21 11:45:43 +13:00
parent fb246bdd08
commit 2c65d3a398
2 changed files with 149 additions and 69 deletions

View File

@ -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('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
$this->assertEquals('z<div></div>z', $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
$this->assertEquals('z&lt;div&gt;&lt;/div&gt;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';
}
}

View File

@ -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;
} }
} }