failover) { $this->addMethodsFrom('failover'); } if(isset($_GET['debugfailover'])) { Debug::message("$this->class / $this->failover"); } // Set up cached methods $methodNames = $this->allMethodNames(); foreach($methodNames as $methodName) { if($methodName[0] == "_") { $trimmedName = substr($methodName,1); $this->createMethod($trimmedName, "return \$obj->cachedCall('$methodName', '$trimmedName', \$args);"); } } parent::defineMethods(); } /** * Accessor overloader. * Allows default getting of fields via $this->getVal(), or mediation via a * getParamName() method. * @param string $field The field name. * @return mixed The field. */ public function __get($field) { if($this->hasMethod($funcName = "get$field")) { return $this->$funcName(); } else if($this->hasField($field)) { return $this->getField($field); } else if($this->failover) { return $this->failover->$field; } } /** * Setter overloader. * Allows default setting of fields in $this->setVal(), or mediation via a * getParamName() method. * @param string $field The field name. * @param mixed $val The field value. */ public function __set($field, $val) { if(method_exists($this, $funcName = "set$field")) { return $this->$funcName($val); } else { $this->setField($field, $val); } } /** * Is-set overloader. * Will check to see if the given field exists on this object. Calls the hasField() method, * as well as checking failover classes. * @param string $field The field name. * @return boolean True if field exists */ public function __isset($field) { if($this->hasField($field)) { return true; } if($this->failover && $this->failover->hasField($field)) { return true; } return false; } /** * Get a field by it's name. This should be overloaded in child classes. * @param string $field fieldname */ protected function getField($field) { } /** * Set a fields value. This should be overloaded in child classes. * @param string $field The field name. * @param mixed $val The field value. */ protected function setField($field, $val) { $this->$field = $val; } /** * Checks if a field exists on this object. This should be overloaded in child classes. * @param string $field The field name * @return boolean */ public function hasField($field) { } /** * Cache used by castingHelperPair(). * @var array */ protected static $castingHelperPair_cache; /** * Returns the "casting helper" for the given field and the casting class name. A casting helper * is a piece of PHP code that, when evaluated, will create an object to represent the value. * * The return value is an map containing two values: * - className: The name of the class (eg: 'Varchar') * - castingHelper: The casting helper (eg: 'return new Varchar($fieldName);') * * @param string $field The field name * @return array */ public function castingHelperPair($field) { $class = $this->class; if(!isset(self::$castingHelperPair_cache[$class])) { if($this->failover) { $this->failover->buildCastingHelperCache(self::$castingHelperPair_cache[$class]); } $this->buildCastingHelperCache(self::$castingHelperPair_cache[$class]); self::$castingHelperPair_cache[$class]['ClassName'] = array("className" => "Varchar", "castingHelper" => "return new Varchar(\$fieldName);"); } return isset(self::$castingHelperPair_cache[$class][$field]) ? self::$castingHelperPair_cache[$class][$field] : null; } /** * A helper function used by castingHelperPair() to build the cache. * @param array */ public function buildCastingHelperCache(&$cache) { $class = $this->class; $classes = ClassInfo::ancestry($class); foreach($classes as $componentClass) { if($componentClass == "ViewableData") $isViewableData = true; if($componentClass == "DataObject") $isDataObject = true; if(isset($isDataObject) && $isDataObject) { $fields = eval("return {$componentClass}::\$db;"); if($fields) foreach($fields as $fieldName => $fieldSchema) { $cache[$fieldName] = ViewableData::castingObjectCreatorPair($fieldSchema); } } if(isset($isViewableData) && $isViewableData) { $fields = eval("return {$componentClass}::\$casting;"); if($fields) foreach($fields as $fieldName => $fieldSchema) { $cache[$fieldName] = ViewableData::castingObjectCreatorPair($fieldSchema); } } } } /** * Returns the "casting helper" for the given field. A casting helper * is a piece of PHP code that, when evaluated, will create an object to represent the value. * @param string $field The field name. * @return string */ public function castingHelper($field) { $pair = $this->castingHelperPair($field); return $pair['castingHelper']; } /** * Converts a field spec into an object creator. * For example: "Int" becomes "new Int($fieldName);" and "Varchar(50)" becomes "new Varchar($fieldName, 50);" * @param string $fieldSchema The field spec. * @return string */ public static function castingObjectCreator($fieldSchema) { if(strpos($fieldSchema,'(') === false) { return "return Object::create('{$fieldSchema}',\$fieldName);"; } else { return "return new " . ereg_replace('^([^(]+)\\(','\\1($fieldName,', $fieldSchema) . ';'; } } /** * Converts a field spec into an object creator pair; this is a map containing className and castingHelper. * See {@link castingObjectCreator} for more information. * @param string $fieldSchema The field spec. * @return array */ public static function castingObjectCreatorPair($fieldSchema) { if(strpos($fieldSchema,'(') === false) { return array( 'className' => $fieldSchema, 'castingHelper' => "return Object::create('{$fieldSchema}',\$fieldName);" ); } else if(ereg('^([^(]+)\\(', $fieldSchema, $parts)) { return array( 'className' => $parts[1], 'castingHelper' => "return new " . ereg_replace('^([^(]+)\\(','\\1($fieldName,', $fieldSchema) . ';', ); } else { user_error("castingObjectCreatorPair: Bad field schema '$fieldSchema' in class $this->class", E_USER_WARNING); } } /** * Return the object version of the given field/method. * @param string $fieldName The name of the field/method. * @param array $args The arugments. * @param boolean $forceReturnObject If true, this method will *always* return an object. If there's * no sensible one available, it will return new ViewableData() * @return mixed; */ public function obj($fieldName, $args = null, $forceReturnObject = false) { if(isset($_GET['debug_profile'])) { Profiler::mark("template($fieldName)", " on $this->class object"); } if($args) { $identifier = $fieldName . ',' . implode(',', $args); } else { $identifier = $fieldName; } if(isset($this->_object_cache[$identifier])) { $fieldObj = $this->_object_cache[$identifier]; } else { if($this->hasMethod($fieldName)) { $val = call_user_func_array(array(&$this, $fieldName), $args); } else { $val = $this->$fieldName; } $this->_natural_cache[$identifier] = $val; if(is_object($val)) { $fieldObj = $val; } else { $helperPair = $this->castingHelperPair($fieldName); if(!$helperPair && $this->failover) { $helperPair = $this->failover->castingHelperPair($fieldName); } $constructor = $helperPair['castingHelper']; if($constructor) { $fieldObj = eval($constructor); $fieldObj->setVal($val); } } $this->_object_cache[$identifier] = isset($fieldObj) ? $fieldObj : null; } if(!isset($fieldObj) && $forceReturnObject){ $fieldObj = new ViewableData(); } if(isset($_GET['debug_profile'])) { Profiler::unmark("template($fieldName)", " on $this->class object"); } return isset($fieldObj) ? $fieldObj : null; } /** * Return the value (non-object) version of the given field/method. * @deprecated */ public function val($fieldName, $args = null) { return $this->XML_val($fieldName, $args); } /** * Returns the value of the given field / method in an XML-safe format. * @param string $fieldName The field name. * @param array $args The arguments. * @param boolean $cache Cache calls to this function. * @return string */ public function XML_val($fieldName, $args = null, $cache = false) { if(isset($_GET['debug_profile'])) { Profiler::mark("template($fieldName)", " on $this->class object"); } if($cache) { if($args) { $identifier = $fieldName . ',' . implode(',', $args); } else { $identifier = $fieldName; } if(isset($this->_xml_cache[$identifier])) { if(isset($_GET['debug_profile'])) { Profiler::unmark("template($fieldName)", " on $this->class object"); } return $this->_xml_cache[$identifier]; } } // This will happen when cachedCall was called on an object; don't bother re-calling the method, just // do the conversion step below if($cache && isset($this->_object_cache[$identifier])) { $val = $this->_object_cache[$identifier]; // Get the field / method } else { if($this->hasMethod($fieldName)) { $val = call_user_func_array(array(&$this, $fieldName), $args); } else { $val = $this->$fieldName; } if(isset($identifier)) { $this->_natural_cache[$identifier] = $val; } } // Case 1: object; converted to XML_val() by if(is_object($val)) { if($cache) { $this->_object_cache[$identifier] = $val; } $val = $val->forTemplate(); if($cache) { $this->_xml_cache[$identifier] = $val; } } else { // Identify the 'casted class' of this field, which will give us some hints about what kind of // data has been returned if(isset($_GET['debug_profile'])) { Profiler::mark('casting cost'); } $helperPair = $this->castingHelperPair($fieldName); $castedClass = $helperPair['className']; // Note: these probably shouldn't be hard-coded. But right now it's not a problem, and I don't // want to over-engineer if(!$castedClass || $castedClass == 'HTMLText' || $castedClass == 'HTMLVarchar' || $castedClass == 'Text') { // Case 2: the value is already XML-safe, just return it } else { // Case 3: the value is raw and must be made XML-safe $val = Convert::raw2xml($val); } if(isset($_GET['debug_profile'])) { Profiler::unmark('casting cost'); } if($cache) { $this->_xml_cache[$identifier] = $val; } } if(isset($_GET['debug_profile'])) { Profiler::unmark("template($fieldName)", " on $this->class object"); } return $val; } /** * Return a named array of calls to XML_val with different parameters. * Each value in the array is used as the first argument to XML_val. The result is a named array of the return values. * * The intended use-case is when converting simple templates to PHP methods to optimise code, as we did in the form classes. * If you're calling renderWith more than a few times on a very simple template, this can be useful. * * extract(getXMLValues(array('Title','Field','Message'))) * // You can now use $Title, $Field, and $Message as you would in a template * * @param array $elementList The list of field names. * @return array */ public function getXMLValues($elementList) { foreach($elementList as $elementName) { $result[$elementName] = $this->XML_val($elementName); } return $result; } /** * Return the value of the given field without any escaping. * @param string $fieldName The field name. * @param array $args The arguments. * @return string */ public function RAW_val($fieldName, $args = null) { return Convert::xml2raw($this->XML_val($fieldName, $args)); } /** * Return the value of the given field in an SQL safe format. * @param string $fieldName The field name. * @param array $args The arguments. * @return string */ public function SQL_val($fieldName, $args = null) { return Convert::xml2sql($this->XML_val($fieldName, $args)); } /** * Return the value of the given field in an JavaScript safe format. * @param string $fieldName The field name. * @param array $args The arguments. * @return string */ public function JS_val($fieldName, $args = null) { return Convert::xml2js($this->XML_val($fieldName, $args)); } /** * Return the value of the given field in an XML attribute safe format. * @param string $fieldName The field name. * @param array $args The arguments. * @return string */ public function ATT_val($fieldName, $args = null) { return Convert::xml2att($this->XML_val($fieldName, $args)); } /** * SSViewer's data-access method. * All template calls to ViewableData are fed through this function. It takes care of caching * data, and linking up parents to support Menu1_Menu2() syntax for nested data. * @param string $funcName the method to call * @param string $identifier * @param array $args The arguments * @return mixed */ function cachedCall($funcName, $identifier = null, $args = null) { if(isset($_GET['debug_profile'])) { Profiler::mark("template($funcName)", " on $this->class"); } if(!$identifier) { if($args) { $identifier = $funcName . ',' . implode(',', $args); } else { $identifier = $funcName; } } if(isset($this->_natural_cache[$identifier])) { if(isset($_GET['debug_profile'])) { Profiler::unmark("template($funcName)", " on $this->class"); } return $this->_natural_cache[$identifier]; } if($this->hasMethod($funcName)) { $val = call_user_func_array(array(&$this, $funcName), $args); } else { $val = $this->$funcName; } $this->_natural_cache[$identifier] = $val; if(is_object($val)) { $this->_object_cache[$identifier] = $val; } else { $helperPair = $this->castingHelperPair($funcName); $castedClass = $helperPair['className']; if($castedClass && $castedClass != 'HTMLText' && $castedClass != 'HTMLVarchar' && $castedClass != 'Text') { $val = Convert::raw2xml($val); } $this->_xml_cache[$identifier] = $val; } if(isset($_GET['debug_profile'])) { Profiler::unmark("template($funcName)", " on $this->class"); } return $val; } /** * @param $obj ViewableData_Customised|ViewableData_ObjectCustomised */ function setCustomisedObj($obj) { $this->customisedObj = $obj; } /** * Returns true if the given method/parameter has a value * If the item is an object, it will use the exists() method to determine existence * @param string $funcName The function name. * @param array $args The arguments. * @return boolean */ function hasValue($funcName, $args = null) { $test = $this->cachedCall($funcName, null, $args); if(is_object($test)) { return $test->exists(); } else if($test && $test !== '
') { return true; } } /** * Set up the "iterator properties" for this object. * These are properties that give information about where we are in the set. * @param int $pos Position in iterator * @param int $totalItems Total number of items */ function iteratorProperties($pos, $totalItems) { $this->iteratorPos = $pos; $this->iteratorTotalItems = $totalItems; } /** * Returns true if this item is the first in the container set. * @return boolean */ function First() { return $this->iteratorPos == 0; } /** * Returns true if this item is the last in the container set. * @return boolean */ function Last() { return $this->iteratorPos == $this->iteratorTotalItems - 1; } /** * Returns 'first' if this item is the first in the container set. * Returns 'last' if this item is the last in the container set. */ function FirstLast() { if($this->iteratorPos == 0) { return "first"; } else if($this->iteratorPos == $this->iteratorTotalItems - 1) { return "last"; } else { return ""; } } /** * Returns 'middle' if this item is between first and last. * @return boolean */ function MiddleString(){ if($this->Middle()) return "middle"; else return ""; } /** * Returns true if this item is one of the middle items in the container set. * @return boolean */ function Middle() { return $this->iteratorPos > 0 && $this->iteratorPos < $this->iteratorTotalItems - 1; } /** * Returns true if this item is an even item in the container set. * @return boolean */ function Even() { return $this->iteratorPos % 2; } /** * Returns true if this item is an even item in the container set. * @return boolean */ function Odd() { return !$this->iteratorPos % 2; } /** * Returns 'even' if this item is an even item in the container set. * Returns 'odd' if this item is an odd item in the container set. * @return string */ function EvenOdd() { return $this->Even() ? 'even' : 'odd'; } /** * Returns the numerical number of this item in the dataset. * The count starts from $startIndex, which defaults to 1. * @param int $startIndex Number to start count from. * @return int */ function Pos($startIndex = 1) { return $this->iteratorPos + $startIndex; } /** * Return the total number of "sibling" items in the dataset. * @return int */ function TotalItems() { return $this->iteratorTotalItems; } /** * Returns the currently logged in user. * @return Member */ function CurrentMember() { return Member::currentUser(); } /** * Returns the Security ID. * This is used to prevent CRSF attacks in forms. * @return int */ function SecurityID() { if(Session::get('SecurityID')) { $securityID = Session::get('SecurityID'); } else { $securityID = rand(); Session::set('SecurityID', $securityID); } return $securityID; } /** * Checks if the current user has the given permission. * Can be used to implement security-specific sections within templates * @return int The Permission record-ID if the permission can be found, null otherwise */ function HasPerm($permCode) { return Permission::check($permCode); } /** * Add some arbitrary data to this viewabledata object. Returns a new object with the * merged data. * @param mixed $data The data to add. * @return ViewableData */ function customise($data) { if(is_array($data)) { return new ViewableData_Customised($this, $data); } else if(is_object($data)) { return new ViewableData_ObjectCustomised($this, $data); } else { return $this; } } /** * Render this data using the given template, and return the result as a string * You can pass one of the following: * - A template name. * - An array of template names. The first template that exists will be used. * - An SSViewer object. * @param string|array|SSViewer The template. * @return string */ function renderWith($template) { if(!is_object($template)) { $template = new SSViewer($template); } // if the object is already customised (e.g. through Controller->run()), use it $obj = ($this->customisedObj) ? $this->customisedObj : $this; if(is_a($template,'SSViewer')) { return $template->process($obj); } else { user_error("ViewableData::renderWith() Was passed a $template->class object instead of a SSViewer object", E_USER_ERROR); } } /** * Return the site's absolute base URL, with a slash on the end. * @return string */ function BaseHref() { return Director::absoluteBaseURL(); } /** * When rendering some objects it is necessary to iterate over the object being rendered, to * do this, you need access to itself. * * @return ViewableData */ function Me() { return $this; } /** * Return a Debugger object. * This is set up like so that you can put $Debug.Content into your template to get debugging * information about $Content. * @return ViewableData_Debugger */ function Debug() { $d = new ViewableData_Debugger($this); return $d->forTemplate(); } /** * Returns the current controller * @return Controller */ function CurrentPage() { return Controller::curr(); } /** * Returns the top level ViewableData being rendered. * @return ViewableData */ function Top() { return SSViewer::topLevel(); } /** * Implementation of a "1 record iterator" * Views <%control %> tags operate by looping over an item for as many instances as are * available. When you stick a single ViewableData object in a control tag, the foreach() * loop still needs to work. We do this by creating an iterator that only returns one record. * This will always return the current ViewableData object. */ public function current() { if($this->show) { return $this; } } /** * Implementation of a "1 record iterator" * Views <%control %> tags operate by looping over an item for as many instances as are * available. When you stick a single ViewableData object in a control tag, the foreach() * loop still needs to work. We do this by creating an iterator that only returns one record. * Rewinds the iterator back to the start. */ public function rewind() { $this->show = true; } /** * Implementation of a "1 record iterator" * Views <%control %> tags operate by looping over an item for as many instances as are * available. When you stick a single ViewableData object in a control tag, the foreach() * loop still needs to work. We do this by creating an iterator that only returns one record. * Return the key for the current object. */ public function key() { return 0; } /** * Implementation of a "1 record iterator" * Views <%control %> tags operate by looping over an item for as many instances as are * available. When you stick a single ViewableData object in a control tag, the foreach() * loop still needs to work. We do this by creating an iterator that only returns one record. * Get the next object. */ public function next() { return $this->show = false; } /** * Implementation of a "1 record iterator" * Views <%control %> tags operate by looping over an item for as many instances as are * available. When you stick a single ViewableData object in a control tag, the foreach() * loop still needs to work. We do this by creating an iterator that only returns one record. * Check if there is a current object. */ public function valid() { return $this->show; } /** * Returns the root directory of the theme we're working with. * This can be useful for referencing images within the theme. For example, you might put a reference to * in your template. * * If your image is within a subtheme, such as mytheme_forum, you can set the subtheme parameter. For example, * * * We don't recommend that you use this method when no theme is selected. That is, we recommend that you only put * $ThemeDir into your theme templates. However, if no theme is selected, this will be the project folder/ * * @param subtheme The subtheme name. */ public function ThemeDir($subtheme = null) { $theme = SSViewer::current_theme(); if($theme) { return "themes/$theme" . ($subtheme ? "_$subtheme" : ""); } else { return project(); } } /** * Internal state toggler for the "1 record iterator" * @var bool */ private $show; /** * Object-casting information for class methods * @var mixed */ public static $casting = null; /** * Keep a record of the parent node of this data node. * @var mixed */ protected $parent = null; /** * Keep a record of the parent node of this data node. * @var mixed */ protected $namedAs = null; } /** * A ViewableData object that has been customised with extra data. Use * ViewableData->customise() to create. * @package sapphire * @subpackage view */ class ViewableData_Customised extends ViewableData { public function castingHelperPair($field) { return $this->obj->castingHelperPair($field); } function __construct($obj, $extraData) { $this->obj = $obj; $this->obj->setCustomisedObj($this); $this->extraData = $extraData; parent::__construct(); } function __call($funcName, $args) { if(isset($this->extraData[$funcName])) { return $this->extraData[$funcName]; } else { return call_user_func_array(array(&$this->obj, $funcName), $args); } } function __get($fieldName) { if(isset($this->extraData[$fieldName])) { return $this->extraData[$fieldName]; } return $this->obj->$fieldName; } function __set($fieldName, $val) { if(isset($this->extraData[$fieldName])) unset($this->extraData[$fieldName]); return $this->obj->$fieldName = $val; } function hasMethod($funcName) { return isset($this->extraData[$funcName]) || $this->obj->hasMethod($funcName); } function XML_val($fieldName, $args = null) { if(isset($this->extraData[$fieldName])) { if(isset($_GET['debug_profile'])) { Profiler::mark("template($fieldName)", " on $this->class object"); } if(is_object($this->extraData[$fieldName])) { $val = $this->extraData[$fieldName]->forTemplate(); } else { $val = $this->extraData[$fieldName]; } if(isset($_GET['debug_profile'])) { Profiler::unmark("template($fieldName)", " on $this->class object"); } return $val; } else { return $this->obj->XML_val($fieldName, $args); } } function obj($fieldName, $args = null, $forceReturnObject = false) { if(isset($this->extraData[$fieldName])) { if(!is_object($this->extraData[$fieldName])) { user_error("ViewableData_Customised::obj() '$fieldName' was requested from the array data as an object but it's not an object. I can't cast it.", E_USER_WARNING); } return $this->extraData[$fieldName]; } else { return $this->obj->obj($fieldName, $args, $forceReturnObject); } } function cachedCall($funcName, $identifier = null, $args = null) { if(isset($this->extraData[$funcName])) { return $this->extraData[$funcName]; } else { return $this->obj->cachedCall($funcName, $identifier, $args); } } function customise($data) { if(is_array($data)) { $this->extraData = array_merge($this->extraData, $data); return $this; } else { return parent::customise($data); } } /** * Original ViewableData object * @var ViewableDate */ protected $obj; /** * Array containing the extra data * @var array */ protected $extraData; } /** * A ViewableData object that has been customised with an extra object. Use * ViewableData->customise() to create. * @package sapphire * @subpackage view */ class ViewableData_ObjectCustomised extends ViewableData { function __construct($obj, $extraObj) { $this->obj = $obj; $this->extraObj = $extraObj; $this->obj->setCustomisedObj($this); parent::__construct(); } function __call($funcName, $args) { if($this->extraObj->hasMethod($funcName)) { return call_user_func_array(array(&$this->extraObj, $funcName), $args); } else { return call_user_func_array(array(&$this->obj, $funcName), $args); } } function __get($fieldName) { if($this->extraObj->hasField($fieldName)) { return $this->extraObj->$fieldName; } else { return $this->obj->$fieldName; } } function __set($fieldName, $val) { $this->extraObj->$fieldName = $val; $this->obj->$fieldName = $val; } function hasMethod($funcName) { return $this->extraObj->hasMethod($funcName) || $this->obj->hasMethod($funcName); } function cachedCall($funcName, $identifier = null, $args = null) { $result = $this->extraObj->cachedCall($funcName, $identifier, $args); if(!$result) { $result = $this->obj->cachedCall($funcName, $identifier, $args); } return $result; } function obj($fieldName, $args = null, $forceReturnObject = false) { if($this->extraObj->hasMethod($fieldName) || $this->extraObj->hasField($fieldName)) { return $this->extraObj->obj($fieldName, $args, $forceReturnObject); } else { return $this->obj->obj($fieldName, $args, $forceReturnObject); } } /** * The extra object. * @var ViewableData */ protected $extraObj; /** * The original object. * @var ViewableData */ protected $obj; } /** * Debugger helper. * @package sapphire * @subpackage view * @todo Finish this off */ class ViewableData_Debugger extends ViewableData { /** * The original object * @var ViewableData */ protected $obj; function __construct($obj) { $this->obj = $obj; parent::__construct(); } /** * Return debugging information, as XHTML. If a field name is passed, * it will show debugging information on that field, otherwise it will show * information on all methods and fields. * @var string $field The field name. * @return string */ function forTemplate($field = null) { if($field) { return "Info on $field: