API Enforce default_cast for all field usages

API Introduce HTMLFragment as casting helper for HTMLText with shortcodes disabled
API Introduce DBField::CDATA for XML file value encoding
API RSSFeed now casts from the underlying model rather than by override
API Introduce CustomMethods::getExtraMethodConfig() to allow metadata to be queried
BUG Remove _call hack from VirtualPage
API Remove FormField::$dontEscape
API Introduce HTMLReadonlyField for non-editable readonly HTML
API FormField::Field() now returns string in many cases rather than DBField instance.
API Remove redundant *_val methods from ViewableData
API ViewableData::obj() no longer has a $forceReturnObject parameter as it always returns an object
BUG  Fix issue with ViewableData caching incorrect field values after being modified.
API Remove deprecated DB class methods
API Enforce plain text left/right formfield titles
This commit is contained in:
Damian Mooyman 2016-06-03 20:51:02 +12:00
parent a0213aa2bf
commit 5c9044a007
70 changed files with 1453 additions and 1289 deletions

View File

@ -7,6 +7,9 @@ use Config;
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 @@ abstract class DBSchemaManager {
/**
* 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 @@ abstract class DBSchemaManager {
break;
}
}
} catch(Exception $ex) {
$error = $ex;
}
// finally {
} finally {
$this->schemaUpdateTransaction = null;
$this->schemaIsUpdating = false;
// }
if($error) throw $error;
}
}
/**
@ -303,7 +300,7 @@ abstract class DBSchemaManager {
* - 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 @@ abstract class DBSchemaManager {
$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 @@ abstract class DBSchemaManager {
$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 @@ abstract class DBSchemaManager {
$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 @@ abstract class DBSchemaManager {
$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 @@ abstract class DBSchemaManager {
* 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 @@ abstract class DBSchemaManager {
*/
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 @@ abstract class DBSchemaManager {
$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 @@ abstract class DBSchemaManager {
$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 class DBSchemaManager {
* 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;
}

View File

@ -65,14 +65,6 @@ class DB {
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 @@ class DB {
*/
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 @@ class DB {
*/
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 @@ class DB {
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 @@ class DB {
*/
public static function manipulate($manipulation) {
self::$lastQuery = $manipulation;
return self::get_conn()->manipulate($manipulation);
self::get_conn()->manipulate($manipulation);
}
/**
@ -384,14 +375,6 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
* - '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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
* @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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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 @@ class DB {
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.
*/

View File

@ -2598,9 +2598,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*
* @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)]);

View File

@ -42,9 +42,6 @@ class DBBoolean extends DBField {
return ($this->value) ? 'true' : 'false';
}
/**
* Saves this field to the given data object.
*/
public function saveInto($dataObject) {
$fieldName = $this->name;
if($fieldName) {

View File

@ -225,11 +225,14 @@ abstract class DBComposite extends DBField {
* @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 @@ abstract class DBComposite extends DBField {
// Set local record
$this->record[$field] = $value;
return $this;
}
/**

View File

@ -63,9 +63,6 @@ class DBDecimal extends DBField {
DB::require_field($this->tableName, $this->name, $values);
}
/**
* @param DataObject $dataObject
*/
public function saveInto($dataObject) {
$fieldName = $this->name;

View File

@ -48,12 +48,32 @@ use TextField;
*/
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 @@ abstract class DBField extends ViewableData {
* @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,50 +283,103 @@ abstract class DBField extends ViewableData {
}
/**
* 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 $this->XML();
}
/**
* XML encode this value
*
* @return string
*/
public function XML(){
return Convert::raw2xml($this->RAW());
}
public function XML(){
return Convert::raw2xml($this->RAW());
/**
* Safely escape for XML string
*
* @return string
*/
public function CDATA() {
return $this->forTemplate();
}
/**
@ -307,14 +394,15 @@ abstract class DBField extends ViewableData {
/**
* 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 @@ abstract class DBField extends ViewableData {
* 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 @@ DBG;
}
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;
}
}

View File

@ -14,6 +14,12 @@ use Exception;
* 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;
}
/**
* 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);
}
return parent::__construct($name, $options);
$this->whitelist = $whitelist;
return $this;
}
/**
@ -69,24 +119,52 @@ class DBHTMLText extends DBText {
* 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 <link /> <meta /> 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 @@ class DBHTMLText extends DBText {
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 @@ class DBHTMLText extends DBText {
return $this->RAW();
}
/**
* Safely escape for XML string
*
* @return string
*/
public function CDATA() {
return sprintf(
'<![CDATA[%s]]>',
str_replace(']]>', ']]]]><![CDATA[>', $this->RAW())
);
}
public function prepValueForDB($value) {
return parent::prepValueForDB($this->whitelistContent($value));
}
@ -277,6 +372,15 @@ class DBHTMLText extends DBText {
return new TextField($this->name, $title);
}
/**
* @return string
*/
public function NoHTML()
{
// Preserve line breaks
$text = preg_replace('/\<br(\s*)?\/?\>/i', "\n", $this->RAW());
// Convert back to plain text
return \Convert::xml2raw(strip_tags($text));
}
}

View File

@ -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;
/**
* 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 <img onload="exploiting_code();" src="..." />)
* 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 <link /> <meta /> 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"]);
}
return parent::setOptions($options);
}
public function forTemplate() {
@ -34,11 +75,21 @@ class DBHTMLVarchar extends DBVarchar {
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(
'<![CDATA[%s]]>',
str_replace(']]>', ']]]]><![CDATA[>', $this->forTemplate())
);
}
public function exists() {
@ -53,4 +104,15 @@ class DBHTMLVarchar extends DBVarchar {
return new TextField($this->name, $title);
}
/**
* @return string
*/
public function NoHTML()
{
// Preserve line breaks
$text = preg_replace('/\<br(\s*)?\/?\>/i', "\n", $this->RAW());
// Convert back to plain text
return \Convert::xml2raw(strip_tags($text));
}
}

View File

@ -29,14 +29,14 @@ class DBInt extends DBField {
}
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);
}

View File

@ -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:
* <ul><li>"nullifyEmpty"
* This is a boolean flag.
* True (the default) means that empty strings are automatically converted to nulls to be stored in
* the database. Set it to false to ensure that nulls and empty strings are kept intact in the database.
* </li></ul>
* @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 @@ abstract class DBString extends DBField {
* @return string
*/
public function forTemplate() {
return nl2br($this->XML());
return nl2br(parent::forTemplate());
}
/**
@ -170,9 +205,6 @@ abstract class DBString extends DBField {
/**
* 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 @@ abstract class DBString extends DBField {
return $ret;
}
/**
* Limit the number of words of the current field's
* content. This is XML safe, so characters like &
* are converted to &amp;
*
* @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 @@ abstract class DBString extends DBField {
/**
* Converts the current value for this StringField to uppercase.
*
* @return string
*/
public function UpperCase() {
@ -226,11 +243,11 @@ abstract class DBString extends DBField {
}
/**
* 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();
}
}

View File

@ -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 @@ use SilverStripe\ORM\DB;
* );
* </code>
*
* @see HTMLText
* @see HTMLVarchar
* @see DBHTMLText
* @see DBHTMLVarchar
* @see Varchar
*
* @package framework
@ -30,17 +31,12 @@ use SilverStripe\ORM\DB;
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 @@ class DBText extends DBString {
$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);
if ($sentCount == 0) return '';
for($i = 0; $i < $sentCount; $i++) {
if(isset($sentences[$i])) {
$sentence = trim($sentences[$i]);
if(!empty($sentence)) $output[] .= $sentence;
}
}
return count($output)==0 ? '' : implode($output, '. ') . '.';
public function LimitSentences($maxSentences = 2) {
if(!is_numeric($maxSentences)) {
throw new InvalidArgumentException("Text::LimitSentence() expects one numeric argument");
}
$value = $this->NoHTML();
if( !$value ) {
return "";
}
/**
* Caution: Not XML/HTML-safe - does not respect closing tags.
*/
public function FirstSentence() {
$paragraph = Convert::xml2raw( $this->RAW() );
if( !$paragraph ) return "";
$words = preg_split('/\s+/', $paragraph);
// 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)) {
return implode(' ', array_slice($words, 0, $i+1));
$sentences++;
if($sentences >= $maxSentences) {
return implode(' ', array_slice($words, 0, $i + 1));
}
}
}
/* 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);
// 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);
}
}
/**
* Return the first string that finishes with a period (.) in this text.
*
* @return string
*/
public function FirstSentence() {
return $this->LimitSentences(1);
}
/**
* Caution: Not XML/HTML-safe - does not respect closing tags.
* 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) {
// get first sentence?
// this needs to be more robust
$value = Convert::xml2raw( $this->RAW() /*, true*/ );
if(!$value) return '';
public function Summary($maxWords = 50, $maxParagraphs = 1) {
// Get plain-text version
$value = $this->NoHTML();
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"));
// 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);
}
// 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]));
// Add next sentence
$result .= ' ' . trim(array_shift( $sentences )).'.';
// 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]));
// Ensure that we don't trim half way through a tag or a link
$brokenLink = (
substr_count($result,'<') != substr_count($result,'>')) ||
(substr_count($result,'<a') != substr_count($result,'</a')
);
} while(($count < $maxWords || $brokenLink) && $sentences && trim( $sentences[0]));
if(preg_match('/<a[^>]*>/', $result) && !preg_match( '/<\/a>/', $result)) $result .= '</a>';
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,'<a') != substr_count($result,'</a')
);
} while(($count < $maxWords || $brokenLink) && $sentences && trim($sentences[0]));
if(preg_match( '/<a[^>]*>/', $result) && !preg_match( '/<\/a>/', $result)) {
$result .= '</a>';
}
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, "</p>") === false) return $value;
$data = substr($value, 0, strpos($value, "</p>") + 4);
if(strlen($data) < 20 && strpos($value, "</p>", strlen($data))) {
$data = substr($value, 0, strpos( $value, "</p>", 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 <span> 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 <span> 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, "<span class=\"highlight\">$stringPiece</span>",
$summary);
$summary = str_ireplace(
$stringPiece,
"<span class=\"highlight\">$stringPiece</span>",
$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 @@ class DBText extends DBString {
* 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();
}
/**

View File

@ -10,9 +10,9 @@ use SilverStripe\ORM\DB;
/**
* 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

View File

@ -19,7 +19,7 @@ use SS_HTTPResponse;
class CMSSecurity extends Security {
private static $casting = array(
'Title' => 'HTMLText'
'Title' => 'HTMLFragment'
);
private static $allowed_actions = array(

View File

@ -90,7 +90,7 @@ class PermissionCheckboxSetField extends FormField {
/**
* @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 @@ class PermissionCheckboxSetField extends FormField {
}
}
if($this->readonly) {
return DBField::create_field('HTMLText',
return
"<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
"<li class=\"help\">" .
_t(
@ -258,14 +258,12 @@ class PermissionCheckboxSetField extends FormField {
) .
"</li>" .
$options .
"</ul>\n"
);
"</ul>\n";
} else {
return DBField::create_field('HTMLText',
return
"<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
$options .
"</ul>\n"
);
"</ul>\n";
}
}

View File

@ -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:

View File

@ -299,45 +299,41 @@ class RSSFeed_Entry extends ViewableData {
/**
* 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;
}
/**

View File

@ -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 @@ class XMLDataFormatter extends DataFormatter {
$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('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $fieldValue));
} else {
$fieldValue = Convert::raw2xml($fieldValue);
}
$xml .= "<$fieldName>$fieldValue</$fieldName>\n";
}

View File

@ -50,40 +50,41 @@ trait CustomMethods {
$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 @@ trait CustomMethods {
* @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 @@ trait CustomMethods {
* @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;
}
/**

View File

@ -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.
<div class="hint" markdown="1">
You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML
content via `$MyHtmlField.XML`.
</div>
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]
* [api:DBField]

View File

@ -98,10 +98,50 @@ this purpose.
There's some exceptions to this rule, see the ["security" guide](../security).
</div>
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. `<element>$Field.CDATA</element>` will ensure that the `<element>` body is safely escaped
as a string.
<div class="warning" markdown="1">
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.
</div>
:::ss
$Content.XML
// transforms e.g. "<em>alert</em>" to "&lt;em&gt;alert&lt;/em&gt;"

View File

@ -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
<div>
$SomeHTML
</div>
:::php
class MyObject extends ViewableData {
public function getSomeHTML {
$title = Convert::raw2xml($this->Title);
return "<h1>{$title}</h1>";
}
}
After:
:::ss
<div>
$SomeHTML
</div>
:::php
class MyObject extends ViewableData {
private static $casting = [
'SomeHTML' => 'HTMLText'
];
public function getSomeHTML {
$title = Convert::raw2xml($this->Title);
return "<h1>{$title}</h1>";
}
}
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

View File

@ -116,7 +116,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
);
private static $casting = array (
'TreeTitle' => 'HTMLText'
'TreeTitle' => 'HTMLFragment'
);
/**
@ -458,12 +458,11 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
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('<a href="%s" target="_blank">%s</a>', $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 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
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;

View File

@ -517,7 +517,7 @@ trait ImageManipulation {
*/
public function IconTag() {
return DBField::create_field(
'HTMLText',
'HTMLFragment',
'<img src="' . Convert::raw2att($this->getIcon()) . '" />'
);
}

View File

@ -100,7 +100,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail {
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
'Tag' => 'HTMLText',
'Tag' => 'HTMLFragment',
'Size' => 'Varchar'
);
@ -113,7 +113,7 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail {
*
* @return string
*/
public function forTemplate() {
public function XML() {
return $this->getTag() ?: '';
}

View File

@ -58,9 +58,13 @@ class CheckboxField_Readonly extends ReadonlyField {
}
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';
}
}

View File

@ -144,7 +144,7 @@ class ConfirmedPasswordField extends FormField {
/**
* @param array $properties
*
* @return DBHTMLText
* @return string
*/
public function Field($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');

View File

@ -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 = '<i>'._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00</i>';
$valforInput = '';
}
$valforInput = $this->value ? Convert::raw2att($val) : "";
return "<span class=\"readonly ".$this->extraClass()."\" id=\"" . $this->id() . "\">$val</span>"
return "<span class=\"readonly ".$this->extraClass()."\" id=\"" . $this->ID() . "\">$val</span>"
. "<input type=\"hidden\" name=\"".$this->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 = '<i>'._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00</i>';
$valforInput = '';
}
$valforInput = $this->value ? Convert::raw2att($val) : "";
return "<input class=\"text\" type=\"text\" disabled=\"disabled\""
. " name=\"".$this->name."\" value=\"".$valforInput."\" />";
}

View File

@ -32,7 +32,7 @@ class DatalessField extends FormField {
* 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 @@ class DatalessField extends FormField {
/**
* @param bool $bool
* @return $this
*/
public function setAllowHTML($bool) {
$this->allowHTML = $bool;

View File

@ -97,7 +97,7 @@ class DatetimeField extends FormField {
/**
* @param array $properties
* @return HTMLText
* @return string
*/
public function FieldHolder($properties = array()) {
$config = array(
@ -112,16 +112,17 @@ class DatetimeField extends FormField {
/**
* @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 .
'<div class="clear"><!-- --></div>'
return sprintf(
'%s%s%s<div class="clear"><!-- --></div>',
$this->dateField->FieldHolder(),
$this->timeField->FieldHolder(),
$tzField
);
}
@ -139,6 +140,7 @@ class DatetimeField extends FormField {
* @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);

View File

@ -114,7 +114,7 @@ class DropdownField extends SingleSelectField {
/**
* @param array $properties
* @return DBHTMLText
* @return string
*/
public function Field($properties = array()) {
$options = array();

View File

@ -87,7 +87,7 @@ class FileField extends FormField {
/**
* @param array $properties
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
$properties = array_merge($properties, array(

View File

@ -5,6 +5,7 @@ use SilverStripe\ORM\FieldType\DBField;
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 @@ class Form extends RequestHandler {
public function defaultAction() {
if($this->hasDefaultAction && $this->actions) {
return $this->actions->First();
}
}
}
/**
@ -1804,7 +1814,7 @@ class Form extends RequestHandler {
public static function single_field_required() {
if(self::current_action() == 'callfieldmethod') {
return $_REQUEST['fieldName'];
}
}
}
/**

View File

@ -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 @@ class FormAction extends FormField {
/**
* @param array $properties
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
$properties = array_merge(
@ -100,7 +108,7 @@ class FormAction extends FormField {
/**
* @param array $properties
* @return HTMLText
* @return string
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);
@ -110,22 +118,6 @@ class FormAction extends FormField {
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 @@ class FormAction extends FormField {
}
/**
* 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 @@ class FormAction extends FormField {
}
/**
* 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
*/

View File

@ -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 @@ class FormField extends RequestHandler {
}
/**
* @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 @@ class FormField extends RequestHandler {
}
/**
* @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 @@ class FormField extends RequestHandler {
* such as an input tag.
*
* @param array $properties
*
* @return string
*/
public function Field($properties = array()) {
@ -1366,31 +1377,9 @@ class FormField extends RequestHandler {
$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.
*

View File

@ -0,0 +1,12 @@
<?php
/**
* Readonly field equivalent for literal HTML
*
* Unlike HTMLEditorField_Readonly, does not processs shortcodes
*/
class HTMLReadonlyField extends ReadonlyField {
private static $casting = [
'Value' => 'HTMLFragment'
];
}

View File

@ -12,8 +12,7 @@ class HiddenField extends FormField {
/**
* @param array $properties
*
* @return HTMLText
* @return string
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);

View File

@ -1,9 +1,5 @@
<?php
use SilverStripe\ORM\FieldType\DBField;
/**
* Render a button that will submit the form its contained in through ajax.
* If you want to add custom behaviour, please set {@link includeDefaultJS()} to FALSE
@ -19,9 +15,10 @@ class InlineFormAction extends FormField {
/**
* Create a new action button.
* @param action The method to call when the button is clicked
* @param title The label on the button
* @param extraClass A CSS class to apply to the button in addition to 'action'
*
* @param string $action The method to call when the button is clicked
* @param string $title The label on the button
* @param string $extraClass A CSS class to apply to the button in addition to 'action'
*/
public function __construct($action, $title = "", $extraClass = '') {
$this->extraClass = ' '.$extraClass;
@ -34,24 +31,23 @@ class InlineFormAction extends FormField {
/**
* @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() {

View File

@ -14,6 +14,11 @@
* @subpackage fields-dataless
*/
class LiteralField extends DatalessField {
private static $casting = [
'Value' => 'HTMLFragment',
];
/**
* @var string|FormField
*/

View File

@ -1,4 +1,5 @@
<?php
use SilverStripe\Model\FieldType\DBField;
use SilverStripe\ORM\DataObjectInterface;
@ -47,10 +48,7 @@ class LookupField extends MultiSelectField {
if($mapped) {
$attrValue = implode(', ', array_values($mapped));
if(!$this->dontEscape) {
$attrValue = Convert::raw2xml($attrValue);
}
$attrValue = Convert::raw2xml($attrValue);
$inputValue = implode(', ', array_values($values));
} else {
$attrValue = '<i>('._t('FormField.NONE', 'none').')</i>';
@ -58,7 +56,7 @@ class LookupField extends MultiSelectField {
}
$properties = array_merge($properties, array(
'AttrValue' => $attrValue,
'AttrValue' => DBField::create_field('HTMLFragment', $attrValue),
'InputValue' => $inputValue
));

View File

@ -52,15 +52,14 @@ class MoneyField extends FormField {
/**
* @param array
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
return DBField::create_field('HTMLText',
return
"<div class=\"fieldgroup\">" .
"<div class=\"fieldgroup-field\">" . $this->fieldCurrency->SmallFieldHolder() . "</div>" .
"<div class=\"fieldgroup-field\">" . $this->fieldAmount->SmallFieldHolder() . "</div>" .
"</div>"
);
"</div>";
}
/**

View File

@ -106,7 +106,7 @@ class NullableField extends FormField {
/**
* @param array $properties
*
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
if($this->isReadonly()) {
@ -117,12 +117,12 @@ class NullableField extends FormField {
$nullableCheckbox->setValue(is_null($this->dataValue()));
return DBField::create_field('HTMLText', sprintf(
return sprintf(
'%s %s&nbsp;<span>%s</span>',
$this->valueField->Field(),
$nullableCheckbox->Field(),
$this->getIsNullLabel()
));
);
}
/**

View File

@ -199,10 +199,10 @@ class NumericField_Readonly extends ReadonlyField {
* @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';
}
}

View File

@ -31,7 +31,7 @@ class PhoneNumberField extends FormField {
/**
* @param array $properties
* @return FieldGroup|HTMLText
* @return string
*/
public function Field($properties = array()) {
$fields = new FieldGroup( $this->name );
@ -60,14 +60,17 @@ class PhoneNumberField extends FormField {
}
$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 @@ class PhoneNumberField extends FormField {
$validator->validationError(
$this->name,
_t('PhoneNumberField.VALIDATION', "Please enter a valid phone number"),
"validation",
false
"validation"
);
return false;
}

View File

@ -40,7 +40,7 @@ class ReadonlyField extends FormField {
/**
* @param array $properties
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
// Include a hidden field in the HTML
@ -54,32 +54,6 @@ class ReadonlyField extends FormField {
}
}
/**
* If $dontEscape is true the returned value will be plain text
* and should be escaped in templates via .XML
*
* If $dontEscape is false the returned value will be safely encoded,
* but should not be escaped by the frontend.
*
* @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 '<i>'.Convert::raw2xml($value).'</i>';
}
}
}
public function getAttributes() {
return array_merge(
parent::getAttributes(),
@ -94,4 +68,52 @@ class ReadonlyField extends FormField {
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
*
* If $dontEscape is false the returned value will be safely encoded,
* but should not be escaped by the frontend.
*
* @return mixed|string
*/
public function Value() {
// Get raw value
$value = $this->dataValue();
if($value) {
return $value;
}
// "none" text
$label = _t('FormField.NONE', 'none');
return "<i>('{$label}')</i>";
}
/**
* 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'];
}
}

View File

@ -1,6 +1,8 @@
<?php
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
/**
* Represents a number of fields which are selectable by a radio
* button that appears at the beginning of each item. Using CSS, you can
@ -83,7 +85,7 @@ class SelectionGroup extends CompositeField {
$itemID = $this->ID() . '_' . (++$count);
$extra = array(
"RadioButton" => FormField::create_tag(
"RadioButton" => DBField::create_field('HTMLFragment', FormField::create_tag(
'input',
array(
'class' => 'selector',
@ -93,12 +95,12 @@ class SelectionGroup extends CompositeField {
'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);

View File

@ -26,7 +26,7 @@ class TextareaField extends FormField {
*/
private static $casting = array(
'Value' => 'Text',
'ValueEntities' => 'HTMLText',
'ValueEntities' => 'HTMLFragment',
);
/**
@ -119,8 +119,6 @@ class TextareaField extends FormField {
/**
* 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() {

View File

@ -35,8 +35,7 @@ class ToggleCompositeField extends CompositeField {
* @inheritdoc
*
* @param array $properties
*
* @return string|HTMLText
* @return string
*/
public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');

View File

@ -216,7 +216,7 @@ class TreeDropdownField extends FormField {
/**
* @param array $properties
* @return DBHTMLText
* @return string
*/
public function Field($properties = array()) {
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang');
@ -398,8 +398,9 @@ class TreeDropdownField extends FormField {
* 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;

View File

@ -5,7 +5,7 @@ use SilverStripe\ORM\SS_List;
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 @@ class GridField extends FormField {
* 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 @@ class GridField extends FormField {
$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 @@ class GridField extends FormField {
/**
* @param array $properties
*
* @return HTMLText
* @return string
*/
public function Field($properties = array()) {
$this->extend('onBeforeRender', $this);

View File

@ -1,10 +1,11 @@
<?php
use SilverStripe\Framework\Core\Extensible;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\FieldType\DBHTMLText;
/**
* Provides view and edit forms at GridField-specific URLs.
@ -603,7 +604,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
* Response object for this request after a successful save
*
* @param bool $isNewRecord True if this record was just created
* @return SS_HTTPResponse|HTMLText
* @return SS_HTTPResponse|DBHTMLText
*/
protected function redirectAfterSave($isNewRecord) {
$controller = $this->getToplevelController();

View File

@ -14,6 +14,10 @@ use SilverStripe\ORM\DataObject;
*/
class HTMLEditorField extends TextareaField {
private static $casting = [
'Value' => 'HTMLText',
];
/**
* Use TinyMCE's GZIP compressor
*
@ -128,10 +132,7 @@ class HTMLEditorField extends TextareaField {
* @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 @@ class HTMLEditorField extends TextareaField {
* @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 "<span class=\"readonly typography\" id=\"" . $this->id() . "\">"

View File

@ -1,6 +1,8 @@
<?php
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
require_once('HTML/HTMLBBCodeParser.php');
/*Seting up the PEAR bbcode parser*/
$config = parse_ini_file('BBCodeParser.ini', true);
@ -37,58 +39,6 @@ class BBCodeParser extends TextParser {
*/
private static $smilies_location = null;
/**
* @deprecated 4.0 Use the "BBCodeParser.smilies_location" config setting instead
*/
public static function smilies_location() {
Deprecation::notice('4.0', 'Use the "BBCodeParser.smilies_location" config setting instead');
if(!BBCodeParser::$smilies_location) {
return FRAMEWORK_DIR . '/images/smilies';
}
return static::config()->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 @@ class BBCodeParser extends TextParser {
* 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('&amp;', '&lt;', '&gt;'), $this->content);
@ -197,7 +147,9 @@ class BBCodeParser extends TextParser {
);
$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);
}
}

View File

@ -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 @@ abstract class TextParser extends Object {
/**
* 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();
}

View File

@ -9,8 +9,8 @@
<% loop $Entries %>
<item>
<title>$Title.XML</title>
<link>$AbsoluteLink</link>
<% if $Description %><description>$Description.AbsoluteLinks.XML</description><% end_if %>
<link>$AbsoluteLink.XML</link>
<% if $Description %><description>$Description.AbsoluteLinks.CDATA</description><% end_if %>
<% if $Date %><pubDate>$Date.Rfc822</pubDate>
<% else %><pubDate>$Created.Rfc822</pubDate><% end_if %>
<% if $Author %><dc:creator>$Author.XML</dc:creator><% end_if %>

View File

@ -64,7 +64,7 @@ class RSSFeedTest extends SapphireTest {
$this->assertContains('<title>ItemD</title>', $content);
$this->assertContains(
'<description>&lt;p&gt;ItemD Content test shortcode output&lt;/p&gt;</description>',
'<description><![CDATA[<p>ItemD Content test shortcode output</p>]]></description>',
$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';

View File

@ -165,7 +165,7 @@ class EmailTest extends SapphireTest {
'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 @@ class EmailTest extends SapphireTest {
$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-&gt;send()', $sent['content']);
$this->assertNull($sent['plaincontent']);
$this->assertEquals(
array(

View File

@ -35,10 +35,9 @@ class LookupFieldTest extends SapphireTest {
public function testUnknownStringValueWithNumericArraySource() {
$source = array(1 => 'one', 2 => 'two', 3 => 'three');
$f = new LookupField('test', 'test', $source);
$f->setValue('<ins>w00t</ins>');
$f->dontEscape = true; // simulates CMSMain->compareversions()
$f->setValue('w00t');
$this->assertEquals(
'<span class="readonly" id="test"><ins>w00t</ins></span><input type="hidden" name="test" value="" />',
'<span class="readonly" id="test">w00t</span><input type="hidden" name="test" value="" />',
trim($f->Field()->getValue())
);
}

View File

@ -211,7 +211,7 @@ class DBFieldTest extends SapphireTest {
public function testStringFieldsWithMultibyteData() {
$plainFields = array('Varchar', 'Text');
$htmlFields = array('HTMLVarchar', 'HTMLText');
$htmlFields = array('HTMLVarchar', 'HTMLText', 'HTMLFragment');
$allFields = array_merge($plainFields, $htmlFields);
$value = 'üåäöÜÅÄÖ';

View File

@ -185,14 +185,14 @@ class DBHTMLTextTest extends SapphireTest {
}
function testWhitelist() {
$textObj = new DBHTMLText('Test', 'meta,link');
$textObj = new DBHTMLText('Test', 'whitelist=meta,link');
$this->assertEquals(
'<meta content="Keep"><link href="Also Keep">',
$textObj->whitelistContent('<meta content="Keep"><p>Remove</p><link href="Also Keep" />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(
'<meta content="Keep"><link href="Also Keep">Keep Text',
$textObj->whitelistContent('<meta content="Keep"><p>Remove</p><link href="Also Keep" />Keep Text'),

View File

@ -236,7 +236,7 @@ class MemberTest extends FunctionalTest {
// 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.'&amp;t=[^"]+/');
}
/**

View File

@ -5,7 +5,7 @@ use SilverStripe\ORM\DataObject;
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 @@ after')
$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 = '<b>html</b>';
$this->assertEquals(
'<b>html</b>',
'&lt;b&gt;html&lt;/b&gt;',
$t = SSViewer::fromString('$UncastedValue')->process($vd)
);
$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
$vd->UncastedValue = '<b>html</b>';
$this->assertEquals(
'<b>html</b>',
$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
);
$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
$vd->UncastedValue = '<b>html</b>';
$this->assertEquals(
'&lt;b&gt;html&lt;/b&gt;',
$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
@ -1247,8 +1243,14 @@ after')
</html>');
$tmpl = new SSViewer($tmplFile);
$obj = new ViewableData();
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
$obj->InsertedLink = DBField::create_field(
'HTMLFragment',
'<a class="inserted" href="#anchor">InsertedLink</a>'
);
$obj->ExternalInsertedLink = DBField::create_field(
'HTMLFragment',
'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
);
$result = $tmpl->process($obj);
$this->assertContains(
'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
@ -1295,7 +1297,10 @@ after')
</html>');
$tmpl = new SSViewer($tmplFile);
$obj = new ViewableData();
$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
$obj->InsertedLink = DBField::create_field(
'HTMLFragment',
'<a class="inserted" href="#anchor">InsertedLink</a>'
);
$result = $tmpl->process($obj);
$code = <<<'EOC'
@ -1583,7 +1588,7 @@ class SSViewerTestFixture extends ViewableData {
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 @@ class SSViewerTestFixture extends ViewableData {
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')
);
}

View File

@ -1,4 +1,7 @@
<?php
use SilverStripe\Model\FieldType\DBField;
/**
* See {@link SSViewerTest->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 = "&quot;";
$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 @@ class ViewableDataTest extends SapphireTest {
$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('<foo>', $caster->XML_val('unsafeXML'));
// Test automatic escaping is applied even to fields with no 'casting'
$this->assertEquals('casted', $caster->XML_val('unsafeXML'));
$this->assertEquals('&lt;foo&gt;', $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 @@ class ViewableDataTest extends SapphireTest {
$this->assertEquals('SomeTitleValue', $obj->forTemplate());
}
public function testRAWVal() {
$data = new ViewableDataTest_Castable();
$data->test = 'This &amp; 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'), '&quot;this is a test&quot;');
}
public function testCastingClass() {
$expected = array(
'NonExistant' => null,
//'NonExistant' => null,
'Field' => 'CastingType',
'Argument' => 'ArgumentType',
'ArrayArgument' => 'ArrayArgumentType'
@ -149,7 +144,7 @@ class ViewableDataTest extends SapphireTest {
// 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';

View File

@ -74,9 +74,11 @@ class ArrayData extends ViewableData {
*
* @param string $field
* @param mixed $value
* @return $this
*/
public function setField($field, $value) {
$this->array[$field] = $value;
return $this;
}
/**

View File

@ -25,46 +25,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)
*
@ -120,7 +120,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
@ -195,7 +195,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
);
}
}
/* Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock |
OpenBlock | MalformedBlock | Injection | Text)+ */
protected $match_Template_typestack = array('Template');
@ -450,7 +450,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
/**
/**
* 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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
/**
* 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 @@ 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'])) . "'";
}
/* ComparisonOperator: "!=" | "==" | ">=" | ">" | "<=" | "<" | "=" */
protected $match_ComparisonOperator_typestack = array('ComparisonOperator');
function match_ComparisonOperator ($stack = array()) {
@ -1425,7 +1425,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']);
}
@ -1481,7 +1481,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
$res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');';
}
/* CacheBlockArgument:
!( "if " | "unless " )
(
(
:DollarMarkedLookup |
:QuotedString |
:Lookup
@ -1988,15 +1988,15 @@ 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'];
}
/* CacheBlockArguments: CacheBlockArgument ( < "," < CacheBlockArgument )* */
protected $match_CacheBlockArguments_typestack = array('CacheBlockArguments');
function match_CacheBlockArguments ($stack = array()) {
@ -2046,10 +2046,10 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
/* UncachedBlock:
/* UncachedBlock:
'<%' < "uncached" < CacheBlockArguments? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>'
Template:$TemplateMatcher?
'<%' < 'end_' ("uncached"|"cached"|"cacheblock") > '%>' */
@ -2425,7 +2425,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ 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'] .= '}';
}
/* 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 @@ 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) {
@ -3099,7 +3099,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$res['php'] .= "'$entity'";
}
}
function OldTPart_CallArguments(&$res, $sub) {
$res['php'] .= ',' . $sub['php'];
}
@ -3107,7 +3107,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
function OldTPart__finalise(&$res) {
$res['php'] .= ')';
}
/* OldTTag: "<%" < OldTPart > "%>" */
protected $match_OldTTag_typestack = array('OldTTag');
function match_OldTTag ($stack = array()) {
@ -3137,7 +3137,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$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 @@ class SSTemplateParser extends Parser implements TemplateParser {
function OldSprintfTag__construct(&$res) {
$res['php'] = "sprintf(";
}
function OldSprintfTag_OldTPart(&$res, $sub) {
$res['php'] .= $sub['php'];
}
@ -3199,7 +3199,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ 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
@ -3393,7 +3393,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
}
}
/* 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 @@ class SSTemplateParser extends Parser implements TemplateParser {
}
/* 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 @@ class SSTemplateParser extends Parser implements TemplateParser {
/**
* 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 @@ 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']);
@ -3747,13 +3747,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']);
}
@ -3771,19 +3771,19 @@ 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(); ';
}
/* OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>' */
protected $match_OpenBlock_typestack = array('OpenBlock');
function match_OpenBlock ($stack = array()) {
@ -3851,7 +3851,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ 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).');';
}
@ -3910,7 +3910,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ 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);
}
/* MalformedBlock: MalformedOpenTag | MalformedCloseTag */
protected $match_MalformedBlock_typestack = array('MalformedBlock');
function match_MalformedBlock ($stack = array()) {
@ -4187,7 +4187,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
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 @@ class SSTemplateParser extends Parser implements TemplateParser {
/**
* The TopTemplate also includes the opening stanza to start off the template
*/
@ -4671,13 +4671,13 @@ class SSTemplateParser extends Parser implements TemplateParser {
/**
* 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 @@ 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
@ -4719,13 +4719,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();
@ -4733,7 +4733,7 @@ EOC;
$result = $this->match_Template();
}
if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this);
// Get the result
$code = $result['php'];
}
@ -4741,8 +4741,8 @@ EOC;
// Include top level debugging comments if desired
if($includeDebuggingComments && $templateName && stripos($code, "<?xml") === false) {
$code = $this->includeDebuggingComments($code, $templateName);
}
}
return $code;
}
@ -4778,11 +4778,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

View File

@ -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, "<?xml") === false) {
$code = $this->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

View File

@ -1,11 +1,9 @@
<?php
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Permission;
/**
* This tracks the current scope for an SSViewer instance. It has three goals:
* - Handle entering & leaving sub-scopes in loops and withs
@ -100,12 +98,12 @@ class SSViewer_Scope {
$this->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 @@ class SSViewer_Scope {
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 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
* $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 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
}
}
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 @@ class SSViewer implements Flushable {
* @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 @@ class SSViewer implements Flushable {
}
}
return DBField::create_field('HTMLText', $output, null, array('shortcodes' => false));
return DBField::create_field('HTMLFragment', $output);
}
/**

View File

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

View File

@ -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();

View File

@ -1,6 +1,8 @@
<?php
use SilverStripe\Model\FieldType\DBVarchar;
use SilverStripe\Model\FieldType\DBField;
use SilverStripe\Model\FieldType\DBHTMLText;
/**
* A ViewableData object is any object that can be rendered into a template/view.
@ -65,28 +67,6 @@ class ViewableData extends Object implements IteratorAggregate {
// -----------------------------------------------------------------------------------------------------------------
/**
* Converts a field spec into an object creator. For example: "Int" becomes "new Int($fieldName);" and "Varchar(50)"
* becomes "new DBVarchar($fieldName, 50);".
*
* @param string $fieldSchema The field spec
* @return string
*/
public static function castingObjectCreator($fieldSchema) {
Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
}
/**
* Convert a field schema (e.g. "Varchar(50)") into a casting object creator array that contains both a className
* and castingHelper constructor code. See {@link castingObjectCreator} for more information about the constructor.
*
* @param string $fieldSchema
* @return array
*/
public static function castingObjectCreatorPair($fieldSchema) {
Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
}
// FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
/**
@ -114,6 +94,7 @@ class ViewableData extends Object implements IteratorAggregate {
} elseif($this->failover) {
return $this->failover->$property;
}
return null;
}
/**
@ -124,6 +105,7 @@ class ViewableData extends Object implements IteratorAggregate {
* @param mixed $value
*/
public function __set($property, $value) {
$this->objCacheClear();
if($this->hasMethod($method = "set$property")) {
$this->$method($value);
} else {
@ -180,9 +162,12 @@ class ViewableData extends Object implements IteratorAggregate {
*
* @param string $field
* @param mixed $value
* @return $this
*/
public function setField($field, $value) {
$this->objCacheClear();
$this->$field = $value;
return $this;
}
// -----------------------------------------------------------------------------------------------------------------
@ -190,11 +175,15 @@ class ViewableData extends Object implements IteratorAggregate {
/**
* 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 @@ class ViewableData extends Object implements IteratorAggregate {
// 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)")
* 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 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.
*
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
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 @@ class ViewableData extends Object implements IteratorAggregate {
*
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
*
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
*
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
*
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
* @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 @@ class ViewableData extends Object implements IteratorAggregate {
* @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 !== '<p></p>');
}
}
/**#@+
/**
* 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;
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 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));
}
/**
* 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 @@ class ViewableData extends Object implements IteratorAggregate {
* @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 @@ class ViewableData_Customised extends ViewableData {
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);
}
}