diff --git a/ORM/Connect/DBSchemaManager.php b/ORM/Connect/DBSchemaManager.php index bc390e1efc4..7fb1fbfeded 100644 --- a/ORM/Connect/DBSchemaManager.php +++ b/ORM/Connect/DBSchemaManager.php @@ -7,6 +7,9 @@ use Object; use Director; use SilverStripe\ORM\FieldType\DBPrimaryKey; +use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBPrimaryKey; + /** * Represents and handles all schema management for a database @@ -110,8 +113,7 @@ public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { /** * Initiates a schema update within a single callback * - * @var callable $callback - * @throws Exception + * @param callable $callback */ public function schemaUpdate($callback) { // Begin schema update @@ -153,15 +155,10 @@ public function schemaUpdate($callback) { break; } } - } catch(Exception $ex) { - $error = $ex; - } - // finally { + } finally { $this->schemaUpdateTransaction = null; $this->schemaIsUpdating = false; - // } - - if($error) throw $error; + } } /** @@ -303,7 +300,7 @@ protected function transInitTable($table) { * - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full * control over the index. * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type - * @param string|array $options SQL statement to append to the CREATE TABLE call. + * @param array $options Create table options (ENGINE, etc.) * @param array|bool $extensions List of extensions */ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true, @@ -314,7 +311,7 @@ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $ $this->alterationMessage("Table $table: created", "created"); } else { if (Config::inst()->get('SilverStripe\ORM\Connect\DBSchemaManager', 'check_and_repair_on_build')) { - $this->checkAndRepairTable($table, $options); + $this->checkAndRepairTable($table); } // Check if options changed @@ -353,12 +350,14 @@ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $ $fieldSpec = substr($fieldSpec, 0, $pos); } + /** @var DBField $fieldObj */ $fieldObj = Object::create_from_string($fieldSpec, $fieldName); - $fieldObj->arrayValue = $arrayValue; + $fieldObj->setArrayValue($arrayValue); $fieldObj->setTable($table); if($fieldObj instanceof DBPrimaryKey) { + /** @var DBPrimaryKey $fieldObj */ $fieldObj->setAutoIncrement($hasAutoIncPK); } @@ -383,7 +382,7 @@ public function dontRequireTable($table) { $suffix = ''; while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) { $suffix = $suffix - ? ($suffix + 1) + ? ((int)$suffix + 1) : 2; } $this->renameTable($table, "_obsolete_{$table}$suffix"); @@ -414,6 +413,8 @@ public function requireIndex($table, $index, $spec) { $specString = $this->convertIndexSpec($spec); // Check existing index + $oldSpecString = null; + $indexKey = null; if (!$newTable) { $indexKey = $this->indexKey($table, $index, $spec); $indexList = $this->indexList($table); @@ -502,6 +503,7 @@ protected function determineIndexType($spec) { * Converts an array or string index spec into a universally useful array * * @see convertIndexSpec() for approximate inverse + * @param string $name Index name * @param string|array $spec * @return array The resulting spec array with the required fields name, type, and value */ @@ -544,7 +546,9 @@ protected function parseIndexSpec($name, $spec) { */ protected function convertIndexSpec($indexSpec) { // Return already converted spec - if (!is_array($indexSpec)) return $indexSpec; + if (!is_array($indexSpec)) { + return $indexSpec; + } // Combine elements into standard string format return "{$indexSpec['type']} ({$indexSpec['value']})"; @@ -647,7 +651,7 @@ public function requireField($table, $field, $spec) { $new = preg_split("/'\s*,\s*'/", $newStr); $oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue); - $old = preg_split("/'\s*,\s*'/", $newStr); + $old = preg_split("/'\s*,\s*'/", $oldStr); $holder = array(); foreach ($old as $check) { @@ -690,7 +694,7 @@ public function dontRequireField($table, $fieldName) { $suffix = ''; while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) { $suffix = $suffix - ? ($suffix + 1) + ? ((int)$suffix + 1) : 2; } $this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix"); @@ -942,10 +946,10 @@ abstract public function fieldList($table); * This allows the cached values for a table's field list to be erased. * If $tablename is empty, then the whole cache is erased. * - * @param string|bool $tableName + * @param string $tableName * @return boolean */ - public function clearCachedFieldlist($tableName = false) { + public function clearCachedFieldlist($tableName = null) { return true; } diff --git a/ORM/DB.php b/ORM/DB.php index 137d9648b7b..963df4c5685 100644 --- a/ORM/DB.php +++ b/ORM/DB.php @@ -65,14 +65,6 @@ public static function set_conn(SS_Database $connection, $name = 'default') { self::$connections[$name] = $connection; } - /** - * @deprecated since version 4.0 Use DB::set_conn instead - */ - public static function setConn(SS_Database $connection, $name = 'default') { - Deprecation::notice('4.0', 'Use DB::set_conn instead'); - self::set_conn($connection, $name); - } - /** * Get the global database connection. * @@ -103,7 +95,10 @@ public static function getConn($name = 'default') { */ public static function get_schema($name = 'default') { $connection = self::get_conn($name); - if($connection) return $connection->getSchemaManager(); + if($connection) { + return $connection->getSchemaManager(); + } + return null; } /** @@ -134,7 +129,10 @@ public static function build_sql(SQLExpression $expression, &$parameters, $name */ public static function get_connector($name = 'default') { $connection = self::get_conn($name); - if($connection) return $connection->getConnector(); + if($connection) { + return $connection->getConnector(); + } + return null; } /** @@ -273,13 +271,6 @@ public static function connection_attempted() { return self::$connection_attempted; } - /** - * @deprecated since version 4.0 DB::getConnect was never implemented and is obsolete - */ - public static function getConnect($parameters) { - Deprecation::notice('4.0', 'DB::getConnect was never implemented and is obsolete'); - } - /** * Execute the given SQL query. * @param string $sql The SQL query to execute @@ -371,7 +362,7 @@ public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ER */ public static function manipulate($manipulation) { self::$lastQuery = $manipulation; - return self::get_conn()->manipulate($manipulation); + self::get_conn()->manipulate($manipulation); } /** @@ -384,14 +375,6 @@ public static function get_generated_id($table) { return self::get_conn()->getGeneratedID($table); } - /** - * @deprecated since version 4.0 Use DB::get_generated_id instead - */ - public static function getGeneratedID($table) { - Deprecation::notice('4.0', 'Use DB::get_generated_id instead'); - return self::get_generated_id($table); - } - /** * Check if the connection to the database is active. * @@ -401,14 +384,6 @@ public static function is_active() { return ($conn = self::get_conn()) && $conn->isActive(); } - /** - * @deprecated since version 4.0 Use DB::is_active instead - */ - public static function isActive() { - Deprecation::notice('4.0', 'Use DB::is_active instead'); - return self::is_active(); - } - /** * Create the database and connect to it. This can be called if the * initial database connection is not successful because the database @@ -421,14 +396,6 @@ public static function create_database($database) { return self::get_conn()->selectDatabase($database, true); } - /** - * @deprecated since version 4.0 Use DB::create_database instead - */ - public static function createDatabase($connect, $username, $password, $database) { - Deprecation::notice('4.0', 'Use DB::create_database instead'); - return self::create_database($database); - } - /** * Create a new table. * @param string $table The name of the table @@ -438,7 +405,7 @@ public static function createDatabase($connect, $username, $password, $database) * - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" * for MySQL. * - 'temporary' - If true, then a temporary table will be created - * @param array $advancedOptions + * @param array $advancedOptions Advanced creation options * @return string The table name generated. This may be different from the table name, for example with * temporary tables. */ @@ -448,14 +415,6 @@ public static function create_table($table, $fields = null, $indexes = null, $op return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions); } - /** - * @deprecated since version 4.0 Use DB::create_table instead - */ - public static function createTable($table, $fields = null, $indexes = null, $options = null) { - Deprecation::notice('4.0', 'Use DB::create_table instead'); - return self::create_table($table, $fields, $indexes, $options); - } - /** * Create a new field on a table. * @param string $table Name of the table. @@ -466,14 +425,6 @@ public static function create_field($table, $field, $spec) { return self::get_schema()->createField($table, $field, $spec); } - /** - * @deprecated since version 4.0 Use DB::create_field instead - */ - public static function createField($table, $field, $spec) { - Deprecation::notice('4.0', 'Use DB::create_field instead'); - return self::create_field($table, $field, $spec); - } - /** * Generate the following table in the database, modifying whatever already exists * as necessary. @@ -492,18 +443,7 @@ public static function createField($table, $field, $spec) { public static function require_table($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true, $options = null, $extensions = null ) { - return self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, - $extensions); - } - - /** - * @deprecated since version 4.0 Use DB::require_table instead - */ - public static function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true, - $options = null, $extensions = null - ) { - Deprecation::notice('4.0', 'Use DB::require_table instead'); - return self::require_table($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions); + self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions); } /** @@ -514,15 +454,7 @@ public static function requireTable($table, $fieldSchema = null, $indexSchema = * @param string $spec The field specification. */ public static function require_field($table, $field, $spec) { - return self::get_schema()->requireField($table, $field, $spec); - } - - /** - * @deprecated since version 4.0 Use DB::require_field instead - */ - public static function requireField($table, $field, $spec) { - Deprecation::notice('4.0', 'Use DB::require_field instead'); - return self::require_field($table, $field, $spec); + self::get_schema()->requireField($table, $field, $spec); } /** @@ -536,14 +468,6 @@ public static function require_index($table, $index, $spec) { self::get_schema()->requireIndex($table, $index, $spec); } - /** - * @deprecated since version 4.0 Use DB::require_index instead - */ - public static function requireIndex($table, $index, $spec) { - Deprecation::notice('4.0', 'Use DB::require_index instead'); - self::require_index($table, $index, $spec); - } - /** * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename). * @@ -553,14 +477,6 @@ public static function dont_require_table($table) { self::get_schema()->dontRequireTable($table); } - /** - * @deprecated since version 4.0 Use DB::dont_require_table instead - */ - public static function dontRequireTable($table) { - Deprecation::notice('4.0', 'Use DB::dont_require_table instead'); - self::dont_require_table($table); - } - /** * See {@link SS_Database->dontRequireField()}. * @@ -571,14 +487,6 @@ public static function dont_require_field($table, $fieldName) { self::get_schema()->dontRequireField($table, $fieldName); } - /** - * @deprecated since version 4.0 Use DB::dont_require_field instead - */ - public static function dontRequireField($table, $fieldName) { - Deprecation::notice('4.0', 'Use DB::dont_require_field instead'); - self::dont_require_field($table, $fieldName); - } - /** * Checks a table's integrity and repairs it if necessary. * @@ -589,14 +497,6 @@ public static function check_and_repair_table($table) { return self::get_schema()->checkAndRepairTable($table); } - /** - * @deprecated since version 4.0 Use DB::check_and_repair_table instead - */ - public static function checkAndRepairTable($table) { - Deprecation::notice('4.0', 'Use DB::check_and_repair_table instead'); - self::check_and_repair_table($table); - } - /** * Return the number of rows affected by the previous operation. * @@ -606,14 +506,6 @@ public static function affected_rows() { return self::get_conn()->affectedRows(); } - /** - * @deprecated since version 4.0 Use DB::affected_rows instead - */ - public static function affectedRows() { - Deprecation::notice('4.0', 'Use DB::affected_rows instead'); - return self::affected_rows(); - } - /** * Returns a list of all tables in the database. * The table names will be in lower case. @@ -624,14 +516,6 @@ public static function table_list() { return self::get_schema()->tableList(); } - /** - * @deprecated since version 4.0 Use DB::table_list instead - */ - public static function tableList() { - Deprecation::notice('4.0', 'Use DB::table_list instead'); - return self::table_list(); - } - /** * Get a list of all the fields for the given table. * Returns a map of field name => field spec. @@ -643,14 +527,6 @@ public static function field_list($table) { return self::get_schema()->fieldList($table); } - /** - * @deprecated since version 4.0 Use DB::field_list instead - */ - public static function fieldList($table) { - Deprecation::notice('4.0', 'Use DB::field_list instead'); - return self::field_list($table); - } - /** * Enable supression of database messages. */ diff --git a/ORM/DataObject.php b/ORM/DataObject.php index f6e4423cd1b..1cbe4b803bb 100644 --- a/ORM/DataObject.php +++ b/ORM/DataObject.php @@ -2598,9 +2598,10 @@ public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) * * @param string $fieldName Name of the field * @param mixed $val New field value - * @return DataObject $this + * @return $this */ public function setField($fieldName, $val) { + $this->objCacheClear(); //if it's a has_one component, destroy the cache if (substr($fieldName, -2) == 'ID') { unset($this->components[substr($fieldName, 0, -2)]); diff --git a/ORM/FieldType/DBBoolean.php b/ORM/FieldType/DBBoolean.php index 077b1ea628f..406204ed9bc 100644 --- a/ORM/FieldType/DBBoolean.php +++ b/ORM/FieldType/DBBoolean.php @@ -42,9 +42,6 @@ public function NiceAsBoolean() { return ($this->value) ? 'true' : 'false'; } - /** - * Saves this field to the given data object. - */ public function saveInto($dataObject) { $fieldName = $this->name; if($fieldName) { diff --git a/ORM/FieldType/DBComposite.php b/ORM/FieldType/DBComposite.php index ecb9584d20b..e4a0199a45b 100644 --- a/ORM/FieldType/DBComposite.php +++ b/ORM/FieldType/DBComposite.php @@ -225,11 +225,14 @@ public function hasField($field) { * @param string $field * @param mixed $value * @param bool $markChanged + * @return $this */ public function setField($field, $value, $markChanged = true) { + $this->objCacheClear(); + // Skip non-db fields if(!$this->hasField($field)) { - return; + return $this; } // Set changed @@ -246,6 +249,7 @@ public function setField($field, $value, $markChanged = true) { // Set local record $this->record[$field] = $value; + return $this; } /** diff --git a/ORM/FieldType/DBDecimal.php b/ORM/FieldType/DBDecimal.php index 79476e4948f..1f89bdb1470 100644 --- a/ORM/FieldType/DBDecimal.php +++ b/ORM/FieldType/DBDecimal.php @@ -63,9 +63,6 @@ public function requireField() { DB::require_field($this->tableName, $this->name, $values); } - /** - * @param DataObject $dataObject - */ public function saveInto($dataObject) { $fieldName = $this->name; diff --git a/ORM/FieldType/DBField.php b/ORM/FieldType/DBField.php index 43ff6e8884a..fe400324919 100644 --- a/ORM/FieldType/DBField.php +++ b/ORM/FieldType/DBField.php @@ -48,12 +48,32 @@ */ abstract class DBField extends ViewableData { + /** + * Raw value of this field + * + * @var mixed + */ protected $value; + /** + * Table this field belongs to + * + * @var string + */ protected $tableName; + /** + * Name of this field + * + * @var string + */ protected $name; + /** + * Used for generating DB schema. {@see DBSchemaManager} + * + * @var array + */ protected $arrayValue; /** @@ -72,6 +92,19 @@ abstract class DBField extends ViewableData { */ private static $default_search_filter_class = 'PartialMatchFilter'; + private static $casting = array( + 'ATT' => 'HTMLFragment', + 'CDATA' => 'HTMLFragment', + 'HTML' => 'HTMLFragment', + 'HTMLATT' => 'HTMLFragment', + 'JS' => 'HTMLFragment', + 'RAW' => 'HTMLFragment', + 'RAWURLATT' => 'HTMLFragment', + 'URLATT' => 'HTMLFragment', + 'XML' => 'HTMLFragment', + 'ProcessedRAW' => 'HTMLFragment', + ); + /** * @var $default mixed Default-value in the database. * Might be overridden on DataObject-level, but still useful for setting defaults on @@ -97,6 +130,7 @@ public function __construct($name = null) { * @return DBField */ public static function create_field($className, $value, $name = null, $object = null) { + /** @var DBField $dbField */ $dbField = Object::create($className, $name, $object); $dbField->setValue($value, null, false); @@ -249,52 +283,105 @@ public function getTable() { } /** + * Determine 'default' casting for this field. + * * @return string */ public function forTemplate() { - return $this->XML(); + return Convert::raw2xml($this->getValue()); } + /** + * Gets the value appropriate for a HTML attribute string + * + * @return string + */ public function HTMLATT() { return Convert::raw2htmlatt($this->RAW()); } + /** + * urlencode this string + * + * @return string + */ public function URLATT() { return urlencode($this->RAW()); } + /** + * rawurlencode this string + * + * @return string + */ public function RAWURLATT() { return rawurlencode($this->RAW()); } + /** + * Gets the value appropriate for a HTML attribute string + * + * @return string + */ public function ATT() { return Convert::raw2att($this->RAW()); } + /** + * Gets the raw value for this field. + * Note: Skips processors implemented via forTemplate() + * + * @return mixed + */ public function RAW() { - return $this->value; + return $this->getValue(); } + /** + * Gets javascript string literal value + * + * @return string + */ public function JS() { return Convert::raw2js($this->RAW()); } /** * Return JSON encoded value + * * @return string */ public function JSON() { return Convert::raw2json($this->RAW()); } + /** + * Alias for {@see XML()} + * + * @return string + */ public function HTML(){ - return Convert::raw2xml($this->RAW()); + return $this->XML(); } + /** + * XML encode this value + * + * @return string + */ public function XML(){ return Convert::raw2xml($this->RAW()); } + /** + * Safely escape for XML string + * + * @return string + */ + public function CDATA() { + return $this->forTemplate(); + } + /** * Returns the value to be set in the database to blank this field. * Usually it's a choice between null, 0, and '' @@ -307,14 +394,15 @@ public function nullValue() { /** * Saves this field to the given data object. + * + * @param DataObject $dataObject */ public function saveInto($dataObject) { $fieldName = $this->name; - if($fieldName) { - $dataObject->$fieldName = $this->value; - } else { - user_error("DBField::saveInto() Called on a nameless '" . get_class($this) . "' object", E_USER_ERROR); + if(empty($fieldName)) { + throw new \BadMethodCallException("DBField::saveInto() Called on a nameless '" . get_class($this) . "' object"); } + $dataObject->$fieldName = $this->value; } /** @@ -353,9 +441,10 @@ public function scaffoldSearchField($title = null) { * won't work) * * @param string|bool $name + * @param string $name Override name of this field * @return SearchFilter */ - public function defaultSearchFilter($name = false) { + public function defaultSearchFilter($name = null) { $name = ($name) ? $name : $this->name; $filterClass = $this->stat('default_search_filter_class'); return new $filterClass($name); @@ -377,6 +466,22 @@ public function debug() { } public function __toString() { - return $this->forTemplate(); + return (string)$this->forTemplate(); + } + + /** + * @return array + */ + public function getArrayValue() { + return $this->arrayValue; + } + + /** + * @param array $value + * @return $this + */ + public function setArrayValue($value) { + $this->arrayValue = $value; + return $this; } } diff --git a/ORM/FieldType/DBHTMLText.php b/ORM/FieldType/DBHTMLText.php index 73efceb1f71..43ad94eb785 100644 --- a/ORM/FieldType/DBHTMLText.php +++ b/ORM/FieldType/DBHTMLText.php @@ -14,6 +14,12 @@ * Represents a large text field that contains HTML content. * This behaves similarly to {@link Text}, but the template processor won't escape any HTML content within it. * + * Options can be specified in a $db config via one of the following: + * - "HTMLFragment(['shortcodes=true', 'whitelist=meta,link'])" + * - "HTMLFragment('whitelist=meta,link')" + * - "HTMLFragment(['shortcodes=true'])". "HTMLText" is also a synonym for this. + * - "HTMLFragment('shortcodes=true')" + * * @see HTMLVarchar * @see Text * @see Varchar @@ -25,34 +31,78 @@ class DBHTMLText extends DBText { private static $escape_type = 'xml'; private static $casting = array( - "AbsoluteLinks" => "HTMLText", - "BigSummary" => "HTMLText", - "ContextSummary" => "HTMLText", - "FirstParagraph" => "HTMLText", - "FirstSentence" => "HTMLText", - "LimitCharacters" => "HTMLText", - "LimitSentences" => "HTMLText", - "Lower" => "HTMLText", - "LowerCase" => "HTMLText", - "Summary" => "HTMLText", - "Upper" => "HTMLText", - "UpperCase" => "HTMLText", - 'EscapeXML' => 'HTMLText', - 'LimitWordCount' => 'HTMLText', - 'LimitWordCountXML' => 'HTMLText', - 'NoHTML' => 'Text', + "AbsoluteLinks" => "HTMLFragment", + // DBText summary methods - override to HTMLFragment + "BigSummary" => "HTMLFragment", + "ContextSummary" => "HTMLFragment", // Same as DBText + "FirstParagraph" => "HTMLFragment", + "FirstSentence" => "HTMLFragment", + "LimitSentences" => "HTMLFragment", + "Summary" => "HTMLFragment", + // DBString conversion / summary methods - override to HTMLFragment + "LimitCharacters" => "HTMLFragment", + "LimitCharactersToClosestWord" => "HTMLFragment", + "LimitWordCount" => "HTMLFragment", + "LowerCase" => "HTMLFragment", + "UpperCase" => "HTMLFragment", + "NoHTML" => "Text", // Actually stays same as DBString cast ); - protected $processShortcodes = true; + /** + * Enable shortcode parsing on this field + * + * @var bool + */ + protected $processShortcodes = false; - protected $whitelist = false; + /** + * Check if shortcodes are enabled + * + * @return bool + */ + public function getProcessShortcodes() { + return $this->processShortcodes; + } - public function __construct($name = null, $options = array()) { - if(is_string($options)) { - $options = array('whitelist' => $options); - } + /** + * Set shortcodes on or off by default + * + * @param bool $process + * @return $this + */ + public function setProcessShortcodes($process) { + $this->processShortcodes = (bool)$process; + return $this; + } - return parent::__construct($name, $options); + /** + * List of html properties to whitelist + * + * @var array + */ + protected $whitelist = []; + + /** + * List of html properties to whitelist + * + * @return array + */ + public function getWhitelist() { + return $this->whitelist; + } + + /** + * Set list of html properties to whitelist + * + * @param array $whitelist + * @return $this + */ + public function setWhitelist($whitelist) { + if(!is_array($whitelist)) { + $whitelist = preg_split('/\s*,\s*/', $whitelist); + } + $this->whitelist = $whitelist; + return $this; } /** @@ -69,24 +119,52 @@ public function __construct($name = null, $options = array()) { * Text nodes outside of HTML tags are filtered out by default, but may be included by adding * the text() directive. E.g. 'link,meta,text()' will allow only and text at * the root level. + * + * @return $this */ public function setOptions(array $options = array()) { - parent::setOptions($options); - if(array_key_exists("shortcodes", $options)) { - $this->processShortcodes = !!$options["shortcodes"]; + $this->setProcessShortcodes(!!$options["shortcodes"]); } if(array_key_exists("whitelist", $options)) { - if(is_array($options['whitelist'])) { - $this->whitelist = $options['whitelist']; - } - else { - $this->whitelist = preg_split('/,\s*/', $options['whitelist']); - } + $this->setWhitelist($options['whitelist']); } + + return parent::setOptions($options); + } + + public function LimitSentences($maxSentences = 2) + { + // @todo + return parent::LimitSentences($maxSentences); + } + + public function LimitWordCount($numWords = 26, $add = '...') + { + // @todo + return parent::LimitWordCount($numWords, $add); + } + + public function LimitCharacters($limit = 20, $add = '...') + { + // @todo + return parent::LimitCharacters($limit, $add); + } + + public function LimitCharactersToClosestWord($limit = 20, $add = '...') + { + // @todo + return parent::LimitCharactersToClosestWord($limit, $add); } + public function BigSummary($maxWords = 50) + { + // @todo + return parent::BigSummary($maxWords); // TODO: Change the autogenerated stub + } + + /** * Create a summary of the content. This will be some section of the first paragraph, limited by * $maxWords. All internal tags are stripped out - the return value is a string @@ -161,6 +239,11 @@ public function Summary($maxWords = 50, $flex = 15, $add = '...') { return implode(' ', array_slice($words, 0, $maxWords)) . $add; } + public function FirstParagraph() { + // @todo implement + return parent::FirstParagraph(); + } + /** * Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or * there isn't one), it returns the same as Summary() @@ -207,6 +290,18 @@ public function forTemplate() { return $this->RAW(); } + /** + * Safely escape for XML string + * + * @return string + */ + public function CDATA() { + return sprintf( + '', + str_replace(']]>', ']]]]>', $this->RAW()) + ); + } + public function prepValueForDB($value) { return parent::prepValueForDB($this->whitelistContent($value)); } @@ -277,6 +372,15 @@ public function scaffoldSearchField($title = null, $params = null) { return new TextField($this->name, $title); } -} - + /** + * @return string + */ + public function NoHTML() + { + // Preserve line breaks + $text = preg_replace('/\/i', "\n", $this->RAW()); + // Convert back to plain text + return \Convert::xml2raw(strip_tags($text)); + } +} diff --git a/ORM/FieldType/DBHTMLVarchar.php b/ORM/FieldType/DBHTMLVarchar.php index f01f357ee2a..c8affc3c196 100644 --- a/ORM/FieldType/DBHTMLVarchar.php +++ b/ORM/FieldType/DBHTMLVarchar.php @@ -17,14 +17,55 @@ class DBHTMLVarchar extends DBVarchar { private static $escape_type = 'xml'; - protected $processShortcodes = true; + /** + * Enable shortcode parsing on this field + * + * @var bool + */ + protected $processShortcodes = false; - public function setOptions(array $options = array()) { - parent::setOptions($options); + /** + * Check if shortcodes are enabled + * + * @return bool + */ + public function getProcessShortcodes() { + return $this->processShortcodes; + } + /** + * Set shortcodes on or off by default + * + * @param bool $process + * @return $this + */ + public function setProcessShortcodes($process) { + $this->processShortcodes = (bool)$process; + return $this; + } + /** + * @param array $options + * + * Options accepted in addition to those provided by Text: + * + * - shortcodes: If true, shortcodes will be turned into the appropriate HTML. + * If false, shortcodes will not be processed. + * + * - whitelist: If provided, a comma-separated list of elements that will be allowed to be stored + * (be careful on relying on this for XSS protection - some seemingly-safe elements allow + * attributes that can be exploited, for instance ) + * Text nodes outside of HTML tags are filtered out by default, but may be included by adding + * the text() directive. E.g. 'link,meta,text()' will allow only and text at + * the root level. + * + * @return $this + */ + public function setOptions(array $options = array()) { if(array_key_exists("shortcodes", $options)) { - $this->processShortcodes = !!$options["shortcodes"]; + $this->setProcessShortcodes(!!$options["shortcodes"]); } + + return parent::setOptions($options); } public function forTemplate() { @@ -34,11 +75,21 @@ public function forTemplate() { public function RAW() { if ($this->processShortcodes) { return ShortcodeParser::get_active()->parse($this->value); - } - else { + } else { return $this->value; } + } + /** + * Safely escape for XML string + * + * @return string + */ + public function CDATA() { + return sprintf( + '', + str_replace(']]>', ']]]]>', $this->forTemplate()) + ); } public function exists() { @@ -53,4 +104,15 @@ public function scaffoldSearchField($title = null) { return new TextField($this->name, $title); } + /** + * @return string + */ + public function NoHTML() + { + // Preserve line breaks + $text = preg_replace('/\/i', "\n", $this->RAW()); + // Convert back to plain text + return \Convert::xml2raw(strip_tags($text)); + } + } diff --git a/ORM/FieldType/DBInt.php b/ORM/FieldType/DBInt.php index 30f43ae67cd..423a1f84905 100644 --- a/ORM/FieldType/DBInt.php +++ b/ORM/FieldType/DBInt.php @@ -29,14 +29,14 @@ public function Formatted() { } public function requireField() { - $parts=Array( - 'datatype'=>'int', - 'precision'=>11, - 'null'=>'not null', - 'default'=>$this->defaultVal, - 'arrayValue'=>$this->arrayValue); - - $values=Array('type'=>'int', 'parts'=>$parts); + $parts = [ + 'datatype' => 'int', + 'precision' => 11, + 'null' => 'not null', + 'default' => $this->defaultVal, + 'arrayValue' => $this->arrayValue + ]; + $values = ['type' => 'int', 'parts' => $parts]; DB::require_field($this->tableName, $this->name, $values); } diff --git a/ORM/FieldType/DBString.php b/ORM/FieldType/DBString.php index a28e1fcfff4..dda71107aac 100644 --- a/ORM/FieldType/DBString.php +++ b/ORM/FieldType/DBString.php @@ -24,7 +24,6 @@ abstract class DBString extends DBField { "LimitCharacters" => "Text", "LimitCharactersToClosestWord" => "Text", 'LimitWordCount' => 'Text', - 'LimitWordCountXML' => 'HTMLText', "LowerCase" => "Text", "UpperCase" => "Text", 'NoHTML' => 'Text', @@ -33,34 +32,70 @@ abstract class DBString extends DBField { /** * Construct a string type field with a set of optional parameters. * - * @param $name string The name of the field - * @param $options array An array of options e.g. array('nullifyEmpty'=>false). See + * @param string $name string The name of the field + * @param array $options array An array of options e.g. array('nullifyEmpty'=>false). See * {@link StringField::setOptions()} for information on the available options */ public function __construct($name = null, $options = array()) { - // Workaround: The singleton pattern calls this constructor with true/1 as the second parameter, so we - // must ignore it - if(is_array($options)){ + $options = $this->parseConstructorOptions($options); + if($options) { $this->setOptions($options); } parent::__construct($name); } + /** + * Parses the "options" parameter passed to the constructor. This could be a + * string value, or an array of options. Config specification might also + * encode "key=value" pairs in non-associative strings. + * + * @param mixed $options + * @return array The list of parsed options, or empty if there are none + */ + protected function parseConstructorOptions($options) { + if(is_string($options)) { + $options = [$options]; + } + if(!is_array($options)) { + return []; + } + $parsed = []; + foreach($options as $option => $value) { + // Workaround for inability for config args to support associative arrays + if(is_numeric($option) && strpos($value, '=') !== false) { + list($option, $value) = explode('=', $value); + $option = trim($option); + $value = trim($value); + } + // Convert bool values + if(strcasecmp($value, 'true') === 0) { + $value = true; + } elseif(strcasecmp($value, 'false') === 0) { + $value = false; + } + $parsed[$option] = $value; + } + return $parsed; + } + /** * Update the optional parameters for this field. - * @param array $options array of options + * + * @param array $options Array of options * The options allowed are: * + * @return $this */ public function setOptions(array $options = array()) { if(array_key_exists("nullifyEmpty", $options)) { $this->nullifyEmpty = $options["nullifyEmpty"] ? true : false; } + return $this; } /** @@ -110,7 +145,7 @@ public function prepValueForDB($value) { * @return string */ public function forTemplate() { - return nl2br($this->XML()); + return nl2br(parent::forTemplate()); } /** @@ -170,9 +205,6 @@ public function LimitCharactersToClosestWord($limit = 20, $add = '...') { /** * Limit this field's content by a number of words. * - * CAUTION: This is not XML safe. Please use - * {@link LimitWordCountXML()} instead. - * * @param int $numWords Number of words to limit by. * @param string $add Ellipsis to add to the end of truncated string. * @@ -192,22 +224,6 @@ public function LimitWordCount($numWords = 26, $add = '...') { return $ret; } - /** - * Limit the number of words of the current field's - * content. This is XML safe, so characters like & - * are converted to & - * - * @param int $numWords Number of words to limit by. - * @param string $add Ellipsis to add to the end of truncated string. - * - * @return string - */ - public function LimitWordCountXML($numWords = 26, $add = '...') { - $ret = $this->LimitWordCount($numWords, $add); - - return Convert::raw2xml($ret); - } - /** * Converts the current value for this StringField to lowercase. * @@ -219,6 +235,7 @@ public function LowerCase() { /** * Converts the current value for this StringField to uppercase. + * * @return string */ public function UpperCase() { @@ -226,11 +243,11 @@ public function UpperCase() { } /** - * Return the value of the field stripped of html tags. + * Plain text version of this string * - * @return string + * @return string Plain text */ public function NoHTML() { - return strip_tags($this->RAW()); + return $this->RAW(); } } diff --git a/ORM/FieldType/DBText.php b/ORM/FieldType/DBText.php index bb79fecdb50..5b50d89fb13 100644 --- a/ORM/FieldType/DBText.php +++ b/ORM/FieldType/DBText.php @@ -2,13 +2,14 @@ namespace SilverStripe\ORM\FieldType; -use HTTP; use Convert; use NullableField; use TextareaField; use TextField; use Config; use SilverStripe\ORM\DB; +use InvalidArgumentException; +use TextParser; /** * Represents a variable-length string of up to 2 megabytes, designed to store raw text @@ -20,8 +21,8 @@ * ); * * - * @see HTMLText - * @see HTMLVarchar + * @see DBHTMLText + * @see DBHTMLVarchar * @see Varchar * * @package framework @@ -30,17 +31,12 @@ class DBText extends DBString { private static $casting = array( - "AbsoluteLinks" => "Text", "BigSummary" => "Text", - "ContextSummary" => "Text", + "ContextSummary" => "HTMLText", // Always returns HTML as it contains formatting and highlighting "FirstParagraph" => "Text", "FirstSentence" => "Text", - "LimitCharacters" => "Text", "LimitSentences" => "Text", "Summary" => "Text", - 'EscapeXML' => 'Text', - 'LimitWordCount' => 'Text', - 'LimitWordCountXML' => 'HTMLText', ); /** @@ -51,242 +47,207 @@ public function requireField() { $charset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'collation'); - $parts = array( + $parts = [ 'datatype' => 'mediumtext', 'character set' => $charset, 'collate' => $collation, + 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue - ); + ]; - $values= array( + $values = [ 'type' => 'text', 'parts' => $parts - ); + ]; DB::require_field($this->tableName, $this->name, $values); } - /** - * Return the value of the field with relative links converted to absolute urls. - * @return string - */ - public function AbsoluteLinks() { - return HTTP::absoluteURLs($this->RAW()); - } - /** * Limit sentences, can be controlled by passing an integer. * - * @param int $sentCount The amount of sentences you want. + * @param int $maxSentences The amount of sentences you want. * @return string */ - public function LimitSentences($sentCount = 2) { - if(!is_numeric($sentCount)) { - user_error("Text::LimitSentence() expects one numeric argument", E_USER_NOTICE); - } - - $output = array(); - $data = trim(Convert::xml2raw($this->RAW())); - $sentences = explode('.', $data); + public function LimitSentences($maxSentences = 2) { + if(!is_numeric($maxSentences)) { + throw new InvalidArgumentException("Text::LimitSentence() expects one numeric argument"); + } - if ($sentCount == 0) return ''; + $value = $this->NoHTML(); + if( !$value ) { + return ""; + } - for($i = 0; $i < $sentCount; $i++) { - if(isset($sentences[$i])) { - $sentence = trim($sentences[$i]); - if(!empty($sentence)) $output[] .= $sentence; + // Do a word-search + $words = preg_split('/\s+/', $value); + $sentences = 0; + foreach ($words as $i => $word) { + if (preg_match('/(!|\?|\.)$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) { + $sentences++; + if($sentences >= $maxSentences) { + return implode(' ', array_slice($words, 0, $i + 1)); + } } } - return count($output)==0 ? '' : implode($output, '. ') . '.'; + // Failing to find the number of sentences requested, fallback to a logical default + if($maxSentences > 1) { + return $value; + } else { + // If searching for a single sentence (and there are none) just do a text summary + return $this->Summary(20); + } } /** - * Caution: Not XML/HTML-safe - does not respect closing tags. + * Return the first string that finishes with a period (.) in this text. + * + * @return string */ public function FirstSentence() { - $paragraph = Convert::xml2raw( $this->RAW() ); - if( !$paragraph ) return ""; + return $this->LimitSentences(1); + } - $words = preg_split('/\s+/', $paragraph); - foreach ($words as $i => $word) { - if (preg_match('/(!|\?|\.)$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) { - return implode(' ', array_slice($words, 0, $i+1)); + /** + * Builds a basic summary, up to a maximum number of words + * + * @param int $maxWords + * @param int $maxParagraphs Optional paragraph limit + * @return string + */ + public function Summary($maxWords = 50, $maxParagraphs = 1) { + // Get plain-text version + $value = $this->NoHTML(); + if(!$value) { + return ''; } - } - /* If we didn't find a sentence ending, use the summary. We re-call rather than using paragraph so that - * Summary will limit the result this time */ - return $this->Summary(20); + // Set max paragraphs + if($maxParagraphs) { + // Split on >2 linebreaks + $paragraphs = preg_split('#\n{2,}#', $value); + if(count($paragraphs) > $maxParagraphs) { + $paragraphs = array_slice($paragraphs, 0, $maxParagraphs); + } + $value = implode("\n\n", $paragraphs); } - /** - * Caution: Not XML/HTML-safe - does not respect closing tags. - */ - public function Summary($maxWords = 50) { - // get first sentence? - // this needs to be more robust - $value = Convert::xml2raw( $this->RAW() /*, true*/ ); - if(!$value) return ''; - - // grab the first paragraph, or, failing that, the whole content - if(strpos($value, "\n\n")) $value = substr($value, 0, strpos($value, "\n\n")); + // Find sentences $sentences = explode('.', $value); - $count = count(explode(' ', $sentences[0])); + $wordCount = count(preg_split('#\s+#', $sentences[0])); // if the first sentence is too long, show only the first $maxWords words - if($count > $maxWords) { + if($wordCount > $maxWords) { return implode( ' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . '...'; } // add each sentence while there are enough words to do so $result = ''; do { - $result .= trim(array_shift( $sentences )).'.'; - if(count($sentences) > 0) { - $count += count(explode(' ', $sentences[0])); - } - - // Ensure that we don't trim half way through a tag or a link - $brokenLink = ( - substr_count($result,'<') != substr_count($result,'>')) || - (substr_count($result,']*>/', $result) && !preg_match( '/<\/a>/', $result)) $result .= ''; + // If more sentences to process, count number of words + if($sentences) { + $wordCount += count(preg_split('#\s+#', $sentences[0])); + } + } while($wordCount < $maxWords && $sentences && trim( $sentences[0])); - return Convert::raw2xml($result); + return trim($result); } /** * Performs the same function as the big summary, but doesn't trim new paragraphs off data. - * Caution: Not XML/HTML-safe - does not respect closing tags. + * + * @param int $maxWords + * @return string */ - public function BigSummary($maxWords = 50, $plain = true) { - $result = ''; - - // get first sentence? - // this needs to be more robust - $data = $plain ? Convert::xml2raw($this->RAW(), true) : $this->RAW(); - - if(!$data) return ''; - - $sentences = explode('.', $data); - $count = count(explode(' ', $sentences[0])); - - // if the first sentence is too long, show only the first $maxWords words - if($count > $maxWords) { - return implode(' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . '...'; + public function BigSummary($maxWords = 50) { + return $this->Summary($maxWords, 0); } - // add each sentence while there are enough words to do so - do { - $result .= trim(array_shift($sentences)); - if($sentences) { - $result .= '. '; - $count += count(explode(' ', $sentences[0])); - } - - // Ensure that we don't trim half way through a tag or a link - $brokenLink = ( - substr_count($result,'<') != substr_count($result,'>')) || - (substr_count($result,']*>/', $result) && !preg_match( '/<\/a>/', $result)) { - $result .= ''; - } - - return $result; - } - /** - * Caution: Not XML/HTML-safe - does not respect closing tags. + * Get first paragraph + * + * @return string */ - public function FirstParagraph($plain = 1) { - // get first sentence? - // this needs to be more robust - $value = $this->RAW(); - if($plain && $plain != 'html') { - $data = Convert::xml2raw($value); - if(!$data) return ""; - - // grab the first paragraph, or, failing that, the whole content - $pos = strpos($data, "\n\n"); - if($pos) $data = substr($data, 0, $pos); - - return $data; - } else { - if(strpos($value, "

") === false) return $value; - - $data = substr($value, 0, strpos($value, "

") + 4); - - if(strlen($data) < 20 && strpos($value, "

", strlen($data))) { - $data = substr($value, 0, strpos( $value, "

", strlen($data)) + 4 ); + public function FirstParagraph() { + $value = $this->NoHTML(); + if(empty($value)) { + return ''; } - return $data; + // Split paragraphs and return first + $paragraphs = preg_split('#\n{2,}#', $value); + return reset($paragraphs); } - } /** * Perform context searching to give some context to searches, optionally * highlighting the search term. * * @param int $characters Number of characters in the summary - * @param boolean $string Supplied string ("keywords") - * @param boolean $striphtml Strip HTML? - * @param boolean $highlight Add a highlight element around search query? - * @param string $prefix text - * @param string $suffix - * - * @return string + * @param string $keywords Supplied string ("keywords"). Will fall back to 'Search' querystring arg. + * @param bool $highlight Add a highlight element around search query? + * @param string $prefix Prefix text + * @param string $suffix Suffix text + * @return string HTML string with context */ - public function ContextSummary($characters = 500, $string = false, $striphtml = true, $highlight = true, - $prefix = "... ", $suffix = "...") { + public function ContextSummary( + $characters = 500, $keywords = null, $highlight = true, $prefix = "... ", $suffix = "..." + ) { - if(!$string) { + if(!$keywords) { // Use the default "Search" request variable (from SearchForm) - $string = isset($_REQUEST['Search']) ? $_REQUEST['Search'] : ''; + $keywords = isset($_REQUEST['Search']) ? $_REQUEST['Search'] : ''; } - // Remove HTML tags so we don't have to deal with matching tags - $text = $striphtml ? $this->NoHTML() : $this->RAW(); + // Get raw text value, but XML encode it (as we'll be merging with HTML tags soon) + $text = nl2br(Convert::raw2xml($this->NoHTML())); + $keywords = Convert::raw2xml($keywords); // Find the search string - $position = (int) stripos($text, $string); + $position = (int) stripos($text, $keywords); // We want to search string to be in the middle of our block to give it some context $position = max(0, $position - ($characters / 2)); if($position > 0) { // We don't want to start mid-word - $position = max((int) strrpos(substr($text, 0, $position), ' '), - (int) strrpos(substr($text, 0, $position), "\n")); + $position = max( + (int) strrpos(substr($text, 0, $position), ' '), + (int) strrpos(substr($text, 0, $position), "\n") + ); } $summary = substr($text, $position, $characters); - $stringPieces = explode(' ', $string); + $stringPieces = explode(' ', $keywords); if($highlight) { // Add a span around all key words from the search term as well if($stringPieces) { - foreach($stringPieces as $stringPiece) { if(strlen($stringPiece) > 2) { - $summary = str_ireplace($stringPiece, "$stringPiece", - $summary); + $summary = str_ireplace( + $stringPiece, + "$stringPiece", + $summary + ); } } } } $summary = trim($summary); - if($position > 0) $summary = $prefix . $summary; - if(strlen($this->RAW()) > ($characters + $position)) $summary = $summary . $suffix; + // Add leading / trailing '...' if trimmed on either end + if($position > 0) { + $summary = $prefix . $summary; + } + if(strlen($this->value) > ($characters + $position)) { + $summary = $summary . $suffix; + } return $summary; } @@ -295,20 +256,18 @@ public function ContextSummary($characters = 500, $string = false, $striphtml = * Allows a sub-class of TextParser to be rendered. * * @see TextParser for implementation details. - * @param string $parser - * @return string + * @param string $parser Class name of parser (Must extend {@see TextParser}) + * @return DBField Parsed value in the appropriate type */ - public function Parse($parser = "TextParser") { - if($parser == "TextParser" || is_subclass_of($parser, "TextParser")) { - $obj = new $parser($this->RAW()); - return $obj->parse(); - } else { - // Fallback to using raw2xml and show a warning - // TODO Don't kill script execution, we can continue without losing complete control of the app - user_error("Couldn't find an appropriate TextParser sub-class to create (Looked for '$parser')." - . "Make sure it sub-classes TextParser and that you've done ?flush=1.", E_USER_WARNING); - return Convert::raw2xml($this->RAW()); + public function Parse($parser) { + $reflection = new \ReflectionClass($parser); + if($reflection->isAbstract() || !$reflection->isSubclassOf('TextParser')) { + throw new InvalidArgumentException("Invalid parser {$parser}"); } + + /** @var TextParser $obj */ + $obj = \Injector::inst()->createWithArgs($parser, [$this->forTemplate()]); + return $obj->parse(); } /** diff --git a/ORM/FieldType/DBVarchar.php b/ORM/FieldType/DBVarchar.php index a0193fe8e19..c0b8d683272 100644 --- a/ORM/FieldType/DBVarchar.php +++ b/ORM/FieldType/DBVarchar.php @@ -10,9 +10,9 @@ /** * Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text * - * @see HTMLText - * @see HTMLVarchar - * @see Text + * @see DBHTMLText + * @see DBHTMLVarchar + * @see DBText * * @package framework * @subpackage orm diff --git a/Security/CMSSecurity.php b/Security/CMSSecurity.php index a58cffc51d1..b4c8223e2a9 100644 --- a/Security/CMSSecurity.php +++ b/Security/CMSSecurity.php @@ -19,7 +19,7 @@ class CMSSecurity extends Security { private static $casting = array( - 'Title' => 'HTMLText' + 'Title' => 'HTMLFragment' ); private static $allowed_actions = array( diff --git a/Security/PermissionCheckboxSetField.php b/Security/PermissionCheckboxSetField.php index 02bff51a1d3..afd3fbeaec4 100644 --- a/Security/PermissionCheckboxSetField.php +++ b/Security/PermissionCheckboxSetField.php @@ -90,7 +90,7 @@ public function getHiddenPermissions() { /** * @param array $properties - * @return DBHTMLText + * @return string */ public function Field($properties = array()) { Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/CheckboxSetField.css'); @@ -248,7 +248,7 @@ public function Field($properties = array()) { } } if($this->readonly) { - return DBField::create_field('HTMLText', + return "
    ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . "
  • " . _t( @@ -258,14 +258,12 @@ public function Field($properties = array()) { ) . "
  • " . $options . - "
\n" - ); + "\n"; } else { - return DBField::create_field('HTMLText', + return "
    ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" . $options . - "
\n" - ); + "\n"; } } diff --git a/_config/model.yml b/_config/model.yml index 65d31457b29..df88c582a93 100644 --- a/_config/model.yml +++ b/_config/model.yml @@ -25,6 +25,10 @@ Injector: class: SilverStripe\ORM\FieldType\DBForeignKey HTMLText: class: SilverStripe\ORM\FieldType\DBHTMLText + properties: + ProcessShortcodes: true + HTMLFragment: + class: SilverStripe\ORM\FieldType\DBHTMLText HTMLVarchar: class: SilverStripe\ORM\FieldType\DBHTMLVarchar Int: diff --git a/api/RSSFeed.php b/api/RSSFeed.php index 4be731064c5..01f838fd059 100644 --- a/api/RSSFeed.php +++ b/api/RSSFeed.php @@ -299,45 +299,41 @@ public function __construct($entry, $titleField, $descriptionField, $authorField /** * Get the description of this entry * - * @return string Returns the description of the entry. + * @return DBField Returns the description of the entry. */ public function Title() { - return $this->rssField($this->titleField, 'Varchar'); + return $this->rssField($this->titleField); } /** * Get the description of this entry * - * @return string Returns the description of the entry. + * @return DBField Returns the description of the entry. */ public function Description() { - return $this->rssField($this->descriptionField, 'HTMLText'); + return $this->rssField($this->descriptionField); } /** * Get the author of this entry * - * @return string Returns the author of the entry. + * @return DBField Returns the author of the entry. */ public function Author() { - if($this->authorField) return $this->failover->obj($this->authorField); + return $this->rssField($this->authorField); } /** - * Return the named field as an obj() call from $this->failover. - * Default to the given class if there's no casting information. + * Return the safely casted field + * + * @param string $fieldName Name of field + * @return DBField */ - public function rssField($fieldName, $defaultClass = 'Varchar') { + public function rssField($fieldName) { if($fieldName) { - if($this->failover->castingHelper($fieldName)) { - $value = $this->failover->$fieldName; - $obj = $this->failover->obj($fieldName); - $obj->setValue($value); - return $obj; - } else { - return DBField::create_field($defaultClass, $this->failover->XML_val($fieldName), $fieldName); - } + return $this->failover->obj($fieldName); } + return null; } /** diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php index 0786b95a15b..b7b98ff1aab 100644 --- a/api/XMLDataFormatter.php +++ b/api/XMLDataFormatter.php @@ -3,6 +3,8 @@ use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\SS_List; +use SilverStripe\ORM\FieldType\DBHTMLText; + /** * @package framework * @subpackage formatters @@ -54,18 +56,21 @@ public function convertDataObjectWithoutHeader(DataObject $obj, $fields = null, $xml = "<$className href=\"$objHref.xml\">\n"; foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) { // Field filtering - if($fields && !in_array($fieldName, $fields)) continue; - $fieldValue = $obj->obj($fieldName)->forTemplate(); - if(!mb_check_encoding($fieldValue,'utf-8')) $fieldValue = "(data is badly encoded)"; + if($fields && !in_array($fieldName, $fields)) { + continue; + } + $fieldObject = $obj->obj($fieldName); + $fieldValue = $fieldObject->forTemplate(); + if(!mb_check_encoding($fieldValue, 'utf-8')) { + $fieldValue = "(data is badly encoded)"; + } if(is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toXML')) { $xml .= $fieldValue->toXML(); } else { - if('HTMLText' == $fieldType) { + if($fieldObject instanceof DBHTMLText) { // Escape HTML values using CDATA $fieldValue = sprintf('', str_replace(']]>', ']]]]>', $fieldValue)); - } else { - $fieldValue = Convert::raw2xml($fieldValue); } $xml .= "<$fieldName>$fieldValue\n"; } diff --git a/core/CustomMethods.php b/core/CustomMethods.php index 96b53293c03..5cbe31dcc55 100644 --- a/core/CustomMethods.php +++ b/core/CustomMethods.php @@ -50,40 +50,41 @@ public function __call($method, $arguments) { $this->defineMethods(); } - // Validate method being invked - $method = strtolower($method); - if(!isset(self::$extra_methods[$class][$method])) { - // Please do not change the exception code number below. - $class = get_class($this); - throw new BadMethodCallException("Object->__call(): the method '$method' does not exist on '$class'", 2175); + $config = $this->getExtraMethodConfig($method); + if(empty($config)) { + throw new BadMethodCallException( + "Object->__call(): the method '$method' does not exist on '$class'" + ); } - $config = self::$extra_methods[$class][$method]; - switch(true) { - case isset($config['property']) : + case isset($config['property']) : { $obj = $config['index'] !== null ? $this->{$config['property']}[$config['index']] : $this->{$config['property']}; - if($obj) { - if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this); + if ($obj) { + if (!empty($config['callSetOwnerFirst'])) { + $obj->setOwner($this); + } $retVal = call_user_func_array(array($obj, $method), $arguments); - if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner(); + if (!empty($config['callSetOwnerFirst'])) { + $obj->clearOwner(); + } return $retVal; } - if(!empty($this->destroyed)) { + if (!empty($this->destroyed)) { throw new BadMethodCallException( "Object->__call(): attempt to call $method on a destroyed $class object" ); } else { throw new BadMethodCallException( "Object->__call(): $class cannot pass control to $config[property]($config[index])." - . ' Perhaps this object was mistakenly destroyed?' + . ' Perhaps this object was mistakenly destroyed?' ); } - + } case isset($config['wrap']) : array_unshift($arguments, $config['method']); return call_user_func_array(array($this, $config['wrap']), $arguments); @@ -107,8 +108,6 @@ public function __call($method, $arguments) { * @uses addMethodsFrom() */ protected function defineMethods() { - $class = get_class($this); - // Define from all registered callbacks foreach($this->extra_method_registers as $callback) { call_user_func($callback); @@ -139,8 +138,21 @@ protected function registerExtraMethodCallback($name, $callback) { * @return bool */ public function hasMethod($method) { + return method_exists($this, $method) || $this->getExtraMethodConfig($method); + } + + /** + * Get meta-data details on a named method + * + * @param array $method + * @return array List of custom method details, if defined for this method + */ + protected function getExtraMethodConfig($method) { $class = get_class($this); - return method_exists($this, $method) || isset(self::$extra_methods[$class][strtolower($method)]); + if(isset(self::$extra_methods[$class][strtolower($method)])) { + return self::$extra_methods[$class][strtolower($method)]; + } + return null; } /** diff --git a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md index 27e74ac5986..9896a5f8347 100644 --- a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md +++ b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md @@ -189,10 +189,7 @@ the database. However, the template engine knows to escape fields without the ` to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see "[security](../security)"), which is important if these fields store user-provided data. -
-You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML -content via `$MyHtmlField.XML`. -
+See the [Template casting](/developer_guides/templates/casting) section for controlling casting in your templates. ## Overloading @@ -220,4 +217,4 @@ database column using `dbObject`. ## API Documentation * [api:DataObject] -* [api:DBField] \ No newline at end of file +* [api:DBField] diff --git a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md index c35db3a63da..8e9a2edb45f 100644 --- a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md +++ b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md @@ -98,10 +98,50 @@ this purpose. There's some exceptions to this rule, see the ["security" guide](../security). -In case you want to explicitly allow un-escaped HTML input, the property can be cast as [api:HTMLText]. The following -example takes the `Content` field in a `SiteTree` class, which is of this type. It forces the content into an explicitly -escaped format. +For every field used in templates, a casting helper will be applied. This will first check for any +`casting` helper on your model specific to that field, and will fall back to the `default_cast` config +in case none are specified. + +By default, `ViewableData.default_cast` is set to `Text`, which will ensure all fields have special +characters HTML escaped by default. + +The most common casting types are: + + * `Text` Which is a plain text string, and will be safely encoded via HTML entities when placed into + a template. + * `Varchar` which is the same as `Text` but for single-line text that should not have line breaks. + * `HTMLFragment` is a block of raw HTML, which should not be escaped. Take care to sanitise any HTML + value saved into the database. + * `HTMLText` is a `HTMLFragment`, but has shortcodes enabled. This should only be used for content + that is modified via a TinyMCE editor, which will insert shortcodes. + * `Int` for integers. + * `Decimal` for floating point values. + * `Boolean` For boolean values. + * `Datetime` for date and time. + +See the [Model data types and casting](/developer_guides/model/data_types_and_casting) section for +instructions on configuring your model to declare casting types for fields. + +## Escape methods in templates + +Within the template, fields can have their encoding customised at a certain level with format methods. +See [api:DBField] for the specific implementation, but they will generally follow the below rules: + +* `$Field` with no format method supplied will correctly cast itself for the HTML template, as defined + by the casting helper for that field. In most cases this is the best method to use for templates. +* `$Field.XML` Will invoke `htmlentities` on special characters in the value, even if it's already + cast as HTML. +* `$Field.ATT` will ensure the field is XML encoded for placement inside a HTML element property. + This will invoke `htmlentities` on the value (even if already cast as HTML) and will escape quotes. +* `Field.JS` will cast this value as a javascript string. E.g. `var fieldVal = '$Field.JS';` can + be used in javascript defined in templates to encode values safely. +* `$Field.CDATA` will cast this value safely for insertion as a literal string in an XML file. + E.g. `$Field.CDATA` will ensure that the `` body is safely escaped + as a string. + +
+Note: Take care when using `.XML` on `HTMLText` fields, as this will result in double-encoded +html. To ensure that the correct encoding is used for that field in a template, simply use +`$Field` by itself to allow the casting helper to determine the best encoding itself. +
- :::ss - $Content.XML - // transforms e.g. "alert" to "<em>alert</em>" diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 49427c1348b..abcee089382 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -45,6 +45,11 @@ * `DataObject::can` has new method signature with `$context` parameter. * `SiteTree.alternatePreviewLink` is deprecated. Use `updatePreviewLink` instead. * `Injector` dependencies no longer automatically inherit from parent classes. + * `default_cast` is now enforced on all template variables. See upgrading notes below. + * `HTMLText` no longer enables shortcodes by default. You can specify the `db` option for + html fields as `HTMLText(['whitelist=meta,link'])`, or use a `ShortcodeHTMLText` as + a shorthand substitute. + * `FormField->dontEscape` has been removed. Escaping is now managed on a class by class basis. ## New API @@ -99,6 +104,7 @@ * `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions * `DataObject.table_name` config can now be used to customise the database table for any record. * `DataObjectSchema` class added to assist with mapping between classes and tables. + * `FormField::Title` and `FormField::RightTitle` are now cast as plain text by default (but can be overridden). ### Front-end build tooling for CMS interface @@ -168,7 +174,7 @@ admin/font/ => admin/client/dist/font/ * History.js * `debugmethods` querystring argument has been removed from debugging. - + * The following ClassInfo methods are now deprecated: * `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead. * `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead @@ -302,6 +308,64 @@ E.g. ## Upgrading + +### Explicit text casting is now enforced on all template variables + +Now whenever a `$Variable` is used in a template, regardless of whether any casts or methods are +suffixed to the reference, it will be cast to either an explicit DBField for that field, or +the value declared by the `default_cast` on the parent object. + +The default value of `default_cast` is `Text`, meaning that now many cases where a field was +left un-uncoded, this will now be safely encoded via `Convert::raw2xml`. In cases where +un-cast fields were used to place raw HTML into templates, this will now encode this until +explicitly cast for that field. + +You can resolve this in your model by adding an explicit cast to HTML for those fields. + + +Before: + + + :::ss +
+ $SomeHTML +
+ + + :::php + class MyObject extends ViewableData { + public function getSomeHTML { + $title = Convert::raw2xml($this->Title); + return "

{$title}

"; + } + } + + +After: + + + :::ss +
+ $SomeHTML +
+ + + :::php + class MyObject extends ViewableData { + private static $casting = [ + 'SomeHTML' => 'HTMLText' + ]; + + public function getSomeHTML { + $title = Convert::raw2xml($this->Title); + return "

{$title}

"; + } + } + +If you need to encode a field (such as HTMLText) for use in html attributes, use `.ATT` +instead, or if used in an actual XML file use `.CDATA`. + +See the [Template casting](/developer_guides/templates/casting) section for specific details. ### Automatically upgrading diff --git a/filesystem/File.php b/filesystem/File.php index ffd3739d1ab..249b65f8783 100644 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -116,7 +116,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb ); private static $casting = array ( - 'TreeTitle' => 'HTMLText' + 'TreeTitle' => 'HTMLFragment' ); /** @@ -458,12 +458,11 @@ public function getCMSFields() { CompositeField::create( new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'), new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()), - ReadonlyField::create( + HTMLReadonlyField::create( 'ClickableURL', _t('AssetTableField.URL','URL'), sprintf('%s', $this->Link(), $this->Link()) - ) - ->setDontEscape(true), + ), new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'), new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':') ) @@ -632,7 +631,10 @@ public function updateFilesystem() { public function collateDescendants($condition, &$collator) { if($children = $this->Children()) { foreach($children as $item) { - if(!$condition || eval("return $condition;")) $collator[] = $item; + /** @var File $item */ + if(!$condition || eval("return $condition;")) { + $collator[] = $item; + } $item->collateDescendants($condition, $collator); } return true; diff --git a/filesystem/ImageManipulation.php b/filesystem/ImageManipulation.php index b08f2df5e09..76196db219e 100644 --- a/filesystem/ImageManipulation.php +++ b/filesystem/ImageManipulation.php @@ -517,7 +517,7 @@ public function ThumbnailIcon($width, $height) { */ public function IconTag() { return DBField::create_field( - 'HTMLText', + 'HTMLFragment', '' ); } diff --git a/filesystem/storage/DBFile.php b/filesystem/storage/DBFile.php index 90905cd3666..80dfc3410e6 100644 --- a/filesystem/storage/DBFile.php +++ b/filesystem/storage/DBFile.php @@ -100,7 +100,7 @@ protected function getStore() { 'Title' => 'Varchar', 'MimeType' => 'Varchar', 'String' => 'Text', - 'Tag' => 'HTMLText', + 'Tag' => 'HTMLFragment', 'Size' => 'Varchar' ); @@ -113,7 +113,7 @@ public function scaffoldFormField($title = null, $params = null) { * * @return string */ - public function forTemplate() { + public function XML() { return $this->getTag() ?: ''; } diff --git a/forms/CheckboxField.php b/forms/CheckboxField.php index b2a2895ac39..e47b96aa79f 100644 --- a/forms/CheckboxField.php +++ b/forms/CheckboxField.php @@ -58,9 +58,13 @@ public function performReadonlyTransformation() { } public function Value() { - return Convert::raw2xml($this->value ? + return $this->value ? _t('CheckboxField.YESANSWER', 'Yes') : - _t('CheckboxField.NOANSWER', 'No')); + _t('CheckboxField.NOANSWER', 'No'); + } + + public function getValueCast() { + return 'Text'; } } diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index a5fe79dde35..f10562e4a75 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -144,7 +144,7 @@ public function __construct($name, $title = null, $value = "", $form = null, $sh /** * @param array $properties * - * @return DBHTMLText + * @return string */ public function Field($properties = array()) { Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); diff --git a/forms/CurrencyField.php b/forms/CurrencyField.php index 7580665058c..35e2125867f 100644 --- a/forms/CurrencyField.php +++ b/forms/CurrencyField.php @@ -69,13 +69,14 @@ class CurrencyField_Readonly extends ReadonlyField{ */ public function Field($properties = array()) { if($this->value){ - $val = $this->dontEscape ? $this->value : Convert::raw2xml($this->value); + $val = Convert::raw2xml($this->value); $val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/',"",$val), 2); + $valforInput = Convert::raw2att($val); } else { $val = ''._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00'; + $valforInput = ''; } - $valforInput = $this->value ? Convert::raw2att($val) : ""; - return "extraClass()."\" id=\"" . $this->id() . "\">$val" + return "extraClass()."\" id=\"" . $this->ID() . "\">$val" . "name."\" value=\"".$valforInput."\" />"; } @@ -102,12 +103,12 @@ class CurrencyField_Disabled extends CurrencyField{ */ public function Field($properties = array()) { if($this->value){ - $val = $this->dontEscape ? $this->value : Convert::raw2xml($this->value); + $val = Convert::raw2xml($this->value); $val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/',"",$val), 2); + $valforInput = Convert::raw2att($val); } else { - $val = ''._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00'; + $valforInput = ''; } - $valforInput = $this->value ? Convert::raw2att($val) : ""; return "name."\" value=\"".$valforInput."\" />"; } diff --git a/forms/DatalessField.php b/forms/DatalessField.php index 97c0f07b944..9081d9127b4 100644 --- a/forms/DatalessField.php +++ b/forms/DatalessField.php @@ -32,7 +32,7 @@ public function getAttributes() { * Returns the field's representation in the form. * For dataless fields, this defaults to $Field. * - * @return HTMLText + * @return string */ public function FieldHolder($properties = array()) { return $this->Field($properties); @@ -57,6 +57,7 @@ public function performReadonlyTransformation() { /** * @param bool $bool + * @return $this */ public function setAllowHTML($bool) { $this->allowHTML = $bool; diff --git a/forms/DatetimeField.php b/forms/DatetimeField.php index dca87b4eed7..96cbfc43e40 100644 --- a/forms/DatetimeField.php +++ b/forms/DatetimeField.php @@ -97,7 +97,7 @@ public function setName($name) { /** * @param array $properties - * @return HTMLText + * @return string */ public function FieldHolder($properties = array()) { $config = array( @@ -112,16 +112,17 @@ public function FieldHolder($properties = array()) { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/DatetimeField.css'); $tzField = ($this->getConfig('usertimezone')) ? $this->timezoneField->FieldHolder() : ''; - return DBField::create_field('HTMLText', $this->dateField->FieldHolder() . - $this->timeField->FieldHolder() . - $tzField . - '
' + return sprintf( + '%s%s%s
', + $this->dateField->FieldHolder(), + $this->timeField->FieldHolder(), + $tzField ); } @@ -139,6 +140,7 @@ public function Field($properties = array()) { * @param string|array $val String expects an ISO date format. Array notation with 'date' and 'time' * keys can contain localized strings. If the 'dmyfields' option is used for {@link DateField}, * the 'date' value may contain array notation was well (see {@link DateField->setValue()}). + * @return $this */ public function setValue($val) { $locale = new Zend_Locale($this->locale); diff --git a/forms/DropdownField.php b/forms/DropdownField.php index 98eb92afc1f..d1b440f483e 100644 --- a/forms/DropdownField.php +++ b/forms/DropdownField.php @@ -114,7 +114,7 @@ protected function getFieldOption($value, $title) { /** * @param array $properties - * @return DBHTMLText + * @return string */ public function Field($properties = array()) { $options = array(); diff --git a/forms/FileField.php b/forms/FileField.php index c9e637405d9..975f5a7f0ae 100644 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -87,7 +87,7 @@ public function __construct($name, $title = null, $value = null) { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { $properties = array_merge($properties, array( diff --git a/forms/Form.php b/forms/Form.php index 56052dd341c..92a0e6e1d4b 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -5,6 +5,7 @@ use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\SS_List; +use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\SecurityToken; use SilverStripe\Security\NullSecurityToken; @@ -215,6 +216,15 @@ class Form extends RequestHandler { 'forTemplate', ); + private static $casting = array( + 'AttributesHTML' => 'HTMLFragment', + 'FormAttributes' => 'HTMLFragment', + 'MessageType' => 'Text', + 'Message' => 'HTMLFragment', + 'FormName' => 'Text', + 'Legend' => 'HTMLFragment', + ); + /** * @var FormTemplateHelper */ @@ -1732,7 +1742,7 @@ public function buttonClicked() { public function defaultAction() { if($this->hasDefaultAction && $this->actions) { return $this->actions->First(); - } + } } /** @@ -1804,7 +1814,7 @@ public function getSecurityToken() { public static function single_field_required() { if(self::current_action() == 'callfieldmethod') { return $_REQUEST['fieldName']; - } + } } /** diff --git a/forms/FormAction.php b/forms/FormAction.php index 091f1c438c1..3aabbb9edd1 100644 --- a/forms/FormAction.php +++ b/forms/FormAction.php @@ -17,6 +17,14 @@ */ class FormAction extends FormField { + /** + * @config + * @var array + */ + private static $casting = [ + 'ButtonContent' => 'HTMLFragment', + ]; + /** * Action name, normally prefixed with 'action_' * @@ -83,7 +91,7 @@ public function setFullAction($fullAction) { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { $properties = array_merge( @@ -100,7 +108,7 @@ public function Field($properties = array()) { /** * @param array $properties - * @return HTMLText + * @return string */ public function FieldHolder($properties = array()) { return $this->Field($properties); @@ -110,22 +118,6 @@ public function Type() { return 'action'; } - public function Title() { - $title = parent::Title(); - - // Remove this method override in 4.0 - $decoded = Convert::xml2raw($title); - if($title && $decoded !== $title) { - Deprecation::notice( - '4.0', - 'The FormAction title field should not be html encoded. Use buttonContent to set custom html instead' - ); - return $decoded; - } - - return $title; - } - public function getAttributes() { $type = (isset($this->attributes['src'])) ? 'image' : 'submit'; @@ -141,7 +133,7 @@ public function getAttributes() { } /** - * Add content inside a button field. + * Add content inside a button field. This should be pre-escaped raw HTML and should be used sparingly. * * @param string $content * @return $this @@ -152,7 +144,7 @@ public function setButtonContent($content) { } /** - * Gets the content inside the button field + * Gets the content inside the button field. This is raw HTML, and should be used sparingly. * * @return string */ diff --git a/forms/FormField.php b/forms/FormField.php index 6b8609202a6..614c9a61f71 100644 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -131,12 +131,6 @@ class FormField extends RequestHandler { */ private static $default_classes = []; - - /** - * @var bool - */ - public $dontEscape; - /** * Right-aligned, contextual label for the field. * @@ -258,6 +252,22 @@ class FormField extends RequestHandler { */ protected $schemaData = []; + private static $casting = array( + 'FieldHolder' => 'HTMLFragment', + 'Field' => 'HTMLFragment', + 'AttributesHTML' => 'HTMLFragment', + 'Value' => 'Text', + 'extraClass' => 'Text', + 'ID' => 'Text', + 'isReadOnly' => 'Boolean', + 'HolderID' => 'Text', + 'Title' => 'Text', + 'RightTitle' => 'Text', + 'MessageType' => 'Text', + 'Message' => 'HTMLFragment', + 'Description' => 'HTMLFragment', + ); + /** * Structured schema state representing the FormField's current data and validation. * Used to render the FormField as a ReactJS Component on the front-end. @@ -483,13 +493,14 @@ public function Title() { } /** - * @param string $title + * Set the title of this formfield. + * Note: This expects escaped HTML. * + * @param string $title Escaped HTML for title * @return $this */ public function setTitle($title) { $this->title = $title; - return $this; } @@ -504,13 +515,14 @@ public function RightTitle() { } /** - * @param string $rightTitle + * Sets the right title for this formfield + * Note: This expects escaped HTML. * + * @param string $rightTitle Escaped HTML for title * @return $this */ public function setRightTitle($rightTitle) { $this->rightTitle = $rightTitle; - return $this; } @@ -928,7 +940,6 @@ public function setSmallFieldHolderTemplate($smallFieldHolderTemplate) { * such as an input tag. * * @param array $properties - * * @return string */ public function Field($properties = array()) { @@ -1366,31 +1377,9 @@ public function castedCopy($classOrCopy) { $field->setAttribute($attributeKey, $attributeValue); } - $field->dontEscape = $this->dontEscape; - return $field; } - /** - * Determine if escaping of this field should be disabled - * - * @param bool $dontEscape - * @return $this - */ - public function setDontEscape($dontEscape) { - $this->dontEscape = $dontEscape; - return $this; - } - - /** - * Determine if escaping is disabled - * - * @return bool - */ - public function getDontEscape() { - return $this->dontEscape; - } - /** * Sets the component type the FormField will be rendered as on the front-end. * diff --git a/forms/HTMLReadonlyField.php b/forms/HTMLReadonlyField.php new file mode 100644 index 00000000000..34f077caf64 --- /dev/null +++ b/forms/HTMLReadonlyField.php @@ -0,0 +1,12 @@ + 'HTMLFragment' + ]; +} diff --git a/forms/HiddenField.php b/forms/HiddenField.php index d03f76b18d2..36dc4e764a3 100644 --- a/forms/HiddenField.php +++ b/forms/HiddenField.php @@ -12,8 +12,7 @@ class HiddenField extends FormField { /** * @param array $properties - * - * @return HTMLText + * @return string */ public function FieldHolder($properties = array()) { return $this->Field($properties); diff --git a/forms/InlineFormAction.php b/forms/InlineFormAction.php index 19b49109e97..abffa01b4b1 100644 --- a/forms/InlineFormAction.php +++ b/forms/InlineFormAction.php @@ -1,9 +1,5 @@ extraClass = ' '.$extraClass; @@ -34,24 +31,23 @@ public function performReadonlyTransformation() { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { if($this->includeDefaultJS) { - Requirements::javascriptTemplate(FRAMEWORK_DIR . '/client/dist/js/InlineFormAction.js', - array('ID'=>$this->id())); + Requirements::javascriptTemplate( + FRAMEWORK_DIR . '/client/dist/js/InlineFormAction.js', + array('ID'=>$this->ID()) + ); } - return DBField::create_field( - 'HTMLText', - FormField::create_tag('input', array( + return FormField::create_tag('input', array( 'type' => 'submit', 'name' => sprintf('action_%s', $this->getName()), 'value' => $this->title, 'id' => $this->ID(), 'class' => sprintf('action%s', $this->extraClass), - )) - ); + )); } public function Title() { @@ -80,19 +76,17 @@ class InlineFormAction_ReadOnly extends FormField { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { - return DBField::create_field('HTMLText', - FormField::create_tag('input', array( + return FormField::create_tag('input', array( 'type' => 'submit', 'name' => sprintf('action_%s', $this->name), 'value' => $this->title, - 'id' => $this->id(), + 'id' => $this->ID(), 'disabled' => 'disabled', 'class' => 'action disabled ' . $this->extraClass, - )) - ); + )); } public function Title() { diff --git a/forms/LiteralField.php b/forms/LiteralField.php index c2b576329c8..7c6dc8453cf 100644 --- a/forms/LiteralField.php +++ b/forms/LiteralField.php @@ -14,6 +14,11 @@ * @subpackage fields-dataless */ class LiteralField extends DatalessField { + + private static $casting = [ + 'Value' => 'HTMLFragment', + ]; + /** * @var string|FormField */ diff --git a/forms/LookupField.php b/forms/LookupField.php index d6d15c94d1f..3aeebb3b324 100644 --- a/forms/LookupField.php +++ b/forms/LookupField.php @@ -1,4 +1,5 @@ dontEscape) { - $attrValue = Convert::raw2xml($attrValue); - } - + $attrValue = Convert::raw2xml($attrValue); $inputValue = implode(', ', array_values($values)); } else { $attrValue = '('._t('FormField.NONE', 'none').')'; @@ -58,7 +56,7 @@ public function Field($properties = array()) { } $properties = array_merge($properties, array( - 'AttrValue' => $attrValue, + 'AttrValue' => DBField::create_field('HTMLFragment', $attrValue), 'InputValue' => $inputValue )); diff --git a/forms/MoneyField.php b/forms/MoneyField.php index 64362be65bf..7c89c9b0e43 100644 --- a/forms/MoneyField.php +++ b/forms/MoneyField.php @@ -52,15 +52,14 @@ public function __construct($name, $title = null, $value = "") { /** * @param array - * @return HTMLText + * @return string */ public function Field($properties = array()) { - return DBField::create_field('HTMLText', + return "
" . "
" . $this->fieldCurrency->SmallFieldHolder() . "
" . "
" . $this->fieldAmount->SmallFieldHolder() . "
" . - "
" - ); + ""; } /** diff --git a/forms/NullableField.php b/forms/NullableField.php index 9e403a7f94b..e48a9a51c3c 100644 --- a/forms/NullableField.php +++ b/forms/NullableField.php @@ -106,7 +106,7 @@ public function getIsNullId() { /** * @param array $properties * - * @return HTMLText + * @return string */ public function Field($properties = array()) { if($this->isReadonly()) { @@ -117,12 +117,12 @@ public function Field($properties = array()) { $nullableCheckbox->setValue(is_null($this->dataValue())); - return DBField::create_field('HTMLText', sprintf( + return sprintf( '%s %s %s', $this->valueField->Field(), $nullableCheckbox->Field(), $this->getIsNullLabel() - )); + ); } /** diff --git a/forms/NumericField.php b/forms/NumericField.php index 8d5a4a9f0a7..0a31a15e5b0 100644 --- a/forms/NumericField.php +++ b/forms/NumericField.php @@ -199,10 +199,10 @@ public function performReadonlyTransformation() { * @return string */ public function Value() { - if($this->value) { - return Convert::raw2xml((string) $this->value); - } + return $this->value ?: '0'; + } - return '0'; + public function getValueCast() { + return 'Decimal'; } } diff --git a/forms/PhoneNumberField.php b/forms/PhoneNumberField.php index 563ceec1041..1040cc90702 100644 --- a/forms/PhoneNumberField.php +++ b/forms/PhoneNumberField.php @@ -31,7 +31,7 @@ public function __construct($name, $title = null, $value = '', $extension = null /** * @param array $properties - * @return FieldGroup|HTMLText + * @return string */ public function Field($properties = array()) { $fields = new FieldGroup( $this->name ); @@ -60,14 +60,17 @@ public function Field($properties = array()) { } $description = $this->getDescription(); - if($description) $fields->getChildren()->First()->setDescription($description); + if($description) { + $fields->getChildren()->first()->setDescription($description); + } foreach($fields as $field) { + /** @var FormField $field */ $field->setDisabled($this->isDisabled()); $field->setReadonly($this->isReadonly()); } - return $fields; + return $fields->Field($properties); } public function setValue( $value ) { @@ -150,8 +153,7 @@ public function validate($validator){ $validator->validationError( $this->name, _t('PhoneNumberField.VALIDATION', "Please enter a valid phone number"), - "validation", - false + "validation" ); return false; } diff --git a/forms/ReadonlyField.php b/forms/ReadonlyField.php index a75f90dcb6d..f9987337dfd 100644 --- a/forms/ReadonlyField.php +++ b/forms/ReadonlyField.php @@ -40,7 +40,7 @@ public function performReadonlyTransformation() { /** * @param array $properties - * @return HTMLText + * @return string */ public function Field($properties = array()) { // Include a hidden field in the HTML @@ -54,6 +54,31 @@ public function Field($properties = array()) { } } + public function getAttributes() { + return array_merge( + parent::getAttributes(), + array( + 'type' => 'hidden', + 'value' => $this->readonly ? null : $this->value, + ) + ); + } + + public function Type() { + return 'readonly'; + } + + public function castingHelper($field) { + // Get dynamic cast for 'Value' field + if(strcasecmp($field, 'Value') === 0) { + return $this->getValueCast(); + } + + // Fall back to default casting + return parent::castingHelper($field); + } + + /** * If $dontEscape is true the returned value will be plain text * and should be escaped in templates via .XML @@ -64,34 +89,31 @@ public function Field($properties = array()) { * @return mixed|string */ public function Value() { - if($this->value) { - if($this->dontEscape) { - return $this->value; - } else { - return Convert::raw2xml($this->value); - } - } else { - $value = '(' . _t('FormField.NONE', 'none') . ')'; - if($this->dontEscape) { - return $value; - } else { - return ''.Convert::raw2xml($value).''; - } + // Get raw value + $value = $this->dataValue(); + if($value) { + return $value; } - } - public function getAttributes() { - return array_merge( - parent::getAttributes(), - array( - 'type' => 'hidden', - 'value' => $this->readonly ? null : $this->value, - ) - ); + // "none" text + $label = _t('FormField.NONE', 'none'); + return "('{$label}')"; } - public function Type() { - return 'readonly'; + /** + * Get custom cating helper for Value() field + * + * @return string + */ + public function getValueCast() { + // Casting class for 'none' text + $value = $this->dataValue(); + if(empty($value)) { + return 'HTMLFragment'; + } + + // Use default casting + return $this->config()->casting['Value']; } } diff --git a/forms/SelectionGroup.php b/forms/SelectionGroup.php index cfcb7f93196..fa424cd871d 100644 --- a/forms/SelectionGroup.php +++ b/forms/SelectionGroup.php @@ -1,6 +1,8 @@ ID() . '_' . (++$count); $extra = array( - "RadioButton" => FormField::create_tag( + "RadioButton" => DBField::create_field('HTMLFragment', FormField::create_tag( 'input', array( 'class' => 'selector', @@ -93,12 +95,12 @@ public function FieldList() { 'value' => $item->getValue(), 'checked' => $checked ) - ), - "RadioLabel" => FormField::create_tag( + )), + "RadioLabel" => DBField::create_field('HTMLFragment', FormField::create_tag( 'label', array('for' => $itemID), $item->getTitle() - ), + )), "Selected" => $firstSelected, ); $newItems[] = $item->customise($extra); diff --git a/forms/TextareaField.php b/forms/TextareaField.php index 9110b3d014d..3d8f4710984 100644 --- a/forms/TextareaField.php +++ b/forms/TextareaField.php @@ -26,7 +26,7 @@ class TextareaField extends FormField { */ private static $casting = array( 'Value' => 'Text', - 'ValueEntities' => 'HTMLText', + 'ValueEntities' => 'HTMLFragment', ); /** @@ -119,8 +119,6 @@ public function Type() { /** * Return value with all values encoded in html entities * - * Invoke with $ValueEntities.RAW to suppress HTMLText parsing shortcodes. - * * @return string Raw HTML */ public function ValueEntities() { diff --git a/forms/ToggleCompositeField.php b/forms/ToggleCompositeField.php index a4d626ab611..909272a39dd 100644 --- a/forms/ToggleCompositeField.php +++ b/forms/ToggleCompositeField.php @@ -35,8 +35,7 @@ public function __construct($name, $title, $children) { * @inheritdoc * * @param array $properties - * - * @return string|HTMLText + * @return string */ public function FieldHolder($properties = array()) { Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 673d3c6c62e..7aeef45feb4 100644 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -216,7 +216,7 @@ public function setNumChildrenMethod($method) { /** * @param array $properties - * @return DBHTMLText + * @return string */ public function Field($properties = array()) { Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang'); @@ -398,8 +398,9 @@ public function tree(SS_HTTPRequest $request) { * Marking public function for the tree, which combines different filters sensibly. * If a filter function has been set, that will be called. And if search text is set, * filter on that too. Return true if all applicable conditions are true, false otherwise. - * @param object $node - * @return mixed + * + * @param mixed $node + * @return bool */ public function filterMarking($node) { if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false; diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php index 384f1f55605..a4b87f2645e 100644 --- a/forms/gridfield/GridField.php +++ b/forms/gridfield/GridField.php @@ -5,7 +5,7 @@ use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\DataModel; use SilverStripe\ORM\DataObjectInterface; - +use SilverStripe\ORM\FieldType\DBHTMLText; /** * Displays a {@link SS_List} in a grid format. @@ -290,8 +290,7 @@ public function getState($getData = true) { * Returns the whole gridfield rendered with all the attached components. * * @param array $properties - * - * @return HTMLText + * @return string */ public function FieldHolder($properties = array()) { Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); @@ -512,14 +511,11 @@ public function FieldHolder($properties = array()) { $header . "\n" . $footer . "\n" . $body ); - $field = DBField::create_field('HTMLText', FormField::create_tag( + return FormField::create_tag( 'fieldset', $fieldsetAttributes, $content['before'] . $table . $content['after'] - )); - $field->setOptions(array('shortcodes' => false)); - - return $field; + ); } /** @@ -604,8 +600,7 @@ protected function newRowClasses($total, $index, $record) { /** * @param array $properties - * - * @return HTMLText + * @return string */ public function Field($properties = array()) { $this->extend('onBeforeRender', $this); diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index 16832a020d8..8f16724e98d 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -1,10 +1,11 @@ getToplevelController(); diff --git a/forms/htmleditor/HTMLEditorField.php b/forms/htmleditor/HTMLEditorField.php index b913a64e55f..ef72a0299eb 100644 --- a/forms/htmleditor/HTMLEditorField.php +++ b/forms/htmleditor/HTMLEditorField.php @@ -14,6 +14,10 @@ */ class HTMLEditorField extends TextareaField { + private static $casting = [ + 'Value' => 'HTMLText', + ]; + /** * Use TinyMCE's GZIP compressor * @@ -128,10 +132,7 @@ public function setValue($value) { * @return HTMLEditorField_Readonly */ public function performReadonlyTransformation() { - $field = $this->castedCopy('HTMLEditorField_Readonly'); - $field->dontEscape = true; - - return $field; + return $this->castedCopy('HTMLEditorField_Readonly'); } public function performDisabledTransformation() { @@ -150,7 +151,11 @@ public function Field($properties = array()) { * @package forms * @subpackage fields-formattedinput */ -class HTMLEditorField_Readonly extends ReadonlyField { +class HTMLEditorField_Readonly extends HTMLReadonlyField { + private static $casting = [ + 'Value' => 'HTMLText' + ]; + public function Field($properties = array()) { $valforInput = $this->value ? Convert::raw2att($this->value) : ""; return "id() . "\">" diff --git a/parsers/BBCodeParser.php b/parsers/BBCodeParser.php index c59360c7076..03c6f47b1bb 100644 --- a/parsers/BBCodeParser.php +++ b/parsers/BBCodeParser.php @@ -1,6 +1,8 @@ smilies_location; - } - - /** - * @deprecated 4.0 Use the "BBCodeParser.smilies_location" config setting instead - */ - public static function set_icon_folder($path) { - Deprecation::notice('4.0', 'Use the "BBCodeParser.smilies_location" config setting instead'); - static::config()->smilies_location = $path; - } - - /** - * @deprecated 4.0 Use the "BBCodeParser.autolink_urls" config setting instead - */ - public static function autolinkUrls() { - Deprecation::notice('4.0', 'Use the "BBCodeParser.autolink_urls" config setting instead'); - return static::config()->autolink_urls; - } - - /** - * @deprecated 4.0 Use the "BBCodeParser.autolink_urls" config setting instead - */ - public static function disable_autolink_urls($autolink = false) { - Deprecation::notice('4.0', 'Use the "BBCodeParser.autolink_urls" config setting instead'); - static::config()->autolink_urls = $autolink; - } - - /** - * @deprecated 4.0 Use the "BBCodeParser.allow_smilies" config setting instead - */ - public static function smiliesAllowed() { - Deprecation::notice('4.0', 'Use the "BBCodeParser.allow_smilies" config setting instead'); - return static::config()->allow_smilies; - } - - /** - * @deprecated 4.0 Use the "BBCodeParser.allow_smilies" config setting instead - */ - public static function enable_smilies() { - Deprecation::notice('4.0', 'Use the "BBCodeParser.allow_smilies" config setting instead'); - static::config()->allow_similies = true; - } - - public static function usable_tags() { return new ArrayList( array( @@ -167,7 +117,7 @@ public function useable_tagsHTML(){ * Main BBCode parser method. This takes plain jane content and * runs it through so many filters * - * @return Text + * @return DBField */ public function parse() { $this->content = str_replace(array('&', '<', '>'), array('&', '<', '>'), $this->content); @@ -197,7 +147,9 @@ public function parse() { ); $this->content = preg_replace(array_keys($smilies), array_values($smilies), $this->content); } - return $this->content; + + // Ensure to return cast value + return DBField::create_field('HTMLFragment', $this->content); } } diff --git a/parsers/TextParser.php b/parsers/TextParser.php index e09e1ebb033..97ed2d68510 100644 --- a/parsers/TextParser.php +++ b/parsers/TextParser.php @@ -26,6 +26,10 @@ * @subpackage misc */ abstract class TextParser extends Object { + + /** + * @var string + */ protected $content; /** @@ -34,12 +38,15 @@ abstract class TextParser extends Object { * @param string $content The contents of the dbfield */ public function __construct($content = "") { + parent::__construct(); $this->content = $content; parent::__construct(); } /** * Convenience method, shouldn't really be used, but it's here if you want it + * + * @param string $content */ public function setContent($content = "") { $this->content = $content; @@ -48,6 +55,8 @@ public function setContent($content = "") { /** * Define your own parse method to parse $this->content appropriately. * See the class doc-block for more implementation details. + * + * @return DBField */ abstract public function parse(); } diff --git a/templates/RSSFeed.ss b/templates/RSSFeed.ss index f4bc1ff971a..4975bbe9d2d 100644 --- a/templates/RSSFeed.ss +++ b/templates/RSSFeed.ss @@ -9,8 +9,8 @@ <% loop $Entries %> $Title.XML - $AbsoluteLink - <% if $Description %>$Description.AbsoluteLinks.XML<% end_if %> + $AbsoluteLink.XML + <% if $Description %>$Description.AbsoluteLinks.CDATA<% end_if %> <% if $Date %>$Date.Rfc822 <% else %>$Created.Rfc822<% end_if %> <% if $Author %>$Author.XML<% end_if %> diff --git a/tests/api/RSSFeedTest.php b/tests/api/RSSFeedTest.php index 70bcbdef680..2e593659f5e 100644 --- a/tests/api/RSSFeedTest.php +++ b/tests/api/RSSFeedTest.php @@ -64,7 +64,7 @@ public function testRSSFeedWithShortcode() { $this->assertContains('ItemD', $content); $this->assertContains( - '<p>ItemD Content test shortcode output</p>', + 'ItemD Content test shortcode output

]]>
', $content ); } @@ -168,7 +168,7 @@ class RSSFeedTest_ItemD extends ViewableData { // ItemD test fields - all fields use casting but Content & AltContent cast as HTMLText private static $casting = array( 'Title' => 'Varchar', - 'Content' => 'HTMLText' + 'Content' => 'HTMLText', // Supports shortcodes ); public $Title = 'ItemD'; diff --git a/tests/email/EmailTest.php b/tests/email/EmailTest.php index 4e9dfaf3cac..ad44e1a6e25 100644 --- a/tests/email/EmailTest.php +++ b/tests/email/EmailTest.php @@ -165,7 +165,7 @@ public function testSendHTML() { 'from@example.com', 'to@example.com', 'Test send plain', - 'Testing Email->sendPlain()', + 'Testing Email->send()', null, 'cc@example.com', 'bcc@example.com' @@ -180,7 +180,7 @@ public function testSendHTML() { $this->assertEquals('to@example.com', $sent['to']); $this->assertEquals('from@example.com', $sent['from']); $this->assertEquals('Test send plain', $sent['subject']); - $this->assertContains('Testing Email->sendPlain()', $sent['content']); + $this->assertContains('Testing Email->send()', $sent['content']); $this->assertNull($sent['plaincontent']); $this->assertEquals( array( diff --git a/tests/forms/LookupFieldTest.php b/tests/forms/LookupFieldTest.php index 17558a93e2b..99112389e09 100644 --- a/tests/forms/LookupFieldTest.php +++ b/tests/forms/LookupFieldTest.php @@ -35,10 +35,9 @@ public function testStringValueWithNumericArraySource() { public function testUnknownStringValueWithNumericArraySource() { $source = array(1 => 'one', 2 => 'two', 3 => 'three'); $f = new LookupField('test', 'test', $source); - $f->setValue('w00t'); - $f->dontEscape = true; // simulates CMSMain->compareversions() + $f->setValue('w00t'); $this->assertEquals( - 'w00t', + 'w00t', trim($f->Field()->getValue()) ); } diff --git a/tests/model/DBFieldTest.php b/tests/model/DBFieldTest.php index 6ad22393747..52046087c2f 100644 --- a/tests/model/DBFieldTest.php +++ b/tests/model/DBFieldTest.php @@ -211,7 +211,7 @@ public function testExists() { public function testStringFieldsWithMultibyteData() { $plainFields = array('Varchar', 'Text'); - $htmlFields = array('HTMLVarchar', 'HTMLText'); + $htmlFields = array('HTMLVarchar', 'HTMLText', 'HTMLFragment'); $allFields = array_merge($plainFields, $htmlFields); $value = 'üåäöÜÅÄÖ'; diff --git a/tests/model/DBHTMLTextTest.php b/tests/model/DBHTMLTextTest.php index f1f53dddfb3..35e4c0ae943 100644 --- a/tests/model/DBHTMLTextTest.php +++ b/tests/model/DBHTMLTextTest.php @@ -185,14 +185,14 @@ function testExists() { } function testWhitelist() { - $textObj = new DBHTMLText('Test', 'meta,link'); + $textObj = new DBHTMLText('Test', 'whitelist=meta,link'); $this->assertEquals( '', $textObj->whitelistContent('

Remove

Remove Text'), 'Removes any elements not in whitelist excluding text elements' ); - $textObj = new DBHTMLText('Test', 'meta,link,text()'); + $textObj = new DBHTMLText('Test', 'whitelist=meta,link,text()'); $this->assertEquals( 'Keep Text', $textObj->whitelistContent('

Remove

Keep Text'), diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php index 04b79601c29..6f604cb0664 100644 --- a/tests/security/MemberTest.php +++ b/tests/security/MemberTest.php @@ -236,7 +236,7 @@ public function testForgotPasswordEmaling() { // Check existance of reset link $this->assertEmailSent("testuser@example.com", null, 'Your password reset link', - '/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/'); + '/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/'); } /** diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index 2f92f494e67..c5bb6297c2a 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -5,7 +5,7 @@ use SilverStripe\Security\Member; use SilverStripe\Security\SecurityToken; use SilverStripe\Security\Permission; - +use SilverStripe\Model\FieldType\DBField; class SSViewerTest extends SapphireTest { @@ -835,21 +835,17 @@ public function testCastingHelpers() { $t = SSViewer::fromString('$HTMLValue.XML')->process($vd) ); - // Uncasted value (falls back to ViewableData::$default_cast="HTMLText") - $vd = new SSViewerTest_ViewableData(); // TODO Fix caching + // Uncasted value (falls back to ViewableData::$default_cast="Text") + $vd = new SSViewerTest_ViewableData(); $vd->UncastedValue = 'html'; $this->assertEquals( - 'html', + '<b>html</b>', $t = SSViewer::fromString('$UncastedValue')->process($vd) ); - $vd = new SSViewerTest_ViewableData(); // TODO Fix caching - $vd->UncastedValue = 'html'; $this->assertEquals( 'html', $t = SSViewer::fromString('$UncastedValue.RAW')->process($vd) ); - $vd = new SSViewerTest_ViewableData(); // TODO Fix caching - $vd->UncastedValue = 'html'; $this->assertEquals( '<b>html</b>', $t = SSViewer::fromString('$UncastedValue.XML')->process($vd) @@ -1247,8 +1243,14 @@ public function testRewriteHashlinks() { '); $tmpl = new SSViewer($tmplFile); $obj = new ViewableData(); - $obj->InsertedLink = 'InsertedLink'; - $obj->ExternalInsertedLink = 'ExternalInsertedLink'; + $obj->InsertedLink = DBField::create_field( + 'HTMLFragment', + 'InsertedLink' + ); + $obj->ExternalInsertedLink = DBField::create_field( + 'HTMLFragment', + 'ExternalInsertedLink' + ); $result = $tmpl->process($obj); $this->assertContains( 'InsertedLink', @@ -1295,7 +1297,10 @@ public function testRewriteHashlinksInPhpMode() { '); $tmpl = new SSViewer($tmplFile); $obj = new ViewableData(); - $obj->InsertedLink = 'InsertedLink'; + $obj->InsertedLink = DBField::create_field( + 'HTMLFragment', + 'InsertedLink' + ); $result = $tmpl->process($obj); $code = <<<'EOC' @@ -1583,7 +1588,7 @@ private function argedName($fieldName, $arguments) { if($arguments) return $childName . '(' . implode(',', $arguments) . ')'; else return $childName; } - public function obj($fieldName, $arguments=null, $forceReturnedObject=true, $cache=false, $cacheName=null) { + public function obj($fieldName, $arguments=null, $cache=false, $cacheName=null) { $childName = $this->argedName($fieldName, $arguments); // Special field name Loop### to create a list @@ -1618,9 +1623,11 @@ public function hasValue($fieldName, $arguments = null, $cache = true) { class SSViewerTest_ViewableData extends ViewableData implements TestOnly { + private static $default_cast = 'Text'; + private static $casting = array( 'TextValue' => 'Text', - 'HTMLValue' => 'HTMLText' + 'HTMLValue' => 'HTMLFragment' ); public function methodWithOneArgument($arg1) { @@ -1667,14 +1674,14 @@ class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly { public static function get_template_global_variables() { return array( - 'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLText'), + 'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLFragment'), 'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'), 'SSViewerTest_GlobalAutomatic', 'SSViewerTest_GlobalReferencedByString' => 'get_reference', 'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'), - 'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLText') + 'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLFragment') ); } diff --git a/tests/view/ViewableDataTest.php b/tests/view/ViewableDataTest.php index 11601506c53..8a2ebe031f2 100644 --- a/tests/view/ViewableDataTest.php +++ b/tests/view/ViewableDataTest.php @@ -1,4 +1,7 @@ testCastingHelpers()} for more tests related to casting and ViewableData behaviour, * from a template-parsing perspective. @@ -8,14 +11,39 @@ */ class ViewableDataTest extends SapphireTest { + public function testCasting() { + $htmlString = """; + $textString = '"'; + + $htmlField = DBField::create_field('HTMLFragment', $textString); + + $this->assertEquals($textString, $htmlField->forTemplate()); + $this->assertEquals($htmlString, $htmlField->obj('HTMLATT')->forTemplate()); + $this->assertEquals('%22', $htmlField->obj('URLATT')->forTemplate()); + $this->assertEquals('%22', $htmlField->obj('RAWURLATT')->forTemplate()); + $this->assertEquals($htmlString, $htmlField->obj('ATT')->forTemplate()); + $this->assertEquals($textString, $htmlField->obj('RAW')->forTemplate()); + $this->assertEquals('\"', $htmlField->obj('JS')->forTemplate()); + $this->assertEquals($textString, $htmlField->obj('HTML')->forTemplate()); + $this->assertEquals($textString, $htmlField->obj('XML')->forTemplate()); + + $textField = DBField::create_field('Text', $textString); + $this->assertEquals($htmlString, $textField->forTemplate()); + $this->assertEquals($htmlString, $textField->obj('HTMLATT')->forTemplate()); + $this->assertEquals('%22', $textField->obj('URLATT')->forTemplate()); + $this->assertEquals('%22', $textField->obj('RAWURLATT')->forTemplate()); + $this->assertEquals($htmlString, $textField->obj('ATT')->forTemplate()); + $this->assertEquals($textString, $textField->obj('RAW')->forTemplate()); + $this->assertEquals('\"', $textField->obj('JS')->forTemplate()); + $this->assertEquals($htmlString, $textField->obj('HTML')->forTemplate()); + $this->assertEquals($htmlString, $textField->obj('XML')->forTemplate()); + } + public function testRequiresCasting() { $caster = new ViewableDataTest_Castable(); - $this->assertTrue($caster->obj('alwaysCasted') instanceof ViewableDataTest_RequiresCasting); - $this->assertTrue($caster->obj('noCastingInformation') instanceof ViewableData_Caster); - - $this->assertTrue($caster->obj('alwaysCasted', null, false) instanceof ViewableDataTest_RequiresCasting); - $this->assertFalse($caster->obj('noCastingInformation', null, false) instanceof ViewableData_Caster); + $this->assertInstanceOf('ViewableDataTest_RequiresCasting', $caster->obj('alwaysCasted')); + $this->assertInstanceOf('ViewableData_Caster', $caster->obj('noCastingInformation')); } public function testFailoverRequiresCasting() { @@ -23,34 +51,24 @@ public function testFailoverRequiresCasting() { $container = new ViewableDataTest_Container(); $container->setFailover($caster); - $this->assertTrue($container->obj('alwaysCasted') instanceof ViewableDataTest_RequiresCasting); - $this->assertTrue($caster->obj('alwaysCasted', null, false) instanceof ViewableDataTest_RequiresCasting); + $this->assertInstanceOf('ViewableDataTest_RequiresCasting', $container->obj('alwaysCasted')); + $this->assertInstanceOf('ViewableDataTest_RequiresCasting', $caster->obj('alwaysCasted')); - /* @todo This currently fails, because the default_cast static variable is always taken from the topmost - * object, not the failover object the field actually came from. Should we fix this, or declare current - * behaviour as correct? - * - * $this->assertTrue($container->obj('noCastingInformation') instanceof ViewableData_Caster); - * $this->assertFalse($caster->obj('noCastingInformation', null, false) instanceof ViewableData_Caster); - */ + $this->assertInstanceOf('ViewableData_Caster', $container->obj('noCastingInformation')); + $this->assertInstanceOf('ViewableData_Caster', $caster->obj('noCastingInformation')); } public function testCastingXMLVal() { $caster = new ViewableDataTest_Castable(); $this->assertEquals('casted', $caster->XML_val('alwaysCasted')); - $this->assertEquals('noCastingInformation', $caster->XML_val('noCastingInformation')); + $this->assertEquals('casted', $caster->XML_val('noCastingInformation')); - // test automatic escaping is only applied by casted classes - $this->assertEquals('', $caster->XML_val('unsafeXML')); + // Test automatic escaping is applied even to fields with no 'casting' + $this->assertEquals('casted', $caster->XML_val('unsafeXML')); $this->assertEquals('<foo>', $caster->XML_val('castedUnsafeXML')); } - public function testUncastedXMLVal() { - $caster = new ViewableDataTest_Castable(); - $this->assertEquals($caster->XML_val('uncastedZeroValue'), 0); - } - public function testArrayCustomise() { $viewableData = new ViewableDataTest_Castable(); $newViewableData = $viewableData->customise(array ( @@ -95,32 +113,9 @@ public function testDefaultValueWrapping() { $this->assertEquals('SomeTitleValue', $obj->forTemplate()); } - public function testRAWVal() { - $data = new ViewableDataTest_Castable(); - $data->test = 'This & This'; - $this->assertEquals($data->RAW_val('test'), 'This & This'); - } - - public function testSQLVal() { - $data = new ViewableDataTest_Castable(); - $this->assertEquals($data->SQL_val('test'), 'test'); - } - - public function testJSVal() { - $data = new ViewableDataTest_Castable(); - $data->test = '"this is a test"'; - $this->assertEquals($data->JS_val('test'), '\"this is a test\"'); - } - - public function testATTVal() { - $data = new ViewableDataTest_Castable(); - $data->test = '"this is a test"'; - $this->assertEquals($data->ATT_val('test'), '"this is a test"'); - } - public function testCastingClass() { $expected = array( - 'NonExistant' => null, + //'NonExistant' => null, 'Field' => 'CastingType', 'Argument' => 'ArgumentType', 'ArrayArgument' => 'ArrayArgumentType' @@ -149,7 +144,7 @@ public function testObjWithCachedStringValueReturnsValidObject() { // Uncasted data should always be the nonempty string $this->assertNotEmpty($uncastedData, 'Uncasted data was empty.'); - $this->assertTrue(is_string($uncastedData), 'Uncasted data should be a string.'); + //$this->assertTrue(is_string($uncastedData), 'Uncasted data should be a string.'); // Casted data should be the string wrapped in a DBField-object. $this->assertNotEmpty($castedData, 'Casted data was empty.'); @@ -205,7 +200,8 @@ class ViewableDataTest_Castable extends ViewableData { private static $casting = array ( 'alwaysCasted' => 'ViewableDataTest_RequiresCasting', - 'castedUnsafeXML' => 'ViewableData_UnescaptedCaster' + 'castedUnsafeXML' => 'ViewableData_UnescaptedCaster', + 'test' => 'Text', ); public $test = 'test'; diff --git a/view/ArrayData.php b/view/ArrayData.php index 28a6612e41d..fabdd5583f4 100644 --- a/view/ArrayData.php +++ b/view/ArrayData.php @@ -74,9 +74,11 @@ public function getField($f) { * * @param string $field * @param mixed $value + * @return $this */ public function setField($field, $value) { $this->array[$field] = $value; + return $this; } /** diff --git a/view/SSTemplateParser.php b/view/SSTemplateParser.php index 77442298b9b..cc48950c170 100644 --- a/view/SSTemplateParser.php +++ b/view/SSTemplateParser.php @@ -25,46 +25,46 @@ * @subpackage view */ class SSTemplateParseException extends Exception { - + function __construct($message, $parser) { $prior = substr($parser->string, 0, $parser->pos); - + preg_match_all('/\r\n|\r|\n/', $prior, $matches); $line = count($matches[0])+1; - + parent::__construct("Parse error in template on line $line. Error was: $message"); } - + } /** * This is the parser for the SilverStripe template language. It gets called on a string and uses a php-peg parser * to match that string against the language structure, building up the PHP code to execute that structure as it * parses - * + * * The $result array that is built up as part of the parsing (see thirdparty/php-peg/README.md for more on how * parsers build results) has one special member, 'php', which contains the php equivalent of that part of the * template tree. - * + * * Some match rules generate alternate php, or other variations, so check the per-match documentation too. - * + * * Terms used: - * + * * Marked: A string or lookup in the template that has been explictly marked as such - lookups by prepending with * "$" (like $Foo.Bar), strings by wrapping with single or double quotes ('Foo' or "Foo") - * + * * Bare: The opposite of marked. An argument that has to has it's type inferred by usage and 2.4 defaults. - * + * * Example of using a bare argument for a loop block: <% loop Foo %> - * + * * Block: One of two SS template structures. The special characters "<%" and "%>" are used to wrap the opening and * (required or forbidden depending on which block exactly) closing block marks. - * + * * Open Block: An SS template block that doesn't wrap any content or have a closing end tag (in fact, a closing end * tag is forbidden) - * + * * Closed Block: An SS template block that wraps content, and requires a counterpart <% end_blockname %> tag - * + * * Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements * N: eats white space including newlines (using in legacy _t support) * @@ -120,7 +120,7 @@ function construct($matchrule, $name, $arguments = null) { /** * Set the closed blocks that the template parser should use - * + * * This method will delete any existing closed blocks, please use addClosedBlock if you don't * want to overwrite * @param array $closedBlocks @@ -195,7 +195,7 @@ protected function validateExtensionBlock($name, $callable, $type) { ); } } - + /* Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock | OpenBlock | MalformedBlock | Injection | Text)+ */ protected $match_Template_typestack = array('Template'); @@ -450,7 +450,7 @@ function match_Template ($stack = array()) { function Template_STR(&$res, $sub) { $res['php'] .= $sub['php'] . PHP_EOL ; } - + /* Word: / [A-Za-z_] [A-Za-z0-9_]* / */ protected $match_Word_typestack = array('Word'); function match_Word ($stack = array()) { @@ -550,14 +550,14 @@ function match_CallArguments ($stack = array()) { - /** + /** * Values are bare words in templates, but strings in PHP. We rely on PHP's type conversion to back-convert * strings to numbers when needed. */ function CallArguments_Argument(&$res, $sub) { if (!empty($res['php'])) $res['php'] .= ', '; - - $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : + + $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : str_replace('$$FINAL', 'XML_val', $sub['php']); } @@ -739,22 +739,22 @@ function match_Lookup ($stack = array()) { - + function Lookup__construct(&$res) { $res['php'] = '$scope->locally()'; $res['LookupSteps'] = array(); } - - /** - * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to + + /** + * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to * get the next ViewableData in the sequence, and LastLookupStep calls different methods (XML_val, hasValue, obj) * depending on the context the lookup is used in. */ function Lookup_AddLookupStep(&$res, $sub, $method) { $res['LookupSteps'][] = $sub; - + $property = $sub['Call']['Method']['text']; - + if (isset($sub['Call']['CallArguments']) && $arguments = $sub['Call']['CallArguments']['php']) { $res['php'] .= "->$method('$property', array($arguments), true)"; } @@ -1239,21 +1239,21 @@ function match_Argument ($stack = array()) { - + /** * If we get a bare value, we don't know enough to determine exactly what php would be the translation, because * we don't know if the position of use indicates a lookup or a string argument. - * + * * Instead, we record 'ArgumentMode' as a member of this matches results node, which can be: * - lookup if this argument was unambiguously a lookup (marked as such) * - string is this argument was unambiguously a string (marked as such, or impossible to parse as lookup) * - default if this argument needs to be handled as per 2.4 - * + * * In the case of 'default', there is no php member of the results node, but instead 'lookup_php', which * should be used by the parent if the context indicates a lookup, and 'string_php' which should be used * if the context indicates a string */ - + function Argument_DollarMarkedLookup(&$res, $sub) { $res['ArgumentMode'] = 'lookup'; $res['php'] = $sub['Lookup']['php']; @@ -1275,12 +1275,12 @@ function Argument_Lookup(&$res, $sub) { $res['php'] = $sub['php']; } } - + function Argument_FreeString(&$res, $sub) { $res['ArgumentMode'] = 'string'; $res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'"; } - + /* ComparisonOperator: "!=" | "==" | ">=" | ">" | "<=" | "<" | "=" */ protected $match_ComparisonOperator_typestack = array('ComparisonOperator'); function match_ComparisonOperator ($stack = array()) { @@ -1425,7 +1425,7 @@ function Comparison_Argument(&$res, $sub) { if ($sub['ArgumentMode'] == 'default') { if (!empty($res['php'])) $res['php'] .= $sub['string_php']; else $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']); - } + } else { $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } @@ -1481,7 +1481,7 @@ function match_PresenceCheck ($stack = array()) { function PresenceCheck_Not(&$res, $sub) { $res['php'] = '!'; } - + function PresenceCheck_Argument(&$res, $sub) { if ($sub['ArgumentMode'] == 'string') { $res['php'] .= '((bool)'.$sub['php'].')'; @@ -1792,22 +1792,22 @@ function match_If ($stack = array()) { function If_IfPart(&$res, $sub) { - $res['php'] = + $res['php'] = 'if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; - } + } function If_ElseIfPart(&$res, $sub) { - $res['php'] .= + $res['php'] .= 'else if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; } function If_ElsePart(&$res, $sub) { - $res['php'] .= - 'else { ' . PHP_EOL . + $res['php'] .= + 'else { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; } @@ -1879,10 +1879,10 @@ function Require_Call(&$res, $sub) { $res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');'; } - + /* CacheBlockArgument: !( "if " | "unless " ) - ( + ( :DollarMarkedLookup | :QuotedString | :Lookup @@ -1988,15 +1988,15 @@ function match_CacheBlockArgument ($stack = array()) { function CacheBlockArgument_DollarMarkedLookup(&$res, $sub) { $res['php'] = $sub['Lookup']['php']; } - + function CacheBlockArgument_QuotedString(&$res, $sub) { $res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'"; } - + function CacheBlockArgument_Lookup(&$res, $sub) { $res['php'] = $sub['php']; } - + /* CacheBlockArguments: CacheBlockArgument ( < "," < CacheBlockArgument )* */ protected $match_CacheBlockArguments_typestack = array('CacheBlockArguments'); function match_CacheBlockArguments ($stack = array()) { @@ -2046,10 +2046,10 @@ function match_CacheBlockArguments ($stack = array()) { function CacheBlockArguments_CacheBlockArgument(&$res, $sub) { if (!empty($res['php'])) $res['php'] .= ".'_'."; else $res['php'] = ''; - + $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } - + /* CacheBlockTemplate: (Comment | Translate | If | Require | OldI18NTag | Include | ClosedBlock | OpenBlock | MalformedBlock | Injection | Text)+ */ protected $match_CacheBlockTemplate_typestack = array('CacheBlockTemplate','Template'); @@ -2265,8 +2265,8 @@ function match_CacheBlockTemplate ($stack = array()) { - - /* UncachedBlock: + + /* UncachedBlock: '<%' < "uncached" < CacheBlockArguments? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>' Template:$TemplateMatcher? '<%' < 'end_' ("uncached"|"cached"|"cacheblock") > '%>' */ @@ -2425,7 +2425,7 @@ function match_UncachedBlock ($stack = array()) { function UncachedBlock_Template(&$res, $sub){ $res['php'] = $sub['php']; } - + /* CacheRestrictedTemplate: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock | OpenBlock | MalformedBlock | Injection | Text)+ */ protected $match_CacheRestrictedTemplate_typestack = array('CacheRestrictedTemplate','Template'); @@ -2677,17 +2677,17 @@ function match_CacheRestrictedTemplate ($stack = array()) { - function CacheRestrictedTemplate_CacheBlock(&$res, $sub) { + function CacheRestrictedTemplate_CacheBlock(&$res, $sub) { throw new SSTemplateParseException('You cant have cache blocks nested within with, loop or control blocks ' . 'that are within cache blocks', $this); } - - function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) { + + function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) { throw new SSTemplateParseException('You cant have uncache blocks nested within with, loop or control blocks ' . 'that are within cache blocks', $this); } - - /* CacheBlock: + + /* CacheBlock: '<%' < CacheTag:("cached"|"cacheblock") < (CacheBlockArguments)? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>' (CacheBlock | UncachedBlock | CacheBlockTemplate)* @@ -2930,23 +2930,23 @@ function match_CacheBlock ($stack = array()) { function CacheBlock__construct(&$res){ $res['subblocks'] = 0; } - + function CacheBlock_CacheBlockArguments(&$res, $sub){ $res['key'] = !empty($sub['php']) ? $sub['php'] : ''; } - + function CacheBlock_Condition(&$res, $sub){ $res['condition'] = ($res['Conditional']['text'] == 'if' ? '(' : '!(') . $sub['php'] . ') && '; } - + function CacheBlock_CacheBlock(&$res, $sub){ $res['php'] .= $sub['php']; } - + function CacheBlock_UncachedBlock(&$res, $sub){ $res['php'] .= $sub['php']; } - + function CacheBlock_CacheBlockTemplate(&$res, $sub){ // Get the block counter $block = ++$res['subblocks']; @@ -2971,14 +2971,14 @@ function CacheBlock_CacheBlockTemplate(&$res, $sub){ . ".'_$block'"; // block index // Get any condition $condition = isset($res['condition']) ? $res['condition'] : ''; - + $res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL; $res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL; $res['php'] .= $sub['php'] . PHP_EOL; $res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL; $res['php'] .= '}'; } - + /* OldTPart: "_t" N "(" N QuotedString (N "," N CallArguments)? N ")" N (";")? */ protected $match_OldTPart_typestack = array('OldTPart'); function match_OldTPart ($stack = array()) { @@ -3089,7 +3089,7 @@ function match_N ($stack = array()) { function OldTPart__construct(&$res) { $res['php'] = "_t("; } - + function OldTPart_QuotedString(&$res, $sub) { $entity = $sub['String']['text']; if (strpos($entity, '.') === false) { @@ -3099,7 +3099,7 @@ function OldTPart_QuotedString(&$res, $sub) { $res['php'] .= "'$entity'"; } } - + function OldTPart_CallArguments(&$res, $sub) { $res['php'] .= ',' . $sub['php']; } @@ -3107,7 +3107,7 @@ function OldTPart_CallArguments(&$res, $sub) { function OldTPart__finalise(&$res) { $res['php'] .= ')'; } - + /* OldTTag: "<%" < OldTPart > "%>" */ protected $match_OldTTag_typestack = array('OldTTag'); function match_OldTTag ($stack = array()) { @@ -3137,7 +3137,7 @@ function OldTTag_OldTPart(&$res, $sub) { $res['php'] = $sub['php']; } - /* OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" */ + /* OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" */ protected $match_OldSprintfTag_typestack = array('OldSprintfTag'); function match_OldSprintfTag ($stack = array()) { $matchrule = "OldSprintfTag"; $result = $this->construct($matchrule, $matchrule, null); @@ -3191,7 +3191,7 @@ function match_OldSprintfTag ($stack = array()) { function OldSprintfTag__construct(&$res) { $res['php'] = "sprintf("; } - + function OldSprintfTag_OldTPart(&$res, $sub) { $res['php'] .= $sub['php']; } @@ -3199,7 +3199,7 @@ function OldSprintfTag_OldTPart(&$res, $sub) { function OldSprintfTag_CallArguments(&$res, $sub) { $res['php'] .= ',' . $sub['php'] . ')'; } - + /* OldI18NTag: OldSprintfTag | OldTTag */ protected $match_OldI18NTag_typestack = array('OldI18NTag'); function match_OldI18NTag ($stack = array()) { @@ -3382,7 +3382,7 @@ function Include__finalise(&$res){ $template = $res['template']; $arguments = $res['arguments']; - $res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . + $res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . implode(',', $arguments)."), \$scope);\n"; if($this->includeDebuggingComments) { // Add include filename comments on dev sites @@ -3393,7 +3393,7 @@ function Include__finalise(&$res){ } } - /* BlockArguments: :Argument ( < "," < :Argument)* */ + /* BlockArguments: :Argument ( < "," < :Argument)* */ protected $match_BlockArguments_typestack = array('BlockArguments'); function match_BlockArguments ($stack = array()) { $matchrule = "BlockArguments"; $result = $this->construct($matchrule, $matchrule, null); @@ -3596,7 +3596,7 @@ function match_NotBlockTag ($stack = array()) { } - /* ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher? + /* ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher? '<%' < 'end_' '$BlockName' > '%>' */ protected $match_ClosedBlock_typestack = array('ClosedBlock'); function match_ClosedBlock ($stack = array()) { @@ -3692,7 +3692,7 @@ function match_ClosedBlock ($stack = array()) { - + /** * As mentioned in the parser comment, block handling is kept fairly generic for extensibility. The match rule * builds up two important elements in the match result array: @@ -3702,15 +3702,15 @@ function match_ClosedBlock ($stack = array()) { * Once a block has successfully been matched against, it will then look for the actual handler, which should * be on this class (either defined or extended on) as ClosedBlock_Handler_Name(&$res), where Name is the * tag name, first letter captialized (i.e Control, Loop, With, etc). - * + * * This function will be called with the match rule result array as it's first argument. It should return * the php result of this block as it's return value, or throw an error if incorrect arguments were passed. */ - + function ClosedBlock__construct(&$res) { $res['ArgumentCount'] = 0; } - + function ClosedBlock_BlockArguments(&$res, $sub) { if (isset($sub['Argument']['ArgumentMode'])) { $res['Arguments'] = array($sub['Argument']); @@ -3747,13 +3747,13 @@ function ClosedBlock_Handle_Loop(&$res) { //loop without arguments loops on the current scope if ($res['ArgumentCount'] == 0) { - $on = '$scope->obj(\'Up\', null, true)->obj(\'Foo\', null, true)'; + $on = '$scope->obj(\'Up\', null)->obj(\'Foo\', null)'; } else { //loop in the normal way $arg = $res['Arguments'][0]; if ($arg['ArgumentMode'] == 'string') { throw new SSTemplateParseException('Control block cant take string as argument.', $this); } - $on = str_replace('$$FINAL', 'obj', + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); } @@ -3771,19 +3771,19 @@ function ClosedBlock_Handle_With(&$res) { throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one ' . 'argument only.', $this); } - + $arg = $res['Arguments'][0]; if ($arg['ArgumentMode'] == 'string') { throw new SSTemplateParseException('Control block cant take string as argument.', $this); } - + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); - return + return $on . '; $scope->pushScope();' . PHP_EOL . $res['Template']['php'] . PHP_EOL . '; $scope->popScope(); '; } - + /* OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>' */ protected $match_OpenBlock_typestack = array('OpenBlock'); function match_OpenBlock ($stack = array()) { @@ -3851,7 +3851,7 @@ function match_OpenBlock ($stack = array()) { function OpenBlock__construct(&$res) { $res['ArgumentCount'] = 0; } - + function OpenBlock_BlockArguments(&$res, $sub) { if (isset($sub['Argument']['ArgumentMode'])) { $res['Arguments'] = array($sub['Argument']); @@ -3884,9 +3884,9 @@ function OpenBlock_Handle_Debug(&$res) { if ($res['ArgumentCount'] == 0) return '$scope->debug();'; else if ($res['ArgumentCount'] == 1) { $arg = $res['Arguments'][0]; - + if ($arg['ArgumentMode'] == 'string') return 'Debug::show('.$arg['php'].');'; - + $php = ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']; return '$val .= Debug::show('.str_replace('FINALGET!', 'cachedCall', $php).');'; } @@ -3910,7 +3910,7 @@ function OpenBlock_Handle_Current_page(&$res) { if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Current_page takes no arguments', $this); return '$val .= $_SERVER[SCRIPT_URL];'; } - + /* MismatchedEndBlock: '<%' < 'end_' :Word > '%>' */ protected $match_MismatchedEndBlock_typestack = array('MismatchedEndBlock'); function match_MismatchedEndBlock ($stack = array()) { @@ -3942,7 +3942,7 @@ function match_MismatchedEndBlock ($stack = array()) { function MismatchedEndBlock__finalise(&$res) { $blockname = $res['Word']['text']; - throw new SSTemplateParseException('Unexpected close tag end_' . $blockname . + throw new SSTemplateParseException('Unexpected close tag end_' . $blockname . ' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this); } @@ -4031,7 +4031,7 @@ function MalformedOpenTag__finalise(&$res) { throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?" , $this); } - + /* MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' ) */ protected $match_MalformedCloseTag_typestack = array('MalformedCloseTag'); function match_MalformedCloseTag ($stack = array()) { @@ -4096,7 +4096,7 @@ function MalformedCloseTag__finalise(&$res) { throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " . "argument to one?", $this); } - + /* MalformedBlock: MalformedOpenTag | MalformedCloseTag */ protected $match_MalformedBlock_typestack = array('MalformedBlock'); function match_MalformedBlock ($stack = array()) { @@ -4187,7 +4187,7 @@ function match_Comment ($stack = array()) { function Comment__construct(&$res) { $res['php'] = ''; } - + /* TopTemplate: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock | OpenBlock | MalformedBlock | MismatchedEndBlock | Injection | Text)+ */ protected $match_TopTemplate_typestack = array('TopTemplate','Template'); @@ -4457,7 +4457,7 @@ function match_TopTemplate ($stack = array()) { - + /** * The TopTemplate also includes the opening stanza to start off the template */ @@ -4671,13 +4671,13 @@ function match_Text ($stack = array()) { - + /** - * We convert text + * We convert text */ function Text__finalise(&$res) { $text = $res['text']; - + // Unescape any escaped characters in the text, then put back escapes for any single quotes and backslashes $text = stripslashes($text); $text = addcslashes($text, '\'\\'); @@ -4698,14 +4698,14 @@ function Text__finalise(&$res) { $res['php'] .= '$val .= \'' . $text . '\';' . PHP_EOL; } - + /****************** * Here ends the parser itself. Below are utility methods to use the parser */ - + /** * Compiles some passed template source code into the php code that will execute as per the template source. - * + * * @throws SSTemplateParseException * @param $string The source of the template * @param string $templateName The name of the template, normally the filename the template source was loaded from @@ -4719,13 +4719,13 @@ public function compileString($string, $templateName = "", $includeDebuggingComm } else { parent::__construct($string); - + $this->includeDebuggingComments = $includeDebuggingComments; - + // Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF // (and other encodings) properly if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3; - + // Match the source against the parser if ($topTemplate) { $result = $this->match_TopTemplate(); @@ -4733,7 +4733,7 @@ public function compileString($string, $templateName = "", $includeDebuggingComm $result = $this->match_Template(); } if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this); - + // Get the result $code = $result['php']; } @@ -4741,8 +4741,8 @@ public function compileString($string, $templateName = "", $includeDebuggingComm // Include top level debugging comments if desired if($includeDebuggingComments && $templateName && stripos($code, "includeDebuggingComments($code, $templateName); - } - + } + return $code; } @@ -4778,11 +4778,11 @@ protected function includeDebuggingComments($code, $templateName) { } return $code; } - + /** * Compiles some file that contains template source code, and returns the php code that will execute as per that * source - * + * * @static * @param $template - A file path that contains template source code * @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source diff --git a/view/SSTemplateParser.php.inc b/view/SSTemplateParser.php.inc index 942ec945947..bc0b57d68c3 100644 --- a/view/SSTemplateParser.php.inc +++ b/view/SSTemplateParser.php.inc @@ -4,16 +4,16 @@ /*!* !silent This is the uncompiled parser for the SilverStripe template language, PHP with special comments that define the -parser. +parser. -It gets run through the php-peg parser compiler to have those comments turned into code that match parts of the +It gets run through the php-peg parser compiler to have those comments turned into code that match parts of the template language, producing the executable version SSTemplateParser.php To recompile after changing this file, run this from the 'framework/view' directory via command line (in most cases this is: sapphire/view): - - php ../thirdparty/php-peg/cli.php SSTemplateParser.php.inc > SSTemplateParser.php - + + php ../thirdparty/php-peg/cli.php SSTemplateParser.php.inc > SSTemplateParser.php + See the php-peg docs for more information on the parser format, and how to convert this file into SSTemplateParser.php TODO: @@ -22,9 +22,9 @@ TODO: Partial cache blocks i18n - we dont support then deprecated _t() or sprintf(_t()) methods; or the new <% t %> block yet Add with and loop blocks - Add Up and Top + Add Up and Top More error detection? - + This comment will not appear in the output */ @@ -46,46 +46,46 @@ else { * @subpackage view */ class SSTemplateParseException extends Exception { - + function __construct($message, $parser) { $prior = substr($parser->string, 0, $parser->pos); - + preg_match_all('/\r\n|\r|\n/', $prior, $matches); $line = count($matches[0])+1; - + parent::__construct("Parse error in template on line $line. Error was: $message"); } - + } /** * This is the parser for the SilverStripe template language. It gets called on a string and uses a php-peg parser * to match that string against the language structure, building up the PHP code to execute that structure as it * parses - * + * * The $result array that is built up as part of the parsing (see thirdparty/php-peg/README.md for more on how * parsers build results) has one special member, 'php', which contains the php equivalent of that part of the * template tree. - * + * * Some match rules generate alternate php, or other variations, so check the per-match documentation too. - * + * * Terms used: - * + * * Marked: A string or lookup in the template that has been explictly marked as such - lookups by prepending with * "$" (like $Foo.Bar), strings by wrapping with single or double quotes ('Foo' or "Foo") - * + * * Bare: The opposite of marked. An argument that has to has it's type inferred by usage and 2.4 defaults. - * + * * Example of using a bare argument for a loop block: <% loop Foo %> - * + * * Block: One of two SS template structures. The special characters "<%" and "%>" are used to wrap the opening and * (required or forbidden depending on which block exactly) closing block marks. - * + * * Open Block: An SS template block that doesn't wrap any content or have a closing end tag (in fact, a closing end * tag is forbidden) - * + * * Closed Block: An SS template block that wraps content, and requires a counterpart <% end_blockname %> tag - * + * * Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements * N: eats white space including newlines (using in legacy _t support) * @@ -141,7 +141,7 @@ class SSTemplateParser extends Parser implements TemplateParser { /** * Set the closed blocks that the template parser should use - * + * * This method will delete any existing closed blocks, please use addClosedBlock if you don't * want to overwrite * @param array $closedBlocks @@ -216,22 +216,22 @@ class SSTemplateParser extends Parser implements TemplateParser { ); } } - + /*!* SSTemplateParser - # Template is any structurally-complete portion of template (a full nested level in other words). It's the + # Template is any structurally-complete portion of template (a full nested level in other words). It's the # primary matcher, and is used by all enclosing blocks, as well as a base for the top level. # Any new template elements need to be included in this list, if they are to work. - + Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock | OpenBlock | MalformedBlock | Injection | Text)+ */ function Template_STR(&$res, $sub) { $res['php'] .= $sub['php'] . PHP_EOL ; } - + /*!* - + Word: / [A-Za-z_] [A-Za-z0-9_]* / NamespacedWord: / [A-Za-z_\/\\] [A-Za-z0-9_\/\\]* / Number: / [0-9]+ / @@ -239,53 +239,53 @@ class SSTemplateParser extends Parser implements TemplateParser { # CallArguments is a list of one or more comma seperated "arguments" (lookups or strings, either bare or marked) # as passed to a Call within brackets - + CallArguments: :Argument ( < "," < :Argument )* */ - /** + /** * Values are bare words in templates, but strings in PHP. We rely on PHP's type conversion to back-convert * strings to numbers when needed. */ function CallArguments_Argument(&$res, $sub) { if (!empty($res['php'])) $res['php'] .= ', '; - - $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : + + $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : str_replace('$$FINAL', 'XML_val', $sub['php']); } /*!* - + # Call is a php-style function call, e.g. Method(Argument, ...). Unlike PHP, the brackets are optional if no # arguments are passed - + Call: Method:Word ( "(" < :CallArguments? > ")" )? # A lookup is a lookup of a value on the current scope object. It's a sequence of calls seperated by "." - # characters. This final call in the sequence needs handling specially, as different structures need different + # characters. This final call in the sequence needs handling specially, as different structures need different # sorts of values, which require a different final method to be called to get the right return value - + LookupStep: :Call &"." LastLookupStep: :Call Lookup: LookupStep ("." LookupStep)* "." LastLookupStep | LastLookupStep */ - + function Lookup__construct(&$res) { $res['php'] = '$scope->locally()'; $res['LookupSteps'] = array(); } - - /** - * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to + + /** + * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to * get the next ViewableData in the sequence, and LastLookupStep calls different methods (XML_val, hasValue, obj) * depending on the context the lookup is used in. */ function Lookup_AddLookupStep(&$res, $sub, $method) { $res['LookupSteps'][] = $sub; - + $property = $sub['Call']['Method']['text']; - + if (isset($sub['Call']['CallArguments']) && $arguments = $sub['Call']['CallArguments']['php']) { $res['php'] .= "->$method('$property', array($arguments), true)"; } @@ -358,10 +358,10 @@ class SSTemplateParser extends Parser implements TemplateParser { /*!* - + # Injections are where, outside of a block, a value needs to be inserted into the output. You can either # just do $Foo, or {$Foo} if the surrounding text would cause a problem (e.g. {$Foo}Bar) - + SimpleInjection: '$' :Lookup BracketInjection: '{$' :Lookup "}" Injection: BracketInjection | SimpleInjection @@ -371,11 +371,11 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - + # Inside a block's arguments you can still use the same format as a simple injection ($Foo). In this case # it marks the argument as being a lookup, not a string (if it was bare it might still be used as a lookup, # but that depends on where it's used, a la 2.4) - + DollarMarkedLookup: SimpleInjection */ function DollarMarkedLookup_STR(&$res, $sub) { @@ -383,45 +383,45 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - + # Inside a block's arguments you can explictly mark a string by surrounding it with quotes (single or double, # but they must be matching). If you do, inside the quote you can escape any character, but the only character - # that _needs_ escaping is the matching closing quote - + # that _needs_ escaping is the matching closing quote + QuotedString: q:/['"]/ String:/ (\\\\ | \\. | [^$q\\])* / '$q' - + # In order to support 2.4's base syntax, we also need to detect free strings - strings not surrounded by # quotes, and containing spaces or punctuation, but supported as a single string. We support almost as flexible # a string as 2.4 - we don't attempt to determine the closing character by context, but just break on any # character which, in some context, would indicate the end of a free string, regardless of if we're actually in - # that context or not - + # that context or not + FreeString: /[^,)%!=><|&]+/ - + # An argument - either a marked value, or a bare value, prefering lookup matching on the bare value over - # freestring matching as long as that would give a successful parse - + # freestring matching as long as that would give a successful parse + Argument: :DollarMarkedLookup | :QuotedString | :Lookup !(< FreeString)| :FreeString */ - + /** * If we get a bare value, we don't know enough to determine exactly what php would be the translation, because * we don't know if the position of use indicates a lookup or a string argument. - * + * * Instead, we record 'ArgumentMode' as a member of this matches results node, which can be: * - lookup if this argument was unambiguously a lookup (marked as such) * - string is this argument was unambiguously a string (marked as such, or impossible to parse as lookup) * - default if this argument needs to be handled as per 2.4 - * + * * In the case of 'default', there is no php member of the results node, but instead 'lookup_php', which * should be used by the parent if the context indicates a lookup, and 'string_php' which should be used * if the context indicates a string */ - + function Argument_DollarMarkedLookup(&$res, $sub) { $res['ArgumentMode'] = 'lookup'; $res['php'] = $sub['Lookup']['php']; @@ -443,16 +443,16 @@ class SSTemplateParser extends Parser implements TemplateParser { $res['php'] = $sub['php']; } } - + function Argument_FreeString(&$res, $sub) { $res['ArgumentMode'] = 'string'; $res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'"; } - + /*!* - + # if and else_if blocks allow basic comparisons between arguments - + ComparisonOperator: "!=" | "==" | ">=" | ">" | "<=" | "<" | "=" Comparison: Argument < ComparisonOperator > Argument @@ -461,7 +461,7 @@ class SSTemplateParser extends Parser implements TemplateParser { if ($sub['ArgumentMode'] == 'default') { if (!empty($res['php'])) $res['php'] .= $sub['string_php']; else $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']); - } + } else { $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } @@ -472,17 +472,17 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - + # If a comparison operator is not used in an if or else_if block, then the statement is a 'presence check', # which checks if the argument given is present or not. For explicit strings (which were not allowed in 2.4) # this falls back to simple truthiness check - + PresenceCheck: (Not:'not' <)? Argument */ function PresenceCheck_Not(&$res, $sub) { $res['php'] = '!'; } - + function PresenceCheck_Argument(&$res, $sub) { if ($sub['ArgumentMode'] == 'string') { $res['php'] .= '((bool)'.$sub['php'].')'; @@ -495,11 +495,11 @@ class SSTemplateParser extends Parser implements TemplateParser { } } - /*!* - - # if and else_if arguments are a series of presence checks and comparisons, optionally seperated by boolean - # operators - + /*!* + + # if and else_if arguments are a series of presence checks and comparisons, optionally seperated by boolean + # operators + IfArgumentPortion: Comparison | PresenceCheck */ function IfArgumentPortion_STR(&$res, $sub) { @@ -507,14 +507,14 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - + # if and else_if arguments can be combined via these two boolean operators. No precendence overriding is # supported - + BooleanOperator: "||" | "&&" - - # This is the combination of the previous if and else_if argument portions - + + # This is the combination of the previous if and else_if argument portions + IfArgument: :IfArgumentPortion ( < :BooleanOperator < :IfArgumentPortion )* */ function IfArgument_IfArgumentPortion(&$res, $sub) { @@ -525,12 +525,12 @@ class SSTemplateParser extends Parser implements TemplateParser { $res['php'] .= $sub['text']; } - /*!* - + /*!* + # ifs are handled seperately from other closed block tags, because (A) their structure is different - they # can have else_if and else tags in between the if tag and the end_if tag, and (B) they have a different - # argument structure to every other block - + # argument structure to every other block + IfPart: '<%' < 'if' [ :IfArgument > '%>' Template:$TemplateMatcher? ElseIfPart: '<%' < 'else_if' [ :IfArgument > '%>' Template:$TemplateMatcher? ElsePart: '<%' < 'else' > '%>' Template:$TemplateMatcher? @@ -538,45 +538,45 @@ class SSTemplateParser extends Parser implements TemplateParser { If: IfPart ElseIfPart* ElsePart? '<%' < 'end_if' > '%>' */ function If_IfPart(&$res, $sub) { - $res['php'] = + $res['php'] = 'if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; - } + } function If_ElseIfPart(&$res, $sub) { - $res['php'] .= + $res['php'] .= 'else if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; } function If_ElsePart(&$res, $sub) { - $res['php'] .= - 'else { ' . PHP_EOL . + $res['php'] .= + 'else { ' . PHP_EOL . (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL . '}'; } /*!* - + # The require block is handled seperately to the other open blocks as the argument syntax is different # - must have one call style argument, must pass arguments to that call style argument - + Require: '<%' < 'require' [ Call:(Method:Word "(" < :CallArguments > ")") > '%>' */ function Require_Call(&$res, $sub) { $res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');'; } - + /*!* - + # Cache block arguments don't support free strings - + CacheBlockArgument: !( "if " | "unless " ) - ( + ( :DollarMarkedLookup | :QuotedString | :Lookup @@ -585,38 +585,38 @@ class SSTemplateParser extends Parser implements TemplateParser { function CacheBlockArgument_DollarMarkedLookup(&$res, $sub) { $res['php'] = $sub['Lookup']['php']; } - + function CacheBlockArgument_QuotedString(&$res, $sub) { $res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'"; } - + function CacheBlockArgument_Lookup(&$res, $sub) { $res['php'] = $sub['php']; } - + /*!* - + # Collects the arguments passed in to be part of the key of a cacheblock - + CacheBlockArguments: CacheBlockArgument ( < "," < CacheBlockArgument )* - + */ function CacheBlockArguments_CacheBlockArgument(&$res, $sub) { if (!empty($res['php'])) $res['php'] .= ".'_'."; else $res['php'] = ''; - + $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } - + /*!* # CacheBlockTemplate is the same as Template, but doesn't include cache blocks (because they're handled seperately) - + CacheBlockTemplate extends Template (TemplateMatcher = CacheRestrictedTemplate); CacheBlock | UncachedBlock | => '' */ - + /*!* - - UncachedBlock: + + UncachedBlock: '<%' < "uncached" < CacheBlockArguments? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>' Template:$TemplateMatcher? '<%' < 'end_' ("uncached"|"cached"|"cacheblock") > '%>' @@ -624,53 +624,53 @@ class SSTemplateParser extends Parser implements TemplateParser { function UncachedBlock_Template(&$res, $sub){ $res['php'] = $sub['php']; } - + /*!* - + # CacheRestrictedTemplate is the same as Template, but doesn't allow cache blocks - + CacheRestrictedTemplate extends Template */ - function CacheRestrictedTemplate_CacheBlock(&$res, $sub) { + function CacheRestrictedTemplate_CacheBlock(&$res, $sub) { throw new SSTemplateParseException('You cant have cache blocks nested within with, loop or control blocks ' . 'that are within cache blocks', $this); } - - function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) { + + function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) { throw new SSTemplateParseException('You cant have uncache blocks nested within with, loop or control blocks ' . 'that are within cache blocks', $this); } - + /*!* # The partial caching block - - CacheBlock: + + CacheBlock: '<%' < CacheTag:("cached"|"cacheblock") < (CacheBlockArguments)? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>' (CacheBlock | UncachedBlock | CacheBlockTemplate)* '<%' < 'end_' ("cached"|"uncached"|"cacheblock") > '%>' - + */ function CacheBlock__construct(&$res){ $res['subblocks'] = 0; } - + function CacheBlock_CacheBlockArguments(&$res, $sub){ $res['key'] = !empty($sub['php']) ? $sub['php'] : ''; } - + function CacheBlock_Condition(&$res, $sub){ $res['condition'] = ($res['Conditional']['text'] == 'if' ? '(' : '!(') . $sub['php'] . ') && '; } - + function CacheBlock_CacheBlock(&$res, $sub){ $res['php'] .= $sub['php']; } - + function CacheBlock_UncachedBlock(&$res, $sub){ $res['php'] .= $sub['php']; } - + function CacheBlock_CacheBlockTemplate(&$res, $sub){ // Get the block counter $block = ++$res['subblocks']; @@ -695,21 +695,21 @@ class SSTemplateParser extends Parser implements TemplateParser { . ".'_$block'"; // block index // Get any condition $condition = isset($res['condition']) ? $res['condition'] : ''; - + $res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL; $res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL; $res['php'] .= $sub['php'] . PHP_EOL; $res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL; $res['php'] .= '}'; } - + /*!* - + # Deprecated old-style i18n _t and sprintf(_t block tags. We support a slightly more flexible version than we used # to, but just because it's easier to do so. It's strongly recommended to use the new syntax - + # This is the core used by both syntaxes, without the block start & end tags - + OldTPart: "_t" N "(" N QuotedString (N "," N CallArguments)? N ")" N (";")? # whitespace with a newline @@ -718,7 +718,7 @@ class SSTemplateParser extends Parser implements TemplateParser { function OldTPart__construct(&$res) { $res['php'] = "_t("; } - + function OldTPart_QuotedString(&$res, $sub) { $entity = $sub['String']['text']; if (strpos($entity, '.') === false) { @@ -728,7 +728,7 @@ class SSTemplateParser extends Parser implements TemplateParser { $res['php'] .= "'$entity'"; } } - + function OldTPart_CallArguments(&$res, $sub) { $res['php'] .= ',' . $sub['php']; } @@ -736,29 +736,29 @@ class SSTemplateParser extends Parser implements TemplateParser { function OldTPart__finalise(&$res) { $res['php'] .= ')'; } - + /*!* - + # This is the old <% _t() %> tag - + OldTTag: "<%" < OldTPart > "%>" - + */ function OldTTag_OldTPart(&$res, $sub) { $res['php'] = $sub['php']; } /*!* - + # This is the old <% sprintf(_t()) %> tag - - OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" - + + OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" + */ function OldSprintfTag__construct(&$res) { $res['php'] = "sprintf("; } - + function OldSprintfTag_OldTPart(&$res, $sub) { $res['php'] .= $sub['php']; } @@ -766,15 +766,15 @@ class SSTemplateParser extends Parser implements TemplateParser { function OldSprintfTag_CallArguments(&$res, $sub) { $res['php'] .= ',' . $sub['php'] . ')'; } - + /*!* - + # This matches either the old style sprintf(_t()) or _t() tags. As well as including the output portion of the # php, this rule combines all the old i18n stuff into a single match rule to make it easy to not support these - # tags later - + # tags later + OldI18NTag: OldSprintfTag | OldTTag - + */ function OldI18NTag_STR(&$res, $sub) { $res['php'] = '$val .= ' . $sub['php'] . ';'; @@ -830,7 +830,7 @@ class SSTemplateParser extends Parser implements TemplateParser { $template = $res['template']; $arguments = $res['arguments']; - $res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . + $res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . implode(',', $arguments)."), \$scope);\n"; if($this->includeDebuggingComments) { // Add include filename comments on dev sites @@ -842,26 +842,26 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - + # To make the block support reasonably extendable, we don't explicitly define each closed block and it's structure, # but instead match against a generic <% block_name argument, ... %> pattern. Each argument is left as per the # output of the Argument matcher, and the handler (see the PHPDoc block later for more on this) is responsible - # for pulling out the info required - - BlockArguments: :Argument ( < "," < :Argument)* - + # for pulling out the info required + + BlockArguments: :Argument ( < "," < :Argument)* + # NotBlockTag matches against any word that might come after a "<%" that the generic open and closed block handlers - # shouldn't attempt to match against, because they're handled by more explicit matchers - + # shouldn't attempt to match against, because they're handled by more explicit matchers + NotBlockTag: "end_" | (("if" | "else_if" | "else" | "require" | "cached" | "uncached" | "cacheblock" | "include")]) - + # Match against closed blocks - blocks with an opening and a closing tag that surround some internal portion of # template - - ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher? + + ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher? '<%' < 'end_' '$BlockName' > '%>' */ - + /** * As mentioned in the parser comment, block handling is kept fairly generic for extensibility. The match rule * builds up two important elements in the match result array: @@ -871,15 +871,15 @@ class SSTemplateParser extends Parser implements TemplateParser { * Once a block has successfully been matched against, it will then look for the actual handler, which should * be on this class (either defined or extended on) as ClosedBlock_Handler_Name(&$res), where Name is the * tag name, first letter captialized (i.e Control, Loop, With, etc). - * + * * This function will be called with the match rule result array as it's first argument. It should return * the php result of this block as it's return value, or throw an error if incorrect arguments were passed. */ - + function ClosedBlock__construct(&$res) { $res['ArgumentCount'] = 0; } - + function ClosedBlock_BlockArguments(&$res, $sub) { if (isset($sub['Argument']['ArgumentMode'])) { $res['Arguments'] = array($sub['Argument']); @@ -916,13 +916,13 @@ class SSTemplateParser extends Parser implements TemplateParser { //loop without arguments loops on the current scope if ($res['ArgumentCount'] == 0) { - $on = '$scope->obj(\'Up\', null, true)->obj(\'Foo\', null, true)'; + $on = '$scope->obj(\'Up\', null)->obj(\'Foo\', null)'; } else { //loop in the normal way $arg = $res['Arguments'][0]; if ($arg['ArgumentMode'] == 'string') { throw new SSTemplateParseException('Control block cant take string as argument.', $this); } - $on = str_replace('$$FINAL', 'obj', + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); } @@ -940,30 +940,30 @@ class SSTemplateParser extends Parser implements TemplateParser { throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one ' . 'argument only.', $this); } - + $arg = $res['Arguments'][0]; if ($arg['ArgumentMode'] == 'string') { throw new SSTemplateParseException('Control block cant take string as argument.', $this); } - + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); - return + return $on . '; $scope->pushScope();' . PHP_EOL . $res['Template']['php'] . PHP_EOL . '; $scope->popScope(); '; } - + /*!* - + # Open blocks are handled in the same generic manner as closed blocks. There is no need to define which blocks # are which - closed is tried first, and if no matching end tag is found, open is tried next - + OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>' */ function OpenBlock__construct(&$res) { $res['ArgumentCount'] = 0; } - + function OpenBlock_BlockArguments(&$res, $sub) { if (isset($sub['Argument']['ArgumentMode'])) { $res['Arguments'] = array($sub['Argument']); @@ -996,9 +996,9 @@ class SSTemplateParser extends Parser implements TemplateParser { if ($res['ArgumentCount'] == 0) return '$scope->debug();'; else if ($res['ArgumentCount'] == 1) { $arg = $res['Arguments'][0]; - + if ($arg['ArgumentMode'] == 'string') return 'Debug::show('.$arg['php'].');'; - + $php = ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']; return '$val .= Debug::show('.str_replace('FINALGET!', 'cachedCall', $php).');'; } @@ -1022,26 +1022,26 @@ class SSTemplateParser extends Parser implements TemplateParser { if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Current_page takes no arguments', $this); return '$val .= $_SERVER[SCRIPT_URL];'; } - + /*!* - + # This is used to detect when we have a mismatched closing tag (i.e., one with no equivilent opening tag) # Because of parser limitations, this can only be used at the top nesting level of a template. Other mismatched - # closing tags are detected as an invalid open tag - + # closing tags are detected as an invalid open tag + MismatchedEndBlock: '<%' < 'end_' :Word > '%>' */ function MismatchedEndBlock__finalise(&$res) { $blockname = $res['Word']['text']; - throw new SSTemplateParseException('Unexpected close tag end_' . $blockname . + throw new SSTemplateParseException('Unexpected close tag end_' . $blockname . ' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this); } - /*!* - + /*!* + # This is used to detect a malformed opening tag - one where the tag is opened with the "<%" characters, but - # the tag is not structured properly - + # the tag is not structured properly + MalformedOpenTag: '<%' < !NotBlockTag Tag:Word !( ( [ :BlockArguments ] )? > '%>' ) */ function MalformedOpenTag__finalise(&$res) { @@ -1049,12 +1049,12 @@ class SSTemplateParser extends Parser implements TemplateParser { throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?" , $this); } - + /*!* - + # This is used to detect a malformed end tag - one where the tag is opened with the "<%" characters, but - # the tag is not structured properly - + # the tag is not structured properly + MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' ) */ function MalformedCloseTag__finalise(&$res) { @@ -1062,32 +1062,32 @@ class SSTemplateParser extends Parser implements TemplateParser { throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " . "argument to one?", $this); } - + /*!* - + # This is used to detect a malformed tag. It's mostly to keep the Template match rule a bit shorter - + MalformedBlock: MalformedOpenTag | MalformedCloseTag */ /*!* - + # This is used to remove template comments - + Comment: "<%--" (!"--%>" /(?s)./)+ "--%>" */ function Comment__construct(&$res) { $res['php'] = ''; } - + /*!* - + # TopTemplate is the same as Template, but should only be used at the top level (not nested), as it includes - # MismatchedEndBlock detection, which only works at the top level - + # MismatchedEndBlock detection, which only works at the top level + TopTemplate extends Template (TemplateMatcher = Template); MalformedBlock => MalformedBlock | MismatchedEndBlock */ - + /** * The TopTemplate also includes the opening stanza to start off the template */ @@ -1096,11 +1096,11 @@ class SSTemplateParser extends Parser implements TemplateParser { } /*!* - - # Text matches anything that isn't a template command (not an injection, block of any kind or comment) - + + # Text matches anything that isn't a template command (not an injection, block of any kind or comment) + Text: ( - # Any set of characters that aren't potentially a control mark or an escaped character + # Any set of characters that aren't potentially a control mark or an escaped character / [^<${\\]+ / | # An escaped character / (\\.) / | @@ -1114,13 +1114,13 @@ class SSTemplateParser extends Parser implements TemplateParser { '{$' !(/[A-Za-z_]/) )+ */ - + /** - * We convert text + * We convert text */ function Text__finalise(&$res) { $text = $res['text']; - + // Unescape any escaped characters in the text, then put back escapes for any single quotes and backslashes $text = stripslashes($text); $text = addcslashes($text, '\'\\'); @@ -1141,14 +1141,14 @@ EOC; $res['php'] .= '$val .= \'' . $text . '\';' . PHP_EOL; } - + /****************** * Here ends the parser itself. Below are utility methods to use the parser */ - + /** * Compiles some passed template source code into the php code that will execute as per the template source. - * + * * @throws SSTemplateParseException * @param $string The source of the template * @param string $templateName The name of the template, normally the filename the template source was loaded from @@ -1162,13 +1162,13 @@ EOC; } else { parent::__construct($string); - + $this->includeDebuggingComments = $includeDebuggingComments; - + // Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF // (and other encodings) properly if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3; - + // Match the source against the parser if ($topTemplate) { $result = $this->match_TopTemplate(); @@ -1176,7 +1176,7 @@ EOC; $result = $this->match_Template(); } if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this); - + // Get the result $code = $result['php']; } @@ -1184,8 +1184,8 @@ EOC; // Include top level debugging comments if desired if($includeDebuggingComments && $templateName && stripos($code, "includeDebuggingComments($code, $templateName); - } - + } + return $code; } @@ -1221,11 +1221,11 @@ EOC; } return $code; } - + /** * Compiles some file that contains template source code, and returns the php code that will execute as per that * source - * + * * @static * @param $template - A file path that contains template source code * @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source diff --git a/view/SSViewer.php b/view/SSViewer.php index 7e5605a58f0..058e2635eb1 100644 --- a/view/SSViewer.php +++ b/view/SSViewer.php @@ -1,11 +1,9 @@ currentIndex) = end($this->itemStack); } - public function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { + public function getObj($name, $arguments = [], $cache = false, $cacheName = null) { $on = $this->itemIterator ? $this->itemIterator->current() : $this->item; - return $on->obj($name, $arguments, $forceReturnedObject, $cache, $cacheName); + return $on->obj($name, $arguments, $cache, $cacheName); } - public function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { + public function obj($name, $arguments = [], $cache = false, $cacheName = null) { switch ($name) { case 'Up': if ($this->upIndex === null) { @@ -122,7 +120,7 @@ public function obj($name, $arguments = null, $forceReturnedObject = true, $cach break; default: - $this->item = $this->getObj($name, $arguments, $forceReturnedObject, $cache, $cacheName); + $this->item = $this->getObj($name, $arguments, $cache, $cacheName); $this->itemIterator = null; $this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1; $this->currentIndex = count($this->itemStack); @@ -598,7 +596,7 @@ public function popScope() { * $Up and $Top need to restore the overlay from the parent and top-level * scope respectively. */ - public function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { + public function obj($name, $arguments = [], $cache = false, $cacheName = null) { $overlayIndex = false; switch($name) { @@ -622,13 +620,15 @@ public function obj($name, $arguments = null, $forceReturnedObject = true, $cach } } - return parent::obj($name, $arguments, $forceReturnedObject, $cache, $cacheName); + return parent::obj($name, $arguments, $cache, $cacheName); } - public function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { + public function getObj($name, $arguments = [], $cache = false, $cacheName = null) { $result = $this->getInjectedValue($name, (array)$arguments); - if($result) return $result['obj']; - else return parent::getObj($name, $arguments, $forceReturnedObject, $cache, $cacheName); + if($result) { + return $result['obj']; + } + return parent::getObj($name, $arguments, $cache, $cacheName); } public function __call($name, $arguments) { @@ -1187,7 +1187,7 @@ protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underl * @param array|null $arguments - arguments to an included template * @param Object $inheritedScope - the current scope of a parent template including a sub-template * - * @return HTMLText Parsed template output. + * @return DBHTMLText Parsed template output. */ public function process($item, $arguments = null, $inheritedScope = null) { SSViewer::$topLevel[] = $item; @@ -1252,7 +1252,7 @@ public function process($item, $arguments = null, $inheritedScope = null) { } } - return DBField::create_field('HTMLText', $output, null, array('shortcodes' => false)); + return DBField::create_field('HTMLFragment', $output); } /** diff --git a/view/TemplateGlobalProvider.php b/view/TemplateGlobalProvider.php index ad434b470ff..60e34b00226 100644 --- a/view/TemplateGlobalProvider.php +++ b/view/TemplateGlobalProvider.php @@ -28,7 +28,7 @@ interface TemplateGlobalProvider { * - template name => method name * - template name => array(), where the array can contain these key => value pairs * - "method" => method name - * - "casting" => casting class to use (i.e., Varchar, HTMLText, etc) + * - "casting" => casting class to use (i.e., Varchar, HTMLFragment, etc) */ public static function get_template_global_variables(); } diff --git a/view/TemplateIteratorProvider.php b/view/TemplateIteratorProvider.php index ce2207f93ad..07a45ff4792 100644 --- a/view/TemplateIteratorProvider.php +++ b/view/TemplateIteratorProvider.php @@ -28,7 +28,7 @@ interface TemplateIteratorProvider { * - template name => method name * - template name => array(), where the array can contain these key => value pairs * - "method" => method name - * - "casting" => casting class to use (i.e., Varchar, HTMLText, etc) + * - "casting" => casting class to use (i.e., Varchar, HTMLFragment, etc) */ public static function get_template_iterator_variables(); diff --git a/view/ViewableData.php b/view/ViewableData.php index c8641b18c36..1e93d715ea4 100644 --- a/view/ViewableData.php +++ b/view/ViewableData.php @@ -1,6 +1,8 @@ failover) { return $this->failover->$property; } + return null; } /** @@ -124,6 +105,7 @@ public function __get($property) { * @param mixed $value */ public function __set($property, $value) { + $this->objCacheClear(); if($this->hasMethod($method = "set$property")) { $this->$method($value); } else { @@ -180,9 +162,12 @@ public function getField($field) { * * @param string $field * @param mixed $value + * @return $this */ public function setField($field, $value) { + $this->objCacheClear(); $this->$field = $value; + return $this; } // ----------------------------------------------------------------------------------------------------------------- @@ -190,11 +175,15 @@ public function setField($field, $value) { /** * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an * underscore into a {@link ViewableData::cachedCall()}. + * + * @throws LogicException */ public function defineMethods() { + if($this->failover && !is_object($this->failover)) { + throw new LogicException("ViewableData::\$failover set to a non-object"); + } if($this->failover) { - if(is_object($this->failover)) $this->addMethodsFrom('failover'); - else user_error("ViewableData::\$failover set to a non-object", E_USER_WARNING); + $this->addMethodsFrom('failover'); if(isset($_REQUEST['debugfailover'])) { Debug::message("$this->class created with a failover class of {$this->failover->class}"); @@ -244,50 +233,43 @@ public function setCustomisedObj(ViewableData $object) { // CASTING --------------------------------------------------------------------------------------------------------- /** - * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to - * an object (see {@link ViewableData::castingHelper()} for information on casting helpers). - * - * The returned array contains two keys: - * - className: the class the field would be casted to (e.g. "Varchar") - * - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)") - * - * @param string $field - * @return array - */ - public function castingHelperPair($field) { - Deprecation::notice('2.5', 'use castingHelper() instead'); - return $this->castingHelper($field); - } - - /** - * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field - * on this object. + * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) + * for a field on this object. This helper will be a subclass of DBField. * * @param string $field - * @return string Casting helper + * @return string Casting helper As a constructor pattern, and may include arguments. */ public function castingHelper($field) { $specs = $this->config()->casting; if(isset($specs[$field])) { return $specs[$field]; - } elseif($this->failover) { - return $this->failover->castingHelper($field); + } + + // If no specific cast is declared, fall back to failover. + // Note that if there is a failover, the default_cast will always + // be drawn from this object instead of the top level object. + $failover = $this->getFailover(); + if($failover) { + $cast = $failover->castingHelper($field); + if($cast) { + return $cast; } } + // Fall back to default_cast + return $this->config()->get('default_cast'); + } + /** - * Get the class name a field on this object will be casted to + * Get the class name a field on this object will be casted to. * * @param string $field * @return string */ public function castingClass($field) { + // Strip arguments $spec = $this->castingHelper($field); - if(!$spec) return null; - - $bPos = strpos($spec,'('); - if($bPos === false) return $spec; - else return substr($spec, 0, $bPos); + return trim(strtok($spec, '(')); } /** @@ -304,35 +286,6 @@ public function escapeTypeForField($field) { return Injector::inst()->get($class, true)->config()->escape_type; } - /** - * Save the casting cache for this object (including data from any failovers) into a variable - * - * @param reference $cache - */ - public function buildCastingCache(&$cache) { - $ancestry = array_reverse(ClassInfo::ancestry($this->class)); - $merge = true; - - foreach($ancestry as $class) { - if(!isset(self::$casting_cache[$class]) && $merge) { - $mergeFields = is_subclass_of($class, 'SilverStripe\\ORM\\DataObject') ? array('db', 'casting') : array('casting'); - - if($mergeFields) foreach($mergeFields as $field) { - $casting = Config::inst()->get($class, $field, Config::UNINHERITED); - if($casting) foreach($casting as $field => $cast) { - if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast); - } - } - - if($class == 'ViewableData') $merge = false; - } elseif($merge) { - $cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class]; - } - - if($class == 'ViewableData') break; - } - } - // TEMPLATE ACCESS LAYER ------------------------------------------------------------------------------------------- /** @@ -344,7 +297,7 @@ public function buildCastingCache(&$cache) { * * @param string|array|SSViewer $template the template to render into * @param array $customFields fields to customise() the object with before rendering - * @return HTMLText + * @return DBHTMLText */ public function renderWith($template, $customFields = null) { if(!is_object($template)) { @@ -370,6 +323,7 @@ public function renderWith($template, $customFields = null) { * * @param string $fieldName Name of field * @param array $arguments List of optional arguments given + * @return string */ protected function objCacheName($fieldName, $arguments) { return $arguments @@ -384,7 +338,10 @@ protected function objCacheName($fieldName, $arguments) { * @return mixed */ protected function objCacheGet($key) { - if(isset($this->objCache[$key])) return $this->objCache[$key]; + if(isset($this->objCache[$key])) { + return $this->objCache[$key]; + } + return null; } /** @@ -392,9 +349,21 @@ protected function objCacheGet($key) { * * @param string $key Cache key * @param mixed $value + * @return $this */ protected function objCacheSet($key, $value) { $this->objCache[$key] = $value; + return $this; + } + + /** + * Clear object cache + * + * @return $this + */ + protected function objCacheClear() { + $this->objCache = []; + return $this; } /** @@ -403,45 +372,40 @@ protected function objCacheSet($key, $value) { * * @param string $fieldName * @param array $arguments - * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned, - * even if there is no explicit casting information * @param bool $cache Cache this object * @param string $cacheName a custom cache name + * @return Object|DBField */ - public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { - if(!$cacheName && $cache) $cacheName = $this->objCacheName($fieldName, $arguments); + public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null) { + if(!$cacheName && $cache) { + $cacheName = $this->objCacheName($fieldName, $arguments); + } + // Check pre-cached value $value = $cache ? $this->objCacheGet($cacheName) : null; - if(!isset($value)) { - // HACK: Don't call the deprecated FormField::Name() method - $methodIsAllowed = true; - if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false; + if($value !== null) { + return $value; + } - if($methodIsAllowed && $this->hasMethod($fieldName)) { - $value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName(); + // Load value from record + if($this->hasMethod($fieldName)) { + $value = call_user_func_array(array($this, $fieldName), $arguments ?: []); } else { $value = $this->$fieldName; } - if(!is_object($value) && ($this->castingClass($fieldName) || $forceReturnedObject)) { - if(!$castConstructor = $this->castingHelper($fieldName)) { - $castConstructor = $this->config()->default_cast; - } - - $valueObject = Object::create_from_string($castConstructor, $fieldName); + // Cast object + if(!is_object($value)) { + // Force cast + $castingHelper = $this->castingHelper($fieldName); + $valueObject = Object::create_from_string($castingHelper, $fieldName); $valueObject->setValue($value, $this); - $value = $valueObject; } - if($cache) $this->objCacheSet($cacheName, $value); - } - - if(!is_object($value) && $forceReturnedObject) { - $default = $this->config()->default_cast; - $castedValue = new $default($fieldName); - $castedValue->setValue($value); - $value = $castedValue; + // Record in cache + if($cache) { + $this->objCacheSet($cacheName, $value); } return $value; @@ -454,9 +418,10 @@ public function obj($fieldName, $arguments = null, $forceReturnedObject = true, * @param string $field * @param array $arguments * @param string $identifier an optional custom cache identifier + * @return Object|DBField */ - public function cachedCall($field, $arguments = null, $identifier = null) { - return $this->obj($field, $arguments, false, true, $identifier); + public function cachedCall($field, $arguments = [], $identifier = null) { + return $this->obj($field, $arguments, true, $identifier); } /** @@ -468,67 +433,30 @@ public function cachedCall($field, $arguments = null, $identifier = null) { * @param bool $cache * @return bool */ - public function hasValue($field, $arguments = null, $cache = true) { - $result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false); - - if(is_object($result) && $result instanceof Object) { + public function hasValue($field, $arguments = [], $cache = true) { + $result = $this->obj($field, $arguments, $cache); return $result->exists(); - } else { - // Empty paragraph checks are a workaround for TinyMCE - return ($result && $result !== '

'); } - } - /**#@+ + /** + * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a + * template. + * * @param string $field * @param array $arguments * @param bool $cache * @return string */ - - /** - * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a - * template. - */ - public function XML_val($field, $arguments = null, $cache = false) { - $result = $this->obj($field, $arguments, false, $cache); - return (is_object($result) && $result instanceof Object) ? $result->forTemplate() : $result; - } - - /** - * Return the value of the field without any escaping being applied. - */ - public function RAW_val($field, $arguments = null, $cache = true) { - return Convert::xml2raw($this->XML_val($field, $arguments, $cache)); + public function XML_val($field, $arguments = [], $cache = false) { + $result = $this->obj($field, $arguments, $cache); + // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br() + return $result->forTemplate(); } - /** - * Return the value of a field in an SQL-safe format. - */ - public function SQL_val($field, $arguments = null, $cache = true) { - return Convert::raw2sql($this->RAW_val($field, $arguments, $cache)); - } - - /** - * Return the value of a field in a JavaScript-save format. - */ - public function JS_val($field, $arguments = null, $cache = true) { - return Convert::raw2js($this->RAW_val($field, $arguments, $cache)); - } - - /** - * Return the value of a field escaped suitable to be inserted into an XML node attribute. - */ - public function ATT_val($field, $arguments = null, $cache = true) { - return Convert::raw2att($this->RAW_val($field, $arguments, $cache)); - } - - /**#@-*/ - /** * Get an array of XML-escaped values by field name * - * @param array $elements an array of field names + * @param array $fields an array of field names * @return array */ public function getXMLValues($fields) { @@ -579,7 +507,7 @@ public function Me() { * @param string $subtheme the subtheme path to get * @return string */ - public function ThemeDir($subtheme = false) { + public function ThemeDir($subtheme = null) { if( Config::inst()->get('SSViewer', 'theme_enabled') && $theme = Config::inst()->get('SSViewer', 'theme') @@ -681,20 +609,16 @@ public function hasMethod($method) { public function cachedCall($field, $arguments = null, $identifier = null) { if($this->customised->hasMethod($field) || $this->customised->hasField($field)) { - $result = $this->customised->cachedCall($field, $arguments, $identifier); - } else { - $result = $this->original->cachedCall($field, $arguments, $identifier); + return $this->customised->cachedCall($field, $arguments, $identifier); } - - return $result; + return $this->original->cachedCall($field, $arguments, $identifier); } - public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) { + public function obj($fieldName, $arguments = null, $cache = false, $cacheName = null) { if($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) { - return $this->customised->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName); + return $this->customised->obj($fieldName, $arguments, $cache, $cacheName); } - - return $this->original->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName); + return $this->original->obj($fieldName, $arguments, $cache, $cacheName); } }