From 927dbbe717be3975c3e763bd462a8b6ebbe0a035 Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Sat, 11 Feb 2012 15:08:39 +1300 Subject: [PATCH] API-CHANGE: Global template variables can now be called directly using SSViewer_DataPresenter instead of needing to inherit off ViewableData --- admin/code/LeftAndMain.php | 2 +- control/Controller.php | 8 ++- control/Director.php | 19 +++++- core/Core.php | 1 + i18n/i18n.php | 9 ++- security/Member.php | 9 ++- security/Permission.php | 8 ++- security/SecurityToken.php | 19 +++++- tests/control/ControllerTest.php | 4 +- tests/control/RequestHandlingTest.php | 2 +- tests/view/SSViewerTest.php | 46 +++++++++++++++ view/SSViewer.php | 55 +++++++++++++----- view/TemplateGlobalProvider.php | 21 +++++++ view/ViewableData.php | 83 +-------------------------- 14 files changed, 180 insertions(+), 106 deletions(-) create mode 100644 view/TemplateGlobalProvider.php diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 40ab6ef25..13cfa82c3 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -1311,7 +1311,7 @@ class LeftAndMain extends Controller { * @return String */ function Locale() { - return DBField::create('DBLocale', $this->i18nLocale()); + return DBField::create('DBLocale', i18n::get_locale()); } /** diff --git a/control/Controller.php b/control/Controller.php index e53aaa7bd..c25dfdbf4 100644 --- a/control/Controller.php +++ b/control/Controller.php @@ -9,7 +9,7 @@ * @package sapphire * @subpackage control */ -class Controller extends RequestHandler { +class Controller extends RequestHandler implements TemplateGlobalProvider { /** * @var array $urlParams An array of arguments extracted from the URL @@ -583,6 +583,12 @@ class Controller extends RequestHandler { return $result; } + + public static function getExposedVariables() { + return array( + 'CurrentPage' => 'curr', + ); + } } diff --git a/control/Director.php b/control/Director.php index 81f1fa29f..44115a365 100644 --- a/control/Director.php +++ b/control/Director.php @@ -11,7 +11,7 @@ * @subpackage control * @see Director::direct(),Director::addRules(),Director::set_environment_type() */ -class Director { +class Director implements TemplateGlobalProvider { static private $urlParams; @@ -866,4 +866,21 @@ class Director { return false; } + /** + * @return array Returns an array of strings of the method names of methods on the call that should be exposed + * as global variables in the templates. + */ + public static function getExposedVariables() { + return array( + 'absoluteBaseURL', + 'baseURL', + 'is_ajax', + 'isAjax' => 'is_ajax', + 'BaseHref' => 'absoluteBaseURL', //@deprecated 3.0 + ); + } + } + + + diff --git a/core/Core.php b/core/Core.php index 234d3bad8..3c2c85f1b 100644 --- a/core/Core.php +++ b/core/Core.php @@ -227,6 +227,7 @@ set_include_path($includePath); require_once 'cache/Cache.php'; require_once 'core/Object.php'; require_once 'core/ClassInfo.php'; +require_once 'view/TemplateGlobalProvider.php'; require_once 'control/Director.php'; require_once 'dev/Debug.php'; require_once 'filesystem/FileFinder.php'; diff --git a/i18n/i18n.php b/i18n/i18n.php index 0e0553c2d..eecc8cf7c 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -57,7 +57,7 @@ * @package sapphire * @subpackage misc */ -class i18n extends Object { +class i18n extends Object implements TemplateGlobalProvider { /** * This static variable is used to store the current defined locale. @@ -1956,6 +1956,13 @@ class i18n extends Object { } } } + + public static function getExposedVariables() { + return array( + 'i18nLocale' => 'get_locale', + 'get_locale', + ); + } } diff --git a/security/Member.php b/security/Member.php index 51b0128be..9b9eac476 100644 --- a/security/Member.php +++ b/security/Member.php @@ -5,7 +5,7 @@ * @package sapphire * @subpackage security */ -class Member extends DataObject { +class Member extends DataObject implements TemplateGlobalProvider { static $db = array( 'FirstName' => 'Varchar', @@ -1378,6 +1378,13 @@ class Member extends DataObject { // If can't find a suitable editor, just default to cms return $currentName ? $currentName : 'cms'; } + + public static function getExposedVariables() { + return array( + 'CurrentMember' => 'currentUser', + 'currentUser' + ); + } } /** diff --git a/security/Permission.php b/security/Permission.php index b856c6a85..ac6816152 100644 --- a/security/Permission.php +++ b/security/Permission.php @@ -4,7 +4,7 @@ * @package sapphire * @subpackage security */ -class Permission extends DataObject { +class Permission extends DataObject implements TemplateGlobalProvider { // the (1) after Type specifies the DB default value which is needed for // upgrades from older SilverStripe versions @@ -621,6 +621,12 @@ class Permission extends DataObject { // Just in case we've altered someone's permissions Permission::flush_permission_cache(); } + + public static function getExposedVariables() { + return array( + 'HasPerm' => 'check' + ); + } } diff --git a/security/SecurityToken.php b/security/SecurityToken.php index c1a996ddb..ec32ab72e 100644 --- a/security/SecurityToken.php +++ b/security/SecurityToken.php @@ -28,7 +28,7 @@ * * @todo Make token name form specific for additional forgery protection. */ -class SecurityToken extends Object { +class SecurityToken extends Object implements TemplateGlobalProvider { /** * @var String @@ -102,6 +102,15 @@ class SecurityToken extends Object { static function get_default_name() { return self::$default_name; } + + /** + * Returns the value of an the global SecurityToken in the current session + * @return int + */ + static function getSecurityID() { + $token = SecurityToken::inst(); + return $token->getValue(); + } /** * @return String @@ -208,7 +217,13 @@ class SecurityToken extends Object { $generator = new RandomGenerator(); return $generator->generateHash('sha1'); } - + + public static function getExposedVariables() { + return array( + 'getSecurityID', + 'SecurityID' => 'getSecurityID' + ); + } } /** diff --git a/tests/control/ControllerTest.php b/tests/control/ControllerTest.php index 766e2c4ad..57f9ccd3d 100644 --- a/tests/control/ControllerTest.php +++ b/tests/control/ControllerTest.php @@ -129,11 +129,13 @@ class ControllerTest extends FunctionalTest { 'Without an allowed_actions, any defined methods are recognised as actions' ); } - + + /* Controller::BaseURL no longer exists, but was just a direct call to Director::BaseURL, so not sure what this code was supposed to test public function testBaseURL() { Director::setBaseURL('/baseurl/'); $this->assertEquals(Controller::BaseURL(), Director::BaseURL()); } + */ } /** diff --git a/tests/control/RequestHandlingTest.php b/tests/control/RequestHandlingTest.php index 493a9a6c0..72b62fe88 100644 --- a/tests/control/RequestHandlingTest.php +++ b/tests/control/RequestHandlingTest.php @@ -145,7 +145,7 @@ class RequestHandlingTest extends FunctionalTest { } function testMethodsOnParentClassesOfRequestHandlerDeclined() { - $response = Director::test('testGoodBase1/getSecurityID'); + $response = Director::test('testGoodBase1/getIterator'); $this->assertEquals(403, $response->getStatusCode()); } diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index cc647dfab..784f13c5e 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -84,6 +84,45 @@ SS $this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection'); $this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'), 'Escapes before injections are correctly unescaped'); } + + function testGlobalVariableCalls() { + $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::is_ajax(), $this->render('{$isAjax}'), 'All variations of is_ajax result in the correct call'); + $this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'), 'All variations of is_ajax result in the correct call'); + $this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'), 'All variations of is_ajax result in the correct call'); + $this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'), 'All variations of is_ajax result in the correct call'); + + $this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'), 'i18n template functions result correct result'); + $this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'), 'i18n template functions result correct result'); + + $this->assertEquals((string)Controller::curr(), $this->render('{$CurrentPage}'), 'i18n template functions result correct result'); + $this->assertEquals((string)Controller::curr(), $this->render('{$currentPage}'), 'i18n template functions result correct result'); + + $this->assertEquals(Member::currentUser(), $this->render('{$CurrentMember}'), 'Member template functions result correct result'); + $this->assertEquals(Member::currentUser(), $this->render('{$CurrentUser}'), 'Member template functions result correct result'); + $this->assertEquals(Member::currentUser(), $this->render('{$currentMember}'), 'Member template functions result correct result'); + $this->assertEquals(Member::currentUser(), $this->render('{$currentUser}'), 'Member 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'); + } + + function testGlobalVariableCallsWithArguments() { + $this->assertEquals(Permission::check("ADMIN"), $this->render('{$HasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result'); + $this->assertEquals(Permission::check("ADMIN"), $this->render('{$hasPerm(\'ADMIN\')}'), 'Permissions template functions result correct result'); + } + + /* //TODO: enable this test + function testLocalFunctionsTakePriorityOverGlobals() { + $data = new ArrayData(array( + 'Page' => new SSViewerTest_Page() + )); + + $result = $this->render('<% control Page %>$absoluteBaseURL<% end_control %>',$data); + $this->assertEquals("testPageCalled",$result, "Local Object's function called. Did not return the actual baseURL of the current site"); + }*/ function testObjectDotArguments() { $this->assertEquals( @@ -755,3 +794,10 @@ class SSViewerTest_ViewableData extends ViewableData implements TestOnly { class SSViewerTest_Controller extends Controller { } + +class SSViewerTest_Page extends SiteTree { + + function absoluteBaseURL() { + return "testPageCalled"; + } +} diff --git a/view/SSViewer.php b/view/SSViewer.php index d24e84416..881c1c31c 100644 --- a/view/SSViewer.php +++ b/view/SSViewer.php @@ -135,31 +135,58 @@ class SSViewer_Scope { */ class SSViewer_DataPresenter extends SSViewer_Scope { - private $extras; - - function __construct($item, $extras = null){ + private static $extras = array(); + + function __construct($item){ parent::__construct($item); - $this->extras = $extras; + + if (count(self::$extras) == 0) { //build up extras array only once per request + //get all the exposed variables from all classes that implement the TemplateGlobalProvider interface + $implementers = ClassInfo::implementorsOf("TemplateGlobalProvider"); + foreach($implementers as $implementer) { + $exposedVariables = $implementer::getExposedVariables(); //get the exposed variables + + foreach($exposedVariables as $varName => $methodName) { + if (!$varName || is_numeric($varName)) $varName = $methodName; //array has just a single value, use it for both key and value + + //e.g. "array(Director, absoluteBaseURL)" means call "Director::absoluteBaseURL()" + self::$extras[$varName] = array($implementer, $methodName); + $firstCharacter = substr($varName, 0, 1); + + if ((strtoupper($firstCharacter) === $firstCharacter)) { //is uppercase, so save the lowercase version, too + self::$extras[lcfirst($varName)] = array($implementer, $methodName); //callable array + } else { //is lowercase, save a version so it also works uppercase + self::$extras[ucfirst($varName)] = array($implementer, $methodName); + } + } + } + } } function __call($name, $arguments) { + //TODO: make local functions take priority over global functions + $property = $arguments[0]; - - if ($this->extras && array_key_exists($property, $this->extras)) { - - $this->resetLocalScope(); - - $value = $this->extras[$arguments[0]]; - + if (array_key_exists($property, self::$extras)) { + $this->resetLocalScope(); //if we are inside a chain (e.g. $A.B.C.Up.E) break out to the beginning of it + + $value = self::$extras[$property]; //get the method call + + //only call callable functions + if (is_callable($value)) { + $value = call_user_func_array($value, array_slice($arguments, 1)); + } + switch ($name) { case 'hasValue': return (bool)$value; - default: + default: //XML_val return $value; } } - - return parent::__call($name, $arguments); + + $callResult = parent::__call($name, $arguments); + return $callResult; } } diff --git a/view/TemplateGlobalProvider.php b/view/TemplateGlobalProvider.php new file mode 100644 index 000000000..e2f44a680 --- /dev/null +++ b/view/TemplateGlobalProvider.php @@ -0,0 +1,21 @@ + method-name) can optionally be supplied + * if the template variable name is different from the name of the method to call. The case of the first character + * in the method name called from the template does not matter, although names specified in the map should + * correspond to the actual method name in the relevant class. + * Note that the template renderer must be able to call these methods statically. + */ + public static function getExposedVariables(); +} + +?> \ No newline at end of file diff --git a/view/ViewableData.php b/view/ViewableData.php index 8506dd03f..a81d2cdac 100644 --- a/view/ViewableData.php +++ b/view/ViewableData.php @@ -689,75 +689,7 @@ class ViewableData extends Object implements IteratorAggregate { return Convert::raw2att(implode(' ', $classes)); } - - /** - * @see Member::currentUser() - */ - public function CurrentMember() { - return Member::currentUser(); - } - - /** - * Return a CSRF-preventing ID to insert into a form. - * - * @return string - */ - public function getSecurityID() { - $token = SecurityToken::inst(); - return $token->getValue(); - } - - /** - * @see Permission::check() - */ - public function HasPerm($code) { - return Permission::check($code); - } - - /** - * @see Director::absoluteBaseURL() - * - * @deprecated 3.0 - */ - public function BaseHref() { - Deprecation::notice('3.0', 'Use AbsoluteBaseURL instead.'); - - return $this->AbsoluteBaseURL(); - } - - /** - * Returns the absolute base url - * - * @return string - */ - public function AbsoluteBaseURL() { - return Director::absoluteBaseURL(); - } - - /** - * Access the BaseURL from template: proxy the value from the Director. - * Needed for building hardcoded links. - * - * @return string base url - */ - function BaseURL() { - return Director::baseURL(); - } - - /** - * @see Director::is_ajax() - */ - public function IsAjax() { - return Director::is_ajax(); - } - - /** - * @see i18n::get_locale() - */ - public function i18nLocale() { - return i18n::get_locale(); - } - + /** * Return debug information about this object that can be rendered into a template * @@ -767,19 +699,6 @@ class ViewableData extends Object implements IteratorAggregate { return new ViewableData_Debugger($this); } - /** - * @see Controller::curr() - */ - public function CurrentPage() { - return Controller::curr(); - } - - /** - * @see SSViewer::topLevel() - */ - public function Top() { - return SSViewer::topLevel(); - } } /**