From e3afabeec22a3fcb5f3dd733acd06d1a0dea55a9 Mon Sep 17 00:00:00 2001
From: Damian Mooyman
Date: Fri, 3 Jun 2016 20:51:02 +1200
Subject: [PATCH] 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
---
ORM/Connect/DBSchemaManager.php | 40 +-
ORM/DB.php | 148 +------
ORM/DataObject.php | 3 +-
ORM/FieldType/DBBoolean.php | 3 -
ORM/FieldType/DBComposite.php | 6 +-
ORM/FieldType/DBDecimal.php | 3 -
ORM/FieldType/DBField.php | 123 +++++-
ORM/FieldType/DBHTMLText.php | 172 ++++++--
ORM/FieldType/DBHTMLVarchar.php | 74 +++-
ORM/FieldType/DBInt.php | 16 +-
ORM/FieldType/DBString.php | 77 ++--
ORM/FieldType/DBText.php | 295 ++++++-------
ORM/FieldType/DBVarchar.php | 6 +-
Security/CMSSecurity.php | 2 +-
Security/PermissionCheckboxSetField.php | 12 +-
_config/model.yml | 4 +
api/RSSFeed.php | 30 +-
api/XMLDataFormatter.php | 17 +-
core/CustomMethods.php | 48 +-
.../00_Model/04_Data_Types_and_Casting.md | 7 +-
.../01_Templates/09_Casting.md | 52 ++-
docs/en/04_Changelogs/4.0.0.md | 66 ++-
filesystem/File.php | 12 +-
filesystem/ImageManipulation.php | 2 +-
filesystem/storage/DBFile.php | 4 +-
forms/CheckboxField.php | 8 +-
forms/ConfirmedPasswordField.php | 2 +-
forms/CurrencyField.php | 13 +-
forms/DatalessField.php | 3 +-
forms/DatetimeField.php | 14 +-
forms/DropdownField.php | 2 +-
forms/FileField.php | 2 +-
forms/Form.php | 14 +-
forms/FormAction.php | 32 +-
forms/FormField.php | 55 +--
forms/HTMLReadonlyField.php | 12 +
forms/HiddenField.php | 3 +-
forms/InlineFormAction.php | 36 +-
forms/LiteralField.php | 5 +
forms/LookupField.php | 8 +-
forms/MoneyField.php | 7 +-
forms/NullableField.php | 6 +-
forms/NumericField.php | 8 +-
forms/PhoneNumberField.php | 12 +-
forms/ReadonlyField.php | 72 +--
forms/SelectionGroup.php | 10 +-
forms/TextareaField.php | 4 +-
forms/ToggleCompositeField.php | 3 +-
forms/TreeDropdownField.php | 7 +-
forms/gridfield/GridField.php | 15 +-
forms/gridfield/GridFieldDetailForm.php | 5 +-
forms/htmleditor/HTMLEditorField.php | 15 +-
parsers/BBCodeParser.php | 60 +--
parsers/TextParser.php | 9 +
templates/RSSFeed.ss | 4 +-
tests/api/RSSFeedTest.php | 4 +-
tests/email/EmailTest.php | 4 +-
tests/forms/LookupFieldTest.php | 5 +-
tests/model/DBFieldTest.php | 2 +-
tests/model/DBHTMLTextTest.php | 4 +-
tests/security/MemberTest.php | 2 +-
tests/view/SSViewerTest.php | 37 +-
tests/view/ViewableDataTest.php | 92 ++--
view/ArrayData.php | 2 +
view/SSTemplateParser.php | 206 ++++-----
view/SSTemplateParser.php.inc | 412 +++++++++---------
view/SSViewer.php | 28 +-
view/TemplateGlobalProvider.php | 2 +-
view/TemplateIteratorProvider.php | 2 +-
view/ViewableData.php | 260 ++++-------
70 files changed, 1447 insertions(+), 1283 deletions(-)
create mode 100644 forms/HTMLReadonlyField.php
diff --git a/ORM/Connect/DBSchemaManager.php b/ORM/Connect/DBSchemaManager.php
index bc390e1efc4..7fb1fbfeded 100644
--- a/ORM/Connect/DBSchemaManager.php
+++ b/ORM/Connect/DBSchemaManager.php
@@ -7,6 +7,9 @@
use Object;
use Director;
use SilverStripe\ORM\FieldType\DBPrimaryKey;
+use SilverStripe\ORM\FieldType\DBField;
+use SilverStripe\ORM\FieldType\DBPrimaryKey;
+
/**
* Represents and handles all schema management for a database
@@ -110,8 +113,7 @@ public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
/**
* Initiates a schema update within a single callback
*
- * @var callable $callback
- * @throws Exception
+ * @param callable $callback
*/
public function schemaUpdate($callback) {
// Begin schema update
@@ -153,15 +155,10 @@ public function schemaUpdate($callback) {
break;
}
}
- } catch(Exception $ex) {
- $error = $ex;
- }
- // finally {
+ } finally {
$this->schemaUpdateTransaction = null;
$this->schemaIsUpdating = false;
- // }
-
- if($error) throw $error;
+ }
}
/**
@@ -303,7 +300,7 @@ protected function transInitTable($table) {
* - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
* control over the index.
* @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
- * @param string|array $options SQL statement to append to the CREATE TABLE call.
+ * @param array $options Create table options (ENGINE, etc.)
* @param array|bool $extensions List of extensions
*/
public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
@@ -314,7 +311,7 @@ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $
$this->alterationMessage("Table $table: created", "created");
} else {
if (Config::inst()->get('SilverStripe\ORM\Connect\DBSchemaManager', 'check_and_repair_on_build')) {
- $this->checkAndRepairTable($table, $options);
+ $this->checkAndRepairTable($table);
}
// Check if options changed
@@ -353,12 +350,14 @@ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $
$fieldSpec = substr($fieldSpec, 0, $pos);
}
+ /** @var DBField $fieldObj */
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
- $fieldObj->arrayValue = $arrayValue;
+ $fieldObj->setArrayValue($arrayValue);
$fieldObj->setTable($table);
if($fieldObj instanceof DBPrimaryKey) {
+ /** @var DBPrimaryKey $fieldObj */
$fieldObj->setAutoIncrement($hasAutoIncPK);
}
@@ -383,7 +382,7 @@ public function dontRequireTable($table) {
$suffix = '';
while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
$suffix = $suffix
- ? ($suffix + 1)
+ ? ((int)$suffix + 1)
: 2;
}
$this->renameTable($table, "_obsolete_{$table}$suffix");
@@ -414,6 +413,8 @@ public function requireIndex($table, $index, $spec) {
$specString = $this->convertIndexSpec($spec);
// Check existing index
+ $oldSpecString = null;
+ $indexKey = null;
if (!$newTable) {
$indexKey = $this->indexKey($table, $index, $spec);
$indexList = $this->indexList($table);
@@ -502,6 +503,7 @@ protected function determineIndexType($spec) {
* Converts an array or string index spec into a universally useful array
*
* @see convertIndexSpec() for approximate inverse
+ * @param string $name Index name
* @param string|array $spec
* @return array The resulting spec array with the required fields name, type, and value
*/
@@ -544,7 +546,9 @@ protected function parseIndexSpec($name, $spec) {
*/
protected function convertIndexSpec($indexSpec) {
// Return already converted spec
- if (!is_array($indexSpec)) return $indexSpec;
+ if (!is_array($indexSpec)) {
+ return $indexSpec;
+ }
// Combine elements into standard string format
return "{$indexSpec['type']} ({$indexSpec['value']})";
@@ -647,7 +651,7 @@ public function requireField($table, $field, $spec) {
$new = preg_split("/'\s*,\s*'/", $newStr);
$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue);
- $old = preg_split("/'\s*,\s*'/", $newStr);
+ $old = preg_split("/'\s*,\s*'/", $oldStr);
$holder = array();
foreach ($old as $check) {
@@ -690,7 +694,7 @@ public function dontRequireField($table, $fieldName) {
$suffix = '';
while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
$suffix = $suffix
- ? ($suffix + 1)
+ ? ((int)$suffix + 1)
: 2;
}
$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
@@ -942,10 +946,10 @@ abstract public function fieldList($table);
* This allows the cached values for a table's field list to be erased.
* If $tablename is empty, then the whole cache is erased.
*
- * @param string|bool $tableName
+ * @param string $tableName
* @return boolean
*/
- public function clearCachedFieldlist($tableName = false) {
+ public function clearCachedFieldlist($tableName = null) {
return true;
}
diff --git a/ORM/DB.php b/ORM/DB.php
index 137d9648b7b..963df4c5685 100644
--- a/ORM/DB.php
+++ b/ORM/DB.php
@@ -65,14 +65,6 @@ public static function set_conn(SS_Database $connection, $name = 'default') {
self::$connections[$name] = $connection;
}
- /**
- * @deprecated since version 4.0 Use DB::set_conn instead
- */
- public static function setConn(SS_Database $connection, $name = 'default') {
- Deprecation::notice('4.0', 'Use DB::set_conn instead');
- self::set_conn($connection, $name);
- }
-
/**
* Get the global database connection.
*
@@ -103,7 +95,10 @@ public static function getConn($name = 'default') {
*/
public static function get_schema($name = 'default') {
$connection = self::get_conn($name);
- if($connection) return $connection->getSchemaManager();
+ if($connection) {
+ return $connection->getSchemaManager();
+ }
+ return null;
}
/**
@@ -134,7 +129,10 @@ public static function build_sql(SQLExpression $expression, &$parameters, $name
*/
public static function get_connector($name = 'default') {
$connection = self::get_conn($name);
- if($connection) return $connection->getConnector();
+ if($connection) {
+ return $connection->getConnector();
+ }
+ return null;
}
/**
@@ -273,13 +271,6 @@ public static function connection_attempted() {
return self::$connection_attempted;
}
- /**
- * @deprecated since version 4.0 DB::getConnect was never implemented and is obsolete
- */
- public static function getConnect($parameters) {
- Deprecation::notice('4.0', 'DB::getConnect was never implemented and is obsolete');
- }
-
/**
* Execute the given SQL query.
* @param string $sql The SQL query to execute
@@ -371,7 +362,7 @@ public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ER
*/
public static function manipulate($manipulation) {
self::$lastQuery = $manipulation;
- return self::get_conn()->manipulate($manipulation);
+ self::get_conn()->manipulate($manipulation);
}
/**
@@ -384,14 +375,6 @@ public static function get_generated_id($table) {
return self::get_conn()->getGeneratedID($table);
}
- /**
- * @deprecated since version 4.0 Use DB::get_generated_id instead
- */
- public static function getGeneratedID($table) {
- Deprecation::notice('4.0', 'Use DB::get_generated_id instead');
- return self::get_generated_id($table);
- }
-
/**
* Check if the connection to the database is active.
*
@@ -401,14 +384,6 @@ public static function is_active() {
return ($conn = self::get_conn()) && $conn->isActive();
}
- /**
- * @deprecated since version 4.0 Use DB::is_active instead
- */
- public static function isActive() {
- Deprecation::notice('4.0', 'Use DB::is_active instead');
- return self::is_active();
- }
-
/**
* Create the database and connect to it. This can be called if the
* initial database connection is not successful because the database
@@ -421,14 +396,6 @@ public static function create_database($database) {
return self::get_conn()->selectDatabase($database, true);
}
- /**
- * @deprecated since version 4.0 Use DB::create_database instead
- */
- public static function createDatabase($connect, $username, $password, $database) {
- Deprecation::notice('4.0', 'Use DB::create_database instead');
- return self::create_database($database);
- }
-
/**
* Create a new table.
* @param string $table The name of the table
@@ -438,7 +405,7 @@ public static function createDatabase($connect, $username, $password, $database)
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
* for MySQL.
* - 'temporary' - If true, then a temporary table will be created
- * @param array $advancedOptions
+ * @param array $advancedOptions Advanced creation options
* @return string The table name generated. This may be different from the table name, for example with
* temporary tables.
*/
@@ -448,14 +415,6 @@ public static function create_table($table, $fields = null, $indexes = null, $op
return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
}
- /**
- * @deprecated since version 4.0 Use DB::create_table instead
- */
- public static function createTable($table, $fields = null, $indexes = null, $options = null) {
- Deprecation::notice('4.0', 'Use DB::create_table instead');
- return self::create_table($table, $fields, $indexes, $options);
- }
-
/**
* Create a new field on a table.
* @param string $table Name of the table.
@@ -466,14 +425,6 @@ public static function create_field($table, $field, $spec) {
return self::get_schema()->createField($table, $field, $spec);
}
- /**
- * @deprecated since version 4.0 Use DB::create_field instead
- */
- public static function createField($table, $field, $spec) {
- Deprecation::notice('4.0', 'Use DB::create_field instead');
- return self::create_field($table, $field, $spec);
- }
-
/**
* Generate the following table in the database, modifying whatever already exists
* as necessary.
@@ -492,18 +443,7 @@ public static function createField($table, $field, $spec) {
public static function require_table($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
$options = null, $extensions = null
) {
- return self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options,
- $extensions);
- }
-
- /**
- * @deprecated since version 4.0 Use DB::require_table instead
- */
- public static function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
- $options = null, $extensions = null
- ) {
- Deprecation::notice('4.0', 'Use DB::require_table instead');
- return self::require_table($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
+ self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
}
/**
@@ -514,15 +454,7 @@ public static function requireTable($table, $fieldSchema = null, $indexSchema =
* @param string $spec The field specification.
*/
public static function require_field($table, $field, $spec) {
- return self::get_schema()->requireField($table, $field, $spec);
- }
-
- /**
- * @deprecated since version 4.0 Use DB::require_field instead
- */
- public static function requireField($table, $field, $spec) {
- Deprecation::notice('4.0', 'Use DB::require_field instead');
- return self::require_field($table, $field, $spec);
+ self::get_schema()->requireField($table, $field, $spec);
}
/**
@@ -536,14 +468,6 @@ public static function require_index($table, $index, $spec) {
self::get_schema()->requireIndex($table, $index, $spec);
}
- /**
- * @deprecated since version 4.0 Use DB::require_index instead
- */
- public static function requireIndex($table, $index, $spec) {
- Deprecation::notice('4.0', 'Use DB::require_index instead');
- self::require_index($table, $index, $spec);
- }
-
/**
* If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
*
@@ -553,14 +477,6 @@ public static function dont_require_table($table) {
self::get_schema()->dontRequireTable($table);
}
- /**
- * @deprecated since version 4.0 Use DB::dont_require_table instead
- */
- public static function dontRequireTable($table) {
- Deprecation::notice('4.0', 'Use DB::dont_require_table instead');
- self::dont_require_table($table);
- }
-
/**
* See {@link SS_Database->dontRequireField()}.
*
@@ -571,14 +487,6 @@ public static function dont_require_field($table, $fieldName) {
self::get_schema()->dontRequireField($table, $fieldName);
}
- /**
- * @deprecated since version 4.0 Use DB::dont_require_field instead
- */
- public static function dontRequireField($table, $fieldName) {
- Deprecation::notice('4.0', 'Use DB::dont_require_field instead');
- self::dont_require_field($table, $fieldName);
- }
-
/**
* Checks a table's integrity and repairs it if necessary.
*
@@ -589,14 +497,6 @@ public static function check_and_repair_table($table) {
return self::get_schema()->checkAndRepairTable($table);
}
- /**
- * @deprecated since version 4.0 Use DB::check_and_repair_table instead
- */
- public static function checkAndRepairTable($table) {
- Deprecation::notice('4.0', 'Use DB::check_and_repair_table instead');
- self::check_and_repair_table($table);
- }
-
/**
* Return the number of rows affected by the previous operation.
*
@@ -606,14 +506,6 @@ public static function affected_rows() {
return self::get_conn()->affectedRows();
}
- /**
- * @deprecated since version 4.0 Use DB::affected_rows instead
- */
- public static function affectedRows() {
- Deprecation::notice('4.0', 'Use DB::affected_rows instead');
- return self::affected_rows();
- }
-
/**
* Returns a list of all tables in the database.
* The table names will be in lower case.
@@ -624,14 +516,6 @@ public static function table_list() {
return self::get_schema()->tableList();
}
- /**
- * @deprecated since version 4.0 Use DB::table_list instead
- */
- public static function tableList() {
- Deprecation::notice('4.0', 'Use DB::table_list instead');
- return self::table_list();
- }
-
/**
* Get a list of all the fields for the given table.
* Returns a map of field name => field spec.
@@ -643,14 +527,6 @@ public static function field_list($table) {
return self::get_schema()->fieldList($table);
}
- /**
- * @deprecated since version 4.0 Use DB::field_list instead
- */
- public static function fieldList($table) {
- Deprecation::notice('4.0', 'Use DB::field_list instead');
- return self::field_list($table);
- }
-
/**
* Enable supression of database messages.
*/
diff --git a/ORM/DataObject.php b/ORM/DataObject.php
index f6e4423cd1b..1cbe4b803bb 100644
--- a/ORM/DataObject.php
+++ b/ORM/DataObject.php
@@ -2598,9 +2598,10 @@ public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
*
* @param string $fieldName Name of the field
* @param mixed $val New field value
- * @return DataObject $this
+ * @return $this
*/
public function setField($fieldName, $val) {
+ $this->objCacheClear();
//if it's a has_one component, destroy the cache
if (substr($fieldName, -2) == 'ID') {
unset($this->components[substr($fieldName, 0, -2)]);
diff --git a/ORM/FieldType/DBBoolean.php b/ORM/FieldType/DBBoolean.php
index 077b1ea628f..406204ed9bc 100644
--- a/ORM/FieldType/DBBoolean.php
+++ b/ORM/FieldType/DBBoolean.php
@@ -42,9 +42,6 @@ public function NiceAsBoolean() {
return ($this->value) ? 'true' : 'false';
}
- /**
- * Saves this field to the given data object.
- */
public function saveInto($dataObject) {
$fieldName = $this->name;
if($fieldName) {
diff --git a/ORM/FieldType/DBComposite.php b/ORM/FieldType/DBComposite.php
index ecb9584d20b..e4a0199a45b 100644
--- a/ORM/FieldType/DBComposite.php
+++ b/ORM/FieldType/DBComposite.php
@@ -225,11 +225,14 @@ public function hasField($field) {
* @param string $field
* @param mixed $value
* @param bool $markChanged
+ * @return $this
*/
public function setField($field, $value, $markChanged = true) {
+ $this->objCacheClear();
+
// Skip non-db fields
if(!$this->hasField($field)) {
- return;
+ return $this;
}
// Set changed
@@ -246,6 +249,7 @@ public function setField($field, $value, $markChanged = true) {
// Set local record
$this->record[$field] = $value;
+ return $this;
}
/**
diff --git a/ORM/FieldType/DBDecimal.php b/ORM/FieldType/DBDecimal.php
index 79476e4948f..1f89bdb1470 100644
--- a/ORM/FieldType/DBDecimal.php
+++ b/ORM/FieldType/DBDecimal.php
@@ -63,9 +63,6 @@ public function requireField() {
DB::require_field($this->tableName, $this->name, $values);
}
- /**
- * @param DataObject $dataObject
- */
public function saveInto($dataObject) {
$fieldName = $this->name;
diff --git a/ORM/FieldType/DBField.php b/ORM/FieldType/DBField.php
index 43ff6e8884a..fe400324919 100644
--- a/ORM/FieldType/DBField.php
+++ b/ORM/FieldType/DBField.php
@@ -48,12 +48,32 @@
*/
abstract class DBField extends ViewableData {
+ /**
+ * Raw value of this field
+ *
+ * @var mixed
+ */
protected $value;
+ /**
+ * Table this field belongs to
+ *
+ * @var string
+ */
protected $tableName;
+ /**
+ * Name of this field
+ *
+ * @var string
+ */
protected $name;
+ /**
+ * Used for generating DB schema. {@see DBSchemaManager}
+ *
+ * @var array
+ */
protected $arrayValue;
/**
@@ -72,6 +92,19 @@ abstract class DBField extends ViewableData {
*/
private static $default_search_filter_class = 'PartialMatchFilter';
+ private static $casting = array(
+ 'ATT' => 'HTMLFragment',
+ 'CDATA' => 'HTMLFragment',
+ 'HTML' => 'HTMLFragment',
+ 'HTMLATT' => 'HTMLFragment',
+ 'JS' => 'HTMLFragment',
+ 'RAW' => 'HTMLFragment',
+ 'RAWURLATT' => 'HTMLFragment',
+ 'URLATT' => 'HTMLFragment',
+ 'XML' => 'HTMLFragment',
+ 'ProcessedRAW' => 'HTMLFragment',
+ );
+
/**
* @var $default mixed Default-value in the database.
* Might be overridden on DataObject-level, but still useful for setting defaults on
@@ -97,6 +130,7 @@ public function __construct($name = null) {
* @return DBField
*/
public static function create_field($className, $value, $name = null, $object = null) {
+ /** @var DBField $dbField */
$dbField = Object::create($className, $name, $object);
$dbField->setValue($value, null, false);
@@ -249,52 +283,105 @@ public function getTable() {
}
/**
+ * Determine 'default' casting for this field.
+ *
* @return string
*/
public function forTemplate() {
- return $this->XML();
+ return Convert::raw2xml($this->getValue());
}
+ /**
+ * Gets the value appropriate for a HTML attribute string
+ *
+ * @return string
+ */
public function HTMLATT() {
return Convert::raw2htmlatt($this->RAW());
}
+ /**
+ * urlencode this string
+ *
+ * @return string
+ */
public function URLATT() {
return urlencode($this->RAW());
}
+ /**
+ * rawurlencode this string
+ *
+ * @return string
+ */
public function RAWURLATT() {
return rawurlencode($this->RAW());
}
+ /**
+ * Gets the value appropriate for a HTML attribute string
+ *
+ * @return string
+ */
public function ATT() {
return Convert::raw2att($this->RAW());
}
+ /**
+ * Gets the raw value for this field.
+ * Note: Skips processors implemented via forTemplate()
+ *
+ * @return mixed
+ */
public function RAW() {
- return $this->value;
+ return $this->getValue();
}
+ /**
+ * Gets javascript string literal value
+ *
+ * @return string
+ */
public function JS() {
return Convert::raw2js($this->RAW());
}
/**
* Return JSON encoded value
+ *
* @return string
*/
public function JSON() {
return Convert::raw2json($this->RAW());
}
+ /**
+ * Alias for {@see XML()}
+ *
+ * @return string
+ */
public function HTML(){
- return Convert::raw2xml($this->RAW());
+ return $this->XML();
}
+ /**
+ * XML encode this value
+ *
+ * @return string
+ */
public function XML(){
return Convert::raw2xml($this->RAW());
}
+ /**
+ * Safely escape for XML string
+ *
+ * @return string
+ */
+ public function CDATA() {
+ return $this->forTemplate();
+ }
+
/**
* Returns the value to be set in the database to blank this field.
* Usually it's a choice between null, 0, and ''
@@ -307,14 +394,15 @@ public function nullValue() {
/**
* Saves this field to the given data object.
+ *
+ * @param DataObject $dataObject
*/
public function saveInto($dataObject) {
$fieldName = $this->name;
- if($fieldName) {
- $dataObject->$fieldName = $this->value;
- } else {
- user_error("DBField::saveInto() Called on a nameless '" . get_class($this) . "' object", E_USER_ERROR);
+ if(empty($fieldName)) {
+ throw new \BadMethodCallException("DBField::saveInto() Called on a nameless '" . get_class($this) . "' object");
}
+ $dataObject->$fieldName = $this->value;
}
/**
@@ -353,9 +441,10 @@ public function scaffoldSearchField($title = null) {
* won't work)
*
* @param string|bool $name
+ * @param string $name Override name of this field
* @return SearchFilter
*/
- public function defaultSearchFilter($name = false) {
+ public function defaultSearchFilter($name = null) {
$name = ($name) ? $name : $this->name;
$filterClass = $this->stat('default_search_filter_class');
return new $filterClass($name);
@@ -377,6 +466,22 @@ public function debug() {
}
public function __toString() {
- return $this->forTemplate();
+ return (string)$this->forTemplate();
+ }
+
+ /**
+ * @return array
+ */
+ public function getArrayValue() {
+ return $this->arrayValue;
+ }
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function setArrayValue($value) {
+ $this->arrayValue = $value;
+ return $this;
}
}
diff --git a/ORM/FieldType/DBHTMLText.php b/ORM/FieldType/DBHTMLText.php
index 73efceb1f71..43ad94eb785 100644
--- a/ORM/FieldType/DBHTMLText.php
+++ b/ORM/FieldType/DBHTMLText.php
@@ -14,6 +14,12 @@
* Represents a large text field that contains HTML content.
* This behaves similarly to {@link Text}, but the template processor won't escape any HTML content within it.
*
+ * Options can be specified in a $db config via one of the following:
+ * - "HTMLFragment(['shortcodes=true', 'whitelist=meta,link'])"
+ * - "HTMLFragment('whitelist=meta,link')"
+ * - "HTMLFragment(['shortcodes=true'])". "HTMLText" is also a synonym for this.
+ * - "HTMLFragment('shortcodes=true')"
+ *
* @see HTMLVarchar
* @see Text
* @see Varchar
@@ -25,34 +31,78 @@ class DBHTMLText extends DBText {
private static $escape_type = 'xml';
private static $casting = array(
- "AbsoluteLinks" => "HTMLText",
- "BigSummary" => "HTMLText",
- "ContextSummary" => "HTMLText",
- "FirstParagraph" => "HTMLText",
- "FirstSentence" => "HTMLText",
- "LimitCharacters" => "HTMLText",
- "LimitSentences" => "HTMLText",
- "Lower" => "HTMLText",
- "LowerCase" => "HTMLText",
- "Summary" => "HTMLText",
- "Upper" => "HTMLText",
- "UpperCase" => "HTMLText",
- 'EscapeXML' => 'HTMLText',
- 'LimitWordCount' => 'HTMLText',
- 'LimitWordCountXML' => 'HTMLText',
- 'NoHTML' => 'Text',
+ "AbsoluteLinks" => "HTMLFragment",
+ // DBText summary methods - override to HTMLFragment
+ "BigSummary" => "HTMLFragment",
+ "ContextSummary" => "HTMLFragment", // Same as DBText
+ "FirstParagraph" => "HTMLFragment",
+ "FirstSentence" => "HTMLFragment",
+ "LimitSentences" => "HTMLFragment",
+ "Summary" => "HTMLFragment",
+ // DBString conversion / summary methods - override to HTMLFragment
+ "LimitCharacters" => "HTMLFragment",
+ "LimitCharactersToClosestWord" => "HTMLFragment",
+ "LimitWordCount" => "HTMLFragment",
+ "LowerCase" => "HTMLFragment",
+ "UpperCase" => "HTMLFragment",
+ "NoHTML" => "Text", // Actually stays same as DBString cast
);
- protected $processShortcodes = true;
+ /**
+ * Enable shortcode parsing on this field
+ *
+ * @var bool
+ */
+ protected $processShortcodes = false;
- protected $whitelist = false;
+ /**
+ * Check if shortcodes are enabled
+ *
+ * @return bool
+ */
+ public function getProcessShortcodes() {
+ return $this->processShortcodes;
+ }
- public function __construct($name = null, $options = array()) {
- if(is_string($options)) {
- $options = array('whitelist' => $options);
- }
+ /**
+ * Set shortcodes on or off by default
+ *
+ * @param bool $process
+ * @return $this
+ */
+ public function setProcessShortcodes($process) {
+ $this->processShortcodes = (bool)$process;
+ return $this;
+ }
- return parent::__construct($name, $options);
+ /**
+ * List of html properties to whitelist
+ *
+ * @var array
+ */
+ protected $whitelist = [];
+
+ /**
+ * List of html properties to whitelist
+ *
+ * @return array
+ */
+ public function getWhitelist() {
+ return $this->whitelist;
+ }
+
+ /**
+ * Set list of html properties to whitelist
+ *
+ * @param array $whitelist
+ * @return $this
+ */
+ public function setWhitelist($whitelist) {
+ if(!is_array($whitelist)) {
+ $whitelist = preg_split('/\s*,\s*/', $whitelist);
+ }
+ $this->whitelist = $whitelist;
+ return $this;
}
/**
@@ -69,24 +119,52 @@ public function __construct($name = null, $options = array()) {
* Text nodes outside of HTML tags are filtered out by default, but may be included by adding
* the text() directive. E.g. 'link,meta,text()' will allow only and text at
* the root level.
+ *
+ * @return $this
*/
public function setOptions(array $options = array()) {
- parent::setOptions($options);
-
if(array_key_exists("shortcodes", $options)) {
- $this->processShortcodes = !!$options["shortcodes"];
+ $this->setProcessShortcodes(!!$options["shortcodes"]);
}
if(array_key_exists("whitelist", $options)) {
- if(is_array($options['whitelist'])) {
- $this->whitelist = $options['whitelist'];
- }
- else {
- $this->whitelist = preg_split('/,\s*/', $options['whitelist']);
- }
+ $this->setWhitelist($options['whitelist']);
}
+
+ return parent::setOptions($options);
+ }
+
+ public function LimitSentences($maxSentences = 2)
+ {
+ // @todo
+ return parent::LimitSentences($maxSentences);
+ }
+
+ public function LimitWordCount($numWords = 26, $add = '...')
+ {
+ // @todo
+ return parent::LimitWordCount($numWords, $add);
+ }
+
+ public function LimitCharacters($limit = 20, $add = '...')
+ {
+ // @todo
+ return parent::LimitCharacters($limit, $add);
+ }
+
+ public function LimitCharactersToClosestWord($limit = 20, $add = '...')
+ {
+ // @todo
+ return parent::LimitCharactersToClosestWord($limit, $add);
}
+ public function BigSummary($maxWords = 50)
+ {
+ // @todo
+ return parent::BigSummary($maxWords); // TODO: Change the autogenerated stub
+ }
+
+
/**
* Create a summary of the content. This will be some section of the first paragraph, limited by
* $maxWords. All internal tags are stripped out - the return value is a string
@@ -161,6 +239,11 @@ public function Summary($maxWords = 50, $flex = 15, $add = '...') {
return implode(' ', array_slice($words, 0, $maxWords)) . $add;
}
+ public function FirstParagraph() {
+ // @todo implement
+ return parent::FirstParagraph();
+ }
+
/**
* Returns the first sentence from the first paragraph. If it can't figure out what the first paragraph is (or
* there isn't one), it returns the same as Summary()
@@ -207,6 +290,18 @@ public function forTemplate() {
return $this->RAW();
}
+ /**
+ * Safely escape for XML string
+ *
+ * @return string
+ */
+ public function CDATA() {
+ return sprintf(
+ '',
+ str_replace(']]>', ']]]]>', $this->RAW())
+ );
+ }
+
public function prepValueForDB($value) {
return parent::prepValueForDB($this->whitelistContent($value));
}
@@ -277,6 +372,15 @@ public function scaffoldSearchField($title = null, $params = null) {
return new TextField($this->name, $title);
}
-}
-
+ /**
+ * @return string
+ */
+ public function NoHTML()
+ {
+ // Preserve line breaks
+ $text = preg_replace('/\ /i', "\n", $this->RAW());
+ // Convert back to plain text
+ return \Convert::xml2raw(strip_tags($text));
+ }
+}
diff --git a/ORM/FieldType/DBHTMLVarchar.php b/ORM/FieldType/DBHTMLVarchar.php
index f01f357ee2a..c8affc3c196 100644
--- a/ORM/FieldType/DBHTMLVarchar.php
+++ b/ORM/FieldType/DBHTMLVarchar.php
@@ -17,14 +17,55 @@ class DBHTMLVarchar extends DBVarchar {
private static $escape_type = 'xml';
- protected $processShortcodes = true;
+ /**
+ * Enable shortcode parsing on this field
+ *
+ * @var bool
+ */
+ protected $processShortcodes = false;
- public function setOptions(array $options = array()) {
- parent::setOptions($options);
+ /**
+ * Check if shortcodes are enabled
+ *
+ * @return bool
+ */
+ public function getProcessShortcodes() {
+ return $this->processShortcodes;
+ }
+ /**
+ * Set shortcodes on or off by default
+ *
+ * @param bool $process
+ * @return $this
+ */
+ public function setProcessShortcodes($process) {
+ $this->processShortcodes = (bool)$process;
+ return $this;
+ }
+ /**
+ * @param array $options
+ *
+ * Options accepted in addition to those provided by Text:
+ *
+ * - shortcodes: If true, shortcodes will be turned into the appropriate HTML.
+ * If false, shortcodes will not be processed.
+ *
+ * - whitelist: If provided, a comma-separated list of elements that will be allowed to be stored
+ * (be careful on relying on this for XSS protection - some seemingly-safe elements allow
+ * attributes that can be exploited, for instance )
+ * Text nodes outside of HTML tags are filtered out by default, but may be included by adding
+ * the text() directive. E.g. 'link,meta,text()' will allow only and text at
+ * the root level.
+ *
+ * @return $this
+ */
+ public function setOptions(array $options = array()) {
if(array_key_exists("shortcodes", $options)) {
- $this->processShortcodes = !!$options["shortcodes"];
+ $this->setProcessShortcodes(!!$options["shortcodes"]);
}
+
+ return parent::setOptions($options);
}
public function forTemplate() {
@@ -34,11 +75,21 @@ public function forTemplate() {
public function RAW() {
if ($this->processShortcodes) {
return ShortcodeParser::get_active()->parse($this->value);
- }
- else {
+ } else {
return $this->value;
}
+ }
+ /**
+ * Safely escape for XML string
+ *
+ * @return string
+ */
+ public function CDATA() {
+ return sprintf(
+ '',
+ str_replace(']]>', ']]]]>', $this->forTemplate())
+ );
}
public function exists() {
@@ -53,4 +104,15 @@ public function scaffoldSearchField($title = null) {
return new TextField($this->name, $title);
}
+ /**
+ * @return string
+ */
+ public function NoHTML()
+ {
+ // Preserve line breaks
+ $text = preg_replace('/\ /i', "\n", $this->RAW());
+ // Convert back to plain text
+ return \Convert::xml2raw(strip_tags($text));
+ }
+
}
diff --git a/ORM/FieldType/DBInt.php b/ORM/FieldType/DBInt.php
index 30f43ae67cd..423a1f84905 100644
--- a/ORM/FieldType/DBInt.php
+++ b/ORM/FieldType/DBInt.php
@@ -29,14 +29,14 @@ public function Formatted() {
}
public function requireField() {
- $parts=Array(
- 'datatype'=>'int',
- 'precision'=>11,
- 'null'=>'not null',
- 'default'=>$this->defaultVal,
- 'arrayValue'=>$this->arrayValue);
-
- $values=Array('type'=>'int', 'parts'=>$parts);
+ $parts = [
+ 'datatype' => 'int',
+ 'precision' => 11,
+ 'null' => 'not null',
+ 'default' => $this->defaultVal,
+ 'arrayValue' => $this->arrayValue
+ ];
+ $values = ['type' => 'int', 'parts' => $parts];
DB::require_field($this->tableName, $this->name, $values);
}
diff --git a/ORM/FieldType/DBString.php b/ORM/FieldType/DBString.php
index a28e1fcfff4..dda71107aac 100644
--- a/ORM/FieldType/DBString.php
+++ b/ORM/FieldType/DBString.php
@@ -24,7 +24,6 @@ abstract class DBString extends DBField {
"LimitCharacters" => "Text",
"LimitCharactersToClosestWord" => "Text",
'LimitWordCount' => 'Text',
- 'LimitWordCountXML' => 'HTMLText',
"LowerCase" => "Text",
"UpperCase" => "Text",
'NoHTML' => 'Text',
@@ -33,34 +32,70 @@ abstract class DBString extends DBField {
/**
* Construct a string type field with a set of optional parameters.
*
- * @param $name string The name of the field
- * @param $options array An array of options e.g. array('nullifyEmpty'=>false). See
+ * @param string $name string The name of the field
+ * @param array $options array An array of options e.g. array('nullifyEmpty'=>false). See
* {@link StringField::setOptions()} for information on the available options
*/
public function __construct($name = null, $options = array()) {
- // Workaround: The singleton pattern calls this constructor with true/1 as the second parameter, so we
- // must ignore it
- if(is_array($options)){
+ $options = $this->parseConstructorOptions($options);
+ if($options) {
$this->setOptions($options);
}
parent::__construct($name);
}
+ /**
+ * Parses the "options" parameter passed to the constructor. This could be a
+ * string value, or an array of options. Config specification might also
+ * encode "key=value" pairs in non-associative strings.
+ *
+ * @param mixed $options
+ * @return array The list of parsed options, or empty if there are none
+ */
+ protected function parseConstructorOptions($options) {
+ if(is_string($options)) {
+ $options = [$options];
+ }
+ if(!is_array($options)) {
+ return [];
+ }
+ $parsed = [];
+ foreach($options as $option => $value) {
+ // Workaround for inability for config args to support associative arrays
+ if(is_numeric($option) && strpos($value, '=') !== false) {
+ list($option, $value) = explode('=', $value);
+ $option = trim($option);
+ $value = trim($value);
+ }
+ // Convert bool values
+ if(strcasecmp($value, 'true') === 0) {
+ $value = true;
+ } elseif(strcasecmp($value, 'false') === 0) {
+ $value = false;
+ }
+ $parsed[$option] = $value;
+ }
+ return $parsed;
+ }
+
/**
* Update the optional parameters for this field.
- * @param array $options array of options
+ *
+ * @param array $options Array of options
* The options allowed are:
*
"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.
*
+ * @return $this
*/
public function setOptions(array $options = array()) {
if(array_key_exists("nullifyEmpty", $options)) {
$this->nullifyEmpty = $options["nullifyEmpty"] ? true : false;
}
+ return $this;
}
/**
@@ -110,7 +145,7 @@ public function prepValueForDB($value) {
* @return string
*/
public function forTemplate() {
- return nl2br($this->XML());
+ return nl2br(parent::forTemplate());
}
/**
@@ -170,9 +205,6 @@ public function LimitCharactersToClosestWord($limit = 20, $add = '...') {
/**
* Limit this field's content by a number of words.
*
- * CAUTION: This is not XML safe. Please use
- * {@link LimitWordCountXML()} instead.
- *
* @param int $numWords Number of words to limit by.
* @param string $add Ellipsis to add to the end of truncated string.
*
@@ -192,22 +224,6 @@ public function LimitWordCount($numWords = 26, $add = '...') {
return $ret;
}
- /**
- * Limit the number of words of the current field's
- * content. This is XML safe, so characters like &
- * are converted to &
- *
- * @param int $numWords Number of words to limit by.
- * @param string $add Ellipsis to add to the end of truncated string.
- *
- * @return string
- */
- public function LimitWordCountXML($numWords = 26, $add = '...') {
- $ret = $this->LimitWordCount($numWords, $add);
-
- return Convert::raw2xml($ret);
- }
-
/**
* Converts the current value for this StringField to lowercase.
*
@@ -219,6 +235,7 @@ public function LowerCase() {
/**
* Converts the current value for this StringField to uppercase.
+ *
* @return string
*/
public function UpperCase() {
@@ -226,11 +243,11 @@ public function UpperCase() {
}
/**
- * Return the value of the field stripped of html tags.
+ * Plain text version of this string
*
- * @return string
+ * @return string Plain text
*/
public function NoHTML() {
- return strip_tags($this->RAW());
+ return $this->RAW();
}
}
diff --git a/ORM/FieldType/DBText.php b/ORM/FieldType/DBText.php
index bb79fecdb50..5b50d89fb13 100644
--- a/ORM/FieldType/DBText.php
+++ b/ORM/FieldType/DBText.php
@@ -2,13 +2,14 @@
namespace SilverStripe\ORM\FieldType;
-use HTTP;
use Convert;
use NullableField;
use TextareaField;
use TextField;
use Config;
use SilverStripe\ORM\DB;
+use InvalidArgumentException;
+use TextParser;
/**
* Represents a variable-length string of up to 2 megabytes, designed to store raw text
@@ -20,8 +21,8 @@
* );
*
*
- * @see HTMLText
- * @see HTMLVarchar
+ * @see DBHTMLText
+ * @see DBHTMLVarchar
* @see Varchar
*
* @package framework
@@ -30,17 +31,12 @@
class DBText extends DBString {
private static $casting = array(
- "AbsoluteLinks" => "Text",
"BigSummary" => "Text",
- "ContextSummary" => "Text",
+ "ContextSummary" => "HTMLText", // Always returns HTML as it contains formatting and highlighting
"FirstParagraph" => "Text",
"FirstSentence" => "Text",
- "LimitCharacters" => "Text",
"LimitSentences" => "Text",
"Summary" => "Text",
- 'EscapeXML' => 'Text',
- 'LimitWordCount' => 'Text',
- 'LimitWordCountXML' => 'HTMLText',
);
/**
@@ -51,242 +47,207 @@ public function requireField() {
$charset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'charset');
$collation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'collation');
- $parts = array(
+ $parts = [
'datatype' => 'mediumtext',
'character set' => $charset,
'collate' => $collation,
+ 'default' => $this->defaultVal,
'arrayValue' => $this->arrayValue
- );
+ ];
- $values= array(
+ $values = [
'type' => 'text',
'parts' => $parts
- );
+ ];
DB::require_field($this->tableName, $this->name, $values);
}
- /**
- * Return the value of the field with relative links converted to absolute urls.
- * @return string
- */
- public function AbsoluteLinks() {
- return HTTP::absoluteURLs($this->RAW());
- }
-
/**
* Limit sentences, can be controlled by passing an integer.
*
- * @param int $sentCount The amount of sentences you want.
+ * @param int $maxSentences The amount of sentences you want.
* @return string
*/
- public function LimitSentences($sentCount = 2) {
- if(!is_numeric($sentCount)) {
- user_error("Text::LimitSentence() expects one numeric argument", E_USER_NOTICE);
- }
-
- $output = array();
- $data = trim(Convert::xml2raw($this->RAW()));
- $sentences = explode('.', $data);
+ public function LimitSentences($maxSentences = 2) {
+ if(!is_numeric($maxSentences)) {
+ throw new InvalidArgumentException("Text::LimitSentence() expects one numeric argument");
+ }
- if ($sentCount == 0) return '';
+ $value = $this->NoHTML();
+ if( !$value ) {
+ return "";
+ }
- for($i = 0; $i < $sentCount; $i++) {
- if(isset($sentences[$i])) {
- $sentence = trim($sentences[$i]);
- if(!empty($sentence)) $output[] .= $sentence;
+ // Do a word-search
+ $words = preg_split('/\s+/', $value);
+ $sentences = 0;
+ foreach ($words as $i => $word) {
+ if (preg_match('/(!|\?|\.)$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) {
+ $sentences++;
+ if($sentences >= $maxSentences) {
+ return implode(' ', array_slice($words, 0, $i + 1));
+ }
}
}
- return count($output)==0 ? '' : implode($output, '. ') . '.';
+ // Failing to find the number of sentences requested, fallback to a logical default
+ if($maxSentences > 1) {
+ return $value;
+ } else {
+ // If searching for a single sentence (and there are none) just do a text summary
+ return $this->Summary(20);
+ }
}
/**
- * Caution: Not XML/HTML-safe - does not respect closing tags.
+ * Return the first string that finishes with a period (.) in this text.
+ *
+ * @return string
*/
public function FirstSentence() {
- $paragraph = Convert::xml2raw( $this->RAW() );
- if( !$paragraph ) return "";
+ return $this->LimitSentences(1);
+ }
- $words = preg_split('/\s+/', $paragraph);
- foreach ($words as $i => $word) {
- if (preg_match('/(!|\?|\.)$/', $word) && !preg_match('/(Dr|Mr|Mrs|Ms|Miss|Sr|Jr|No)\.$/i', $word)) {
- return implode(' ', array_slice($words, 0, $i+1));
+ /**
+ * Builds a basic summary, up to a maximum number of words
+ *
+ * @param int $maxWords
+ * @param int $maxParagraphs Optional paragraph limit
+ * @return string
+ */
+ public function Summary($maxWords = 50, $maxParagraphs = 1) {
+ // Get plain-text version
+ $value = $this->NoHTML();
+ if(!$value) {
+ return '';
}
- }
- /* If we didn't find a sentence ending, use the summary. We re-call rather than using paragraph so that
- * Summary will limit the result this time */
- return $this->Summary(20);
+ // Set max paragraphs
+ if($maxParagraphs) {
+ // Split on >2 linebreaks
+ $paragraphs = preg_split('#\n{2,}#', $value);
+ if(count($paragraphs) > $maxParagraphs) {
+ $paragraphs = array_slice($paragraphs, 0, $maxParagraphs);
+ }
+ $value = implode("\n\n", $paragraphs);
}
- /**
- * Caution: Not XML/HTML-safe - does not respect closing tags.
- */
- public function Summary($maxWords = 50) {
- // get first sentence?
- // this needs to be more robust
- $value = Convert::xml2raw( $this->RAW() /*, true*/ );
- if(!$value) return '';
-
- // grab the first paragraph, or, failing that, the whole content
- if(strpos($value, "\n\n")) $value = substr($value, 0, strpos($value, "\n\n"));
+ // Find sentences
$sentences = explode('.', $value);
- $count = count(explode(' ', $sentences[0]));
+ $wordCount = count(preg_split('#\s+#', $sentences[0]));
// if the first sentence is too long, show only the first $maxWords words
- if($count > $maxWords) {
+ if($wordCount > $maxWords) {
return implode( ' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . '...';
}
// add each sentence while there are enough words to do so
$result = '';
do {
- $result .= trim(array_shift( $sentences )).'.';
- if(count($sentences) > 0) {
- $count += count(explode(' ', $sentences[0]));
- }
-
- // Ensure that we don't trim half way through a tag or a link
- $brokenLink = (
- substr_count($result,'<') != substr_count($result,'>')) ||
- (substr_count($result,']*>/', $result) && !preg_match( '/<\/a>/', $result)) $result .= '';
+ // If more sentences to process, count number of words
+ if($sentences) {
+ $wordCount += count(preg_split('#\s+#', $sentences[0]));
+ }
+ } while($wordCount < $maxWords && $sentences && trim( $sentences[0]));
- return Convert::raw2xml($result);
+ return trim($result);
}
/**
* Performs the same function as the big summary, but doesn't trim new paragraphs off data.
- * Caution: Not XML/HTML-safe - does not respect closing tags.
+ *
+ * @param int $maxWords
+ * @return string
*/
- public function BigSummary($maxWords = 50, $plain = true) {
- $result = '';
-
- // get first sentence?
- // this needs to be more robust
- $data = $plain ? Convert::xml2raw($this->RAW(), true) : $this->RAW();
-
- if(!$data) return '';
-
- $sentences = explode('.', $data);
- $count = count(explode(' ', $sentences[0]));
-
- // if the first sentence is too long, show only the first $maxWords words
- if($count > $maxWords) {
- return implode(' ', array_slice(explode( ' ', $sentences[0] ), 0, $maxWords)) . '...';
+ public function BigSummary($maxWords = 50) {
+ return $this->Summary($maxWords, 0);
}
- // add each sentence while there are enough words to do so
- do {
- $result .= trim(array_shift($sentences));
- if($sentences) {
- $result .= '. ';
- $count += count(explode(' ', $sentences[0]));
- }
-
- // Ensure that we don't trim half way through a tag or a link
- $brokenLink = (
- substr_count($result,'<') != substr_count($result,'>')) ||
- (substr_count($result,']*>/', $result) && !preg_match( '/<\/a>/', $result)) {
- $result .= '';
- }
-
- return $result;
- }
-
/**
- * Caution: Not XML/HTML-safe - does not respect closing tags.
+ * Get first paragraph
+ *
+ * @return string
*/
- public function FirstParagraph($plain = 1) {
- // get first sentence?
- // this needs to be more robust
- $value = $this->RAW();
- if($plain && $plain != 'html') {
- $data = Convert::xml2raw($value);
- if(!$data) return "";
-
- // grab the first paragraph, or, failing that, the whole content
- $pos = strpos($data, "\n\n");
- if($pos) $data = substr($data, 0, $pos);
-
- return $data;
- } else {
- if(strpos($value, "
") === false) return $value;
-
- $data = substr($value, 0, strpos($value, "") + 4);
-
- if(strlen($data) < 20 && strpos($value, "", strlen($data))) {
- $data = substr($value, 0, strpos( $value, "", strlen($data)) + 4 );
+ public function FirstParagraph() {
+ $value = $this->NoHTML();
+ if(empty($value)) {
+ return '';
}
- return $data;
+ // Split paragraphs and return first
+ $paragraphs = preg_split('#\n{2,}#', $value);
+ return reset($paragraphs);
}
- }
/**
* Perform context searching to give some context to searches, optionally
* highlighting the search term.
*
* @param int $characters Number of characters in the summary
- * @param boolean $string Supplied string ("keywords")
- * @param boolean $striphtml Strip HTML?
- * @param boolean $highlight Add a highlight element around search query?
- * @param string $prefix text
- * @param string $suffix
- *
- * @return string
+ * @param string $keywords Supplied string ("keywords"). Will fall back to 'Search' querystring arg.
+ * @param bool $highlight Add a highlight element around search query?
+ * @param string $prefix Prefix text
+ * @param string $suffix Suffix text
+ * @return string HTML string with context
*/
- public function ContextSummary($characters = 500, $string = false, $striphtml = true, $highlight = true,
- $prefix = "... ", $suffix = "...") {
+ public function ContextSummary(
+ $characters = 500, $keywords = null, $highlight = true, $prefix = "... ", $suffix = "..."
+ ) {
- if(!$string) {
+ if(!$keywords) {
// Use the default "Search" request variable (from SearchForm)
- $string = isset($_REQUEST['Search']) ? $_REQUEST['Search'] : '';
+ $keywords = isset($_REQUEST['Search']) ? $_REQUEST['Search'] : '';
}
- // Remove HTML tags so we don't have to deal with matching tags
- $text = $striphtml ? $this->NoHTML() : $this->RAW();
+ // Get raw text value, but XML encode it (as we'll be merging with HTML tags soon)
+ $text = nl2br(Convert::raw2xml($this->NoHTML()));
+ $keywords = Convert::raw2xml($keywords);
// Find the search string
- $position = (int) stripos($text, $string);
+ $position = (int) stripos($text, $keywords);
// We want to search string to be in the middle of our block to give it some context
$position = max(0, $position - ($characters / 2));
if($position > 0) {
// We don't want to start mid-word
- $position = max((int) strrpos(substr($text, 0, $position), ' '),
- (int) strrpos(substr($text, 0, $position), "\n"));
+ $position = max(
+ (int) strrpos(substr($text, 0, $position), ' '),
+ (int) strrpos(substr($text, 0, $position), "\n")
+ );
}
$summary = substr($text, $position, $characters);
- $stringPieces = explode(' ', $string);
+ $stringPieces = explode(' ', $keywords);
if($highlight) {
// Add a span around all key words from the search term as well
if($stringPieces) {
-
foreach($stringPieces as $stringPiece) {
if(strlen($stringPiece) > 2) {
- $summary = str_ireplace($stringPiece, "$stringPiece",
- $summary);
+ $summary = str_ireplace(
+ $stringPiece,
+ "$stringPiece",
+ $summary
+ );
}
}
}
}
$summary = trim($summary);
- if($position > 0) $summary = $prefix . $summary;
- if(strlen($this->RAW()) > ($characters + $position)) $summary = $summary . $suffix;
+ // Add leading / trailing '...' if trimmed on either end
+ if($position > 0) {
+ $summary = $prefix . $summary;
+ }
+ if(strlen($this->value) > ($characters + $position)) {
+ $summary = $summary . $suffix;
+ }
return $summary;
}
@@ -295,20 +256,18 @@ public function ContextSummary($characters = 500, $string = false, $striphtml =
* Allows a sub-class of TextParser to be rendered.
*
* @see TextParser for implementation details.
- * @param string $parser
- * @return string
+ * @param string $parser Class name of parser (Must extend {@see TextParser})
+ * @return DBField Parsed value in the appropriate type
*/
- public function Parse($parser = "TextParser") {
- if($parser == "TextParser" || is_subclass_of($parser, "TextParser")) {
- $obj = new $parser($this->RAW());
- return $obj->parse();
- } else {
- // Fallback to using raw2xml and show a warning
- // TODO Don't kill script execution, we can continue without losing complete control of the app
- user_error("Couldn't find an appropriate TextParser sub-class to create (Looked for '$parser')."
- . "Make sure it sub-classes TextParser and that you've done ?flush=1.", E_USER_WARNING);
- return Convert::raw2xml($this->RAW());
+ public function Parse($parser) {
+ $reflection = new \ReflectionClass($parser);
+ if($reflection->isAbstract() || !$reflection->isSubclassOf('TextParser')) {
+ throw new InvalidArgumentException("Invalid parser {$parser}");
}
+
+ /** @var TextParser $obj */
+ $obj = \Injector::inst()->createWithArgs($parser, [$this->forTemplate()]);
+ return $obj->parse();
}
/**
diff --git a/ORM/FieldType/DBVarchar.php b/ORM/FieldType/DBVarchar.php
index a0193fe8e19..c0b8d683272 100644
--- a/ORM/FieldType/DBVarchar.php
+++ b/ORM/FieldType/DBVarchar.php
@@ -10,9 +10,9 @@
/**
* Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text
*
- * @see HTMLText
- * @see HTMLVarchar
- * @see Text
+ * @see DBHTMLText
+ * @see DBHTMLVarchar
+ * @see DBText
*
* @package framework
* @subpackage orm
diff --git a/Security/CMSSecurity.php b/Security/CMSSecurity.php
index a58cffc51d1..b4c8223e2a9 100644
--- a/Security/CMSSecurity.php
+++ b/Security/CMSSecurity.php
@@ -19,7 +19,7 @@
class CMSSecurity extends Security {
private static $casting = array(
- 'Title' => 'HTMLText'
+ 'Title' => 'HTMLFragment'
);
private static $allowed_actions = array(
diff --git a/Security/PermissionCheckboxSetField.php b/Security/PermissionCheckboxSetField.php
index 02bff51a1d3..afd3fbeaec4 100644
--- a/Security/PermissionCheckboxSetField.php
+++ b/Security/PermissionCheckboxSetField.php
@@ -90,7 +90,7 @@ public function getHiddenPermissions() {
/**
* @param array $properties
- * @return DBHTMLText
+ * @return string
*/
public function Field($properties = array()) {
Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/CheckboxSetField.css');
@@ -248,7 +248,7 @@ public function Field($properties = array()) {
}
}
if($this->readonly) {
- return DBField::create_field('HTMLText',
+ return
"
\n"
- );
+ "\n";
}
}
diff --git a/_config/model.yml b/_config/model.yml
index 65d31457b29..df88c582a93 100644
--- a/_config/model.yml
+++ b/_config/model.yml
@@ -25,6 +25,10 @@ Injector:
class: SilverStripe\ORM\FieldType\DBForeignKey
HTMLText:
class: SilverStripe\ORM\FieldType\DBHTMLText
+ properties:
+ ProcessShortcodes: true
+ HTMLFragment:
+ class: SilverStripe\ORM\FieldType\DBHTMLText
HTMLVarchar:
class: SilverStripe\ORM\FieldType\DBHTMLVarchar
Int:
diff --git a/api/RSSFeed.php b/api/RSSFeed.php
index 4be731064c5..01f838fd059 100644
--- a/api/RSSFeed.php
+++ b/api/RSSFeed.php
@@ -299,45 +299,41 @@ public function __construct($entry, $titleField, $descriptionField, $authorField
/**
* Get the description of this entry
*
- * @return string Returns the description of the entry.
+ * @return DBField Returns the description of the entry.
*/
public function Title() {
- return $this->rssField($this->titleField, 'Varchar');
+ return $this->rssField($this->titleField);
}
/**
* Get the description of this entry
*
- * @return string Returns the description of the entry.
+ * @return DBField Returns the description of the entry.
*/
public function Description() {
- return $this->rssField($this->descriptionField, 'HTMLText');
+ return $this->rssField($this->descriptionField);
}
/**
* Get the author of this entry
*
- * @return string Returns the author of the entry.
+ * @return DBField Returns the author of the entry.
*/
public function Author() {
- if($this->authorField) return $this->failover->obj($this->authorField);
+ return $this->rssField($this->authorField);
}
/**
- * Return the named field as an obj() call from $this->failover.
- * Default to the given class if there's no casting information.
+ * Return the safely casted field
+ *
+ * @param string $fieldName Name of field
+ * @return DBField
*/
- public function rssField($fieldName, $defaultClass = 'Varchar') {
+ public function rssField($fieldName) {
if($fieldName) {
- if($this->failover->castingHelper($fieldName)) {
- $value = $this->failover->$fieldName;
- $obj = $this->failover->obj($fieldName);
- $obj->setValue($value);
- return $obj;
- } else {
- return DBField::create_field($defaultClass, $this->failover->XML_val($fieldName), $fieldName);
- }
+ return $this->failover->obj($fieldName);
}
+ return null;
}
/**
diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php
index 0786b95a15b..b7b98ff1aab 100644
--- a/api/XMLDataFormatter.php
+++ b/api/XMLDataFormatter.php
@@ -3,6 +3,8 @@
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
+use SilverStripe\ORM\FieldType\DBHTMLText;
+
/**
* @package framework
* @subpackage formatters
@@ -54,18 +56,21 @@ public function convertDataObjectWithoutHeader(DataObject $obj, $fields = null,
$xml = "<$className href=\"$objHref.xml\">\n";
foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) {
// Field filtering
- if($fields && !in_array($fieldName, $fields)) continue;
- $fieldValue = $obj->obj($fieldName)->forTemplate();
- if(!mb_check_encoding($fieldValue,'utf-8')) $fieldValue = "(data is badly encoded)";
+ if($fields && !in_array($fieldName, $fields)) {
+ continue;
+ }
+ $fieldObject = $obj->obj($fieldName);
+ $fieldValue = $fieldObject->forTemplate();
+ if(!mb_check_encoding($fieldValue, 'utf-8')) {
+ $fieldValue = "(data is badly encoded)";
+ }
if(is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toXML')) {
$xml .= $fieldValue->toXML();
} else {
- if('HTMLText' == $fieldType) {
+ if($fieldObject instanceof DBHTMLText) {
// Escape HTML values using CDATA
$fieldValue = sprintf('', str_replace(']]>', ']]]]>', $fieldValue));
- } else {
- $fieldValue = Convert::raw2xml($fieldValue);
}
$xml .= "<$fieldName>$fieldValue$fieldName>\n";
}
diff --git a/core/CustomMethods.php b/core/CustomMethods.php
index 96b53293c03..5cbe31dcc55 100644
--- a/core/CustomMethods.php
+++ b/core/CustomMethods.php
@@ -50,40 +50,41 @@ public function __call($method, $arguments) {
$this->defineMethods();
}
- // Validate method being invked
- $method = strtolower($method);
- if(!isset(self::$extra_methods[$class][$method])) {
- // Please do not change the exception code number below.
- $class = get_class($this);
- throw new BadMethodCallException("Object->__call(): the method '$method' does not exist on '$class'", 2175);
+ $config = $this->getExtraMethodConfig($method);
+ if(empty($config)) {
+ throw new BadMethodCallException(
+ "Object->__call(): the method '$method' does not exist on '$class'"
+ );
}
- $config = self::$extra_methods[$class][$method];
-
switch(true) {
- case isset($config['property']) :
+ case isset($config['property']) : {
$obj = $config['index'] !== null ?
$this->{$config['property']}[$config['index']] :
$this->{$config['property']};
- if($obj) {
- if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
+ if ($obj) {
+ if (!empty($config['callSetOwnerFirst'])) {
+ $obj->setOwner($this);
+ }
$retVal = call_user_func_array(array($obj, $method), $arguments);
- if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
+ if (!empty($config['callSetOwnerFirst'])) {
+ $obj->clearOwner();
+ }
return $retVal;
}
- if(!empty($this->destroyed)) {
+ if (!empty($this->destroyed)) {
throw new BadMethodCallException(
"Object->__call(): attempt to call $method on a destroyed $class object"
);
} else {
throw new BadMethodCallException(
"Object->__call(): $class cannot pass control to $config[property]($config[index])."
- . ' Perhaps this object was mistakenly destroyed?'
+ . ' Perhaps this object was mistakenly destroyed?'
);
}
-
+ }
case isset($config['wrap']) :
array_unshift($arguments, $config['method']);
return call_user_func_array(array($this, $config['wrap']), $arguments);
@@ -107,8 +108,6 @@ public function __call($method, $arguments) {
* @uses addMethodsFrom()
*/
protected function defineMethods() {
- $class = get_class($this);
-
// Define from all registered callbacks
foreach($this->extra_method_registers as $callback) {
call_user_func($callback);
@@ -139,8 +138,21 @@ protected function registerExtraMethodCallback($name, $callback) {
* @return bool
*/
public function hasMethod($method) {
+ return method_exists($this, $method) || $this->getExtraMethodConfig($method);
+ }
+
+ /**
+ * Get meta-data details on a named method
+ *
+ * @param array $method
+ * @return array List of custom method details, if defined for this method
+ */
+ protected function getExtraMethodConfig($method) {
$class = get_class($this);
- return method_exists($this, $method) || isset(self::$extra_methods[$class][strtolower($method)]);
+ if(isset(self::$extra_methods[$class][strtolower($method)])) {
+ return self::$extra_methods[$class][strtolower($method)];
+ }
+ return null;
}
/**
diff --git a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
index 27e74ac5986..9896a5f8347 100644
--- a/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
+++ b/docs/en/02_Developer_Guides/00_Model/04_Data_Types_and_Casting.md
@@ -189,10 +189,7 @@ the database. However, the template engine knows to escape fields without the `
to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see
"[security](../security)"), which is important if these fields store user-provided data.
-
-You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML
-content via `$MyHtmlField.XML`.
-
+See the [Template casting](/developer_guides/templates/casting) section for controlling casting in your templates.
## Overloading
@@ -220,4 +217,4 @@ database column using `dbObject`.
## API Documentation
* [api:DataObject]
-* [api:DBField]
\ No newline at end of file
+* [api:DBField]
diff --git a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md
index c35db3a63da..8e9a2edb45f 100644
--- a/docs/en/02_Developer_Guides/01_Templates/09_Casting.md
+++ b/docs/en/02_Developer_Guides/01_Templates/09_Casting.md
@@ -98,10 +98,50 @@ this purpose.
There's some exceptions to this rule, see the ["security" guide](../security).
-In case you want to explicitly allow un-escaped HTML input, the property can be cast as [api:HTMLText]. The following
-example takes the `Content` field in a `SiteTree` class, which is of this type. It forces the content into an explicitly
-escaped format.
+For every field used in templates, a casting helper will be applied. This will first check for any
+`casting` helper on your model specific to that field, and will fall back to the `default_cast` config
+in case none are specified.
+
+By default, `ViewableData.default_cast` is set to `Text`, which will ensure all fields have special
+characters HTML escaped by default.
+
+The most common casting types are:
+
+ * `Text` Which is a plain text string, and will be safely encoded via HTML entities when placed into
+ a template.
+ * `Varchar` which is the same as `Text` but for single-line text that should not have line breaks.
+ * `HTMLFragment` is a block of raw HTML, which should not be escaped. Take care to sanitise any HTML
+ value saved into the database.
+ * `HTMLText` is a `HTMLFragment`, but has shortcodes enabled. This should only be used for content
+ that is modified via a TinyMCE editor, which will insert shortcodes.
+ * `Int` for integers.
+ * `Decimal` for floating point values.
+ * `Boolean` For boolean values.
+ * `Datetime` for date and time.
+
+See the [Model data types and casting](/developer_guides/model/data_types_and_casting) section for
+instructions on configuring your model to declare casting types for fields.
+
+## Escape methods in templates
+
+Within the template, fields can have their encoding customised at a certain level with format methods.
+See [api:DBField] for the specific implementation, but they will generally follow the below rules:
+
+* `$Field` with no format method supplied will correctly cast itself for the HTML template, as defined
+ by the casting helper for that field. In most cases this is the best method to use for templates.
+* `$Field.XML` Will invoke `htmlentities` on special characters in the value, even if it's already
+ cast as HTML.
+* `$Field.ATT` will ensure the field is XML encoded for placement inside a HTML element property.
+ This will invoke `htmlentities` on the value (even if already cast as HTML) and will escape quotes.
+* `Field.JS` will cast this value as a javascript string. E.g. `var fieldVal = '$Field.JS';` can
+ be used in javascript defined in templates to encode values safely.
+* `$Field.CDATA` will cast this value safely for insertion as a literal string in an XML file.
+ E.g. `$Field.CDATA` will ensure that the `` body is safely escaped
+ as a string.
+
+
+Note: Take care when using `.XML` on `HTMLText` fields, as this will result in double-encoded
+html. To ensure that the correct encoding is used for that field in a template, simply use
+`$Field` by itself to allow the casting helper to determine the best encoding itself.
+
- :::ss
- $Content.XML
- // transforms e.g. "alert" to "<em>alert</em>"
diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md
index 49427c1348b..abcee089382 100644
--- a/docs/en/04_Changelogs/4.0.0.md
+++ b/docs/en/04_Changelogs/4.0.0.md
@@ -45,6 +45,11 @@
* `DataObject::can` has new method signature with `$context` parameter.
* `SiteTree.alternatePreviewLink` is deprecated. Use `updatePreviewLink` instead.
* `Injector` dependencies no longer automatically inherit from parent classes.
+ * `default_cast` is now enforced on all template variables. See upgrading notes below.
+ * `HTMLText` no longer enables shortcodes by default. You can specify the `db` option for
+ html fields as `HTMLText(['whitelist=meta,link'])`, or use a `ShortcodeHTMLText` as
+ a shorthand substitute.
+ * `FormField->dontEscape` has been removed. Escaping is now managed on a class by class basis.
## New API
@@ -99,6 +104,7 @@
* `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions
* `DataObject.table_name` config can now be used to customise the database table for any record.
* `DataObjectSchema` class added to assist with mapping between classes and tables.
+ * `FormField::Title` and `FormField::RightTitle` are now cast as plain text by default (but can be overridden).
### Front-end build tooling for CMS interface
@@ -168,7 +174,7 @@ admin/font/ => admin/client/dist/font/
* History.js
* `debugmethods` querystring argument has been removed from debugging.
-
+
* The following ClassInfo methods are now deprecated:
* `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead.
* `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead
@@ -302,6 +308,64 @@ E.g.
## Upgrading
+
+### Explicit text casting is now enforced on all template variables
+
+Now whenever a `$Variable` is used in a template, regardless of whether any casts or methods are
+suffixed to the reference, it will be cast to either an explicit DBField for that field, or
+the value declared by the `default_cast` on the parent object.
+
+The default value of `default_cast` is `Text`, meaning that now many cases where a field was
+left un-uncoded, this will now be safely encoded via `Convert::raw2xml`. In cases where
+un-cast fields were used to place raw HTML into templates, this will now encode this until
+explicitly cast for that field.
+
+You can resolve this in your model by adding an explicit cast to HTML for those fields.
+
+
+Before:
+
+
+ :::ss
+
+ $SomeHTML
+
+
+
+ :::php
+ class MyObject extends ViewableData {
+ public function getSomeHTML {
+ $title = Convert::raw2xml($this->Title);
+ return "
";
+ }
+ }
+
+If you need to encode a field (such as HTMLText) for use in html attributes, use `.ATT`
+instead, or if used in an actual XML file use `.CDATA`.
+
+See the [Template casting](/developer_guides/templates/casting) section for specific details.
### Automatically upgrading
diff --git a/filesystem/File.php b/filesystem/File.php
index ffd3739d1ab..249b65f8783 100644
--- a/filesystem/File.php
+++ b/filesystem/File.php
@@ -116,7 +116,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
);
private static $casting = array (
- 'TreeTitle' => 'HTMLText'
+ 'TreeTitle' => 'HTMLFragment'
);
/**
@@ -458,12 +458,11 @@ public function getCMSFields() {
CompositeField::create(
new ReadonlyField("FileType", _t('AssetTableField.TYPE','File type') . ':'),
new ReadonlyField("Size", _t('AssetTableField.SIZE','File size') . ':', $this->getSize()),
- ReadonlyField::create(
+ HTMLReadonlyField::create(
'ClickableURL',
_t('AssetTableField.URL','URL'),
sprintf('%s', $this->Link(), $this->Link())
- )
- ->setDontEscape(true),
+ ),
new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
)
@@ -632,7 +631,10 @@ public function updateFilesystem() {
public function collateDescendants($condition, &$collator) {
if($children = $this->Children()) {
foreach($children as $item) {
- if(!$condition || eval("return $condition;")) $collator[] = $item;
+ /** @var File $item */
+ if(!$condition || eval("return $condition;")) {
+ $collator[] = $item;
+ }
$item->collateDescendants($condition, $collator);
}
return true;
diff --git a/filesystem/ImageManipulation.php b/filesystem/ImageManipulation.php
index b08f2df5e09..76196db219e 100644
--- a/filesystem/ImageManipulation.php
+++ b/filesystem/ImageManipulation.php
@@ -517,7 +517,7 @@ public function ThumbnailIcon($width, $height) {
*/
public function IconTag() {
return DBField::create_field(
- 'HTMLText',
+ 'HTMLFragment',
''
);
}
diff --git a/filesystem/storage/DBFile.php b/filesystem/storage/DBFile.php
index 90905cd3666..80dfc3410e6 100644
--- a/filesystem/storage/DBFile.php
+++ b/filesystem/storage/DBFile.php
@@ -100,7 +100,7 @@ protected function getStore() {
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
- 'Tag' => 'HTMLText',
+ 'Tag' => 'HTMLFragment',
'Size' => 'Varchar'
);
@@ -113,7 +113,7 @@ public function scaffoldFormField($title = null, $params = null) {
*
* @return string
*/
- public function forTemplate() {
+ public function XML() {
return $this->getTag() ?: '';
}
diff --git a/forms/CheckboxField.php b/forms/CheckboxField.php
index b2a2895ac39..e47b96aa79f 100644
--- a/forms/CheckboxField.php
+++ b/forms/CheckboxField.php
@@ -58,9 +58,13 @@ public function performReadonlyTransformation() {
}
public function Value() {
- return Convert::raw2xml($this->value ?
+ return $this->value ?
_t('CheckboxField.YESANSWER', 'Yes') :
- _t('CheckboxField.NOANSWER', 'No'));
+ _t('CheckboxField.NOANSWER', 'No');
+ }
+
+ public function getValueCast() {
+ return 'Text';
}
}
diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php
index a5fe79dde35..f10562e4a75 100644
--- a/forms/ConfirmedPasswordField.php
+++ b/forms/ConfirmedPasswordField.php
@@ -144,7 +144,7 @@ public function __construct($name, $title = null, $value = "", $form = null, $sh
/**
* @param array $properties
*
- * @return DBHTMLText
+ * @return string
*/
public function Field($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
diff --git a/forms/CurrencyField.php b/forms/CurrencyField.php
index 7580665058c..35e2125867f 100644
--- a/forms/CurrencyField.php
+++ b/forms/CurrencyField.php
@@ -69,13 +69,14 @@ class CurrencyField_Readonly extends ReadonlyField{
*/
public function Field($properties = array()) {
if($this->value){
- $val = $this->dontEscape ? $this->value : Convert::raw2xml($this->value);
+ $val = Convert::raw2xml($this->value);
$val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/',"",$val), 2);
+ $valforInput = Convert::raw2att($val);
} else {
$val = ''._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00';
+ $valforInput = '';
}
- $valforInput = $this->value ? Convert::raw2att($val) : "";
- return "extraClass()."\" id=\"" . $this->id() . "\">$val"
+ return "extraClass()."\" id=\"" . $this->ID() . "\">$val"
. "name."\" value=\"".$valforInput."\" />";
}
@@ -102,12 +103,12 @@ class CurrencyField_Disabled extends CurrencyField{
*/
public function Field($properties = array()) {
if($this->value){
- $val = $this->dontEscape ? $this->value : Convert::raw2xml($this->value);
+ $val = Convert::raw2xml($this->value);
$val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/',"",$val), 2);
+ $valforInput = Convert::raw2att($val);
} else {
- $val = ''._t('CurrencyField.CURRENCYSYMBOL', '$').'0.00';
+ $valforInput = '';
}
- $valforInput = $this->value ? Convert::raw2att($val) : "";
return "name."\" value=\"".$valforInput."\" />";
}
diff --git a/forms/DatalessField.php b/forms/DatalessField.php
index 97c0f07b944..9081d9127b4 100644
--- a/forms/DatalessField.php
+++ b/forms/DatalessField.php
@@ -32,7 +32,7 @@ public function getAttributes() {
* Returns the field's representation in the form.
* For dataless fields, this defaults to $Field.
*
- * @return HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);
@@ -57,6 +57,7 @@ public function performReadonlyTransformation() {
/**
* @param bool $bool
+ * @return $this
*/
public function setAllowHTML($bool) {
$this->allowHTML = $bool;
diff --git a/forms/DatetimeField.php b/forms/DatetimeField.php
index dca87b4eed7..96cbfc43e40 100644
--- a/forms/DatetimeField.php
+++ b/forms/DatetimeField.php
@@ -97,7 +97,7 @@ public function setName($name) {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
$config = array(
@@ -112,16 +112,17 @@ public function FieldHolder($properties = array()) {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/DatetimeField.css');
$tzField = ($this->getConfig('usertimezone')) ? $this->timezoneField->FieldHolder() : '';
- return DBField::create_field('HTMLText', $this->dateField->FieldHolder() .
- $this->timeField->FieldHolder() .
- $tzField .
- ''
+ return sprintf(
+ '%s%s%s',
+ $this->dateField->FieldHolder(),
+ $this->timeField->FieldHolder(),
+ $tzField
);
}
@@ -139,6 +140,7 @@ public function Field($properties = array()) {
* @param string|array $val String expects an ISO date format. Array notation with 'date' and 'time'
* keys can contain localized strings. If the 'dmyfields' option is used for {@link DateField},
* the 'date' value may contain array notation was well (see {@link DateField->setValue()}).
+ * @return $this
*/
public function setValue($val) {
$locale = new Zend_Locale($this->locale);
diff --git a/forms/DropdownField.php b/forms/DropdownField.php
index 98eb92afc1f..d1b440f483e 100644
--- a/forms/DropdownField.php
+++ b/forms/DropdownField.php
@@ -114,7 +114,7 @@ protected function getFieldOption($value, $title) {
/**
* @param array $properties
- * @return DBHTMLText
+ * @return string
*/
public function Field($properties = array()) {
$options = array();
diff --git a/forms/FileField.php b/forms/FileField.php
index c9e637405d9..975f5a7f0ae 100644
--- a/forms/FileField.php
+++ b/forms/FileField.php
@@ -87,7 +87,7 @@ public function __construct($name, $title = null, $value = null) {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
$properties = array_merge($properties, array(
diff --git a/forms/Form.php b/forms/Form.php
index 56052dd341c..92a0e6e1d4b 100644
--- a/forms/Form.php
+++ b/forms/Form.php
@@ -5,6 +5,7 @@
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\SS_List;
+use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\NullSecurityToken;
@@ -215,6 +216,15 @@ class Form extends RequestHandler {
'forTemplate',
);
+ private static $casting = array(
+ 'AttributesHTML' => 'HTMLFragment',
+ 'FormAttributes' => 'HTMLFragment',
+ 'MessageType' => 'Text',
+ 'Message' => 'HTMLFragment',
+ 'FormName' => 'Text',
+ 'Legend' => 'HTMLFragment',
+ );
+
/**
* @var FormTemplateHelper
*/
@@ -1732,7 +1742,7 @@ public function buttonClicked() {
public function defaultAction() {
if($this->hasDefaultAction && $this->actions) {
return $this->actions->First();
- }
+ }
}
/**
@@ -1804,7 +1814,7 @@ public function getSecurityToken() {
public static function single_field_required() {
if(self::current_action() == 'callfieldmethod') {
return $_REQUEST['fieldName'];
- }
+ }
}
/**
diff --git a/forms/FormAction.php b/forms/FormAction.php
index 091f1c438c1..3aabbb9edd1 100644
--- a/forms/FormAction.php
+++ b/forms/FormAction.php
@@ -17,6 +17,14 @@
*/
class FormAction extends FormField {
+ /**
+ * @config
+ * @var array
+ */
+ private static $casting = [
+ 'ButtonContent' => 'HTMLFragment',
+ ];
+
/**
* Action name, normally prefixed with 'action_'
*
@@ -83,7 +91,7 @@ public function setFullAction($fullAction) {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
$properties = array_merge(
@@ -100,7 +108,7 @@ public function Field($properties = array()) {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);
@@ -110,22 +118,6 @@ public function Type() {
return 'action';
}
- public function Title() {
- $title = parent::Title();
-
- // Remove this method override in 4.0
- $decoded = Convert::xml2raw($title);
- if($title && $decoded !== $title) {
- Deprecation::notice(
- '4.0',
- 'The FormAction title field should not be html encoded. Use buttonContent to set custom html instead'
- );
- return $decoded;
- }
-
- return $title;
- }
-
public function getAttributes() {
$type = (isset($this->attributes['src'])) ? 'image' : 'submit';
@@ -141,7 +133,7 @@ public function getAttributes() {
}
/**
- * Add content inside a button field.
+ * Add content inside a button field. This should be pre-escaped raw HTML and should be used sparingly.
*
* @param string $content
* @return $this
@@ -152,7 +144,7 @@ public function setButtonContent($content) {
}
/**
- * Gets the content inside the button field
+ * Gets the content inside the button field. This is raw HTML, and should be used sparingly.
*
* @return string
*/
diff --git a/forms/FormField.php b/forms/FormField.php
index 6b8609202a6..614c9a61f71 100644
--- a/forms/FormField.php
+++ b/forms/FormField.php
@@ -131,12 +131,6 @@ class FormField extends RequestHandler {
*/
private static $default_classes = [];
-
- /**
- * @var bool
- */
- public $dontEscape;
-
/**
* Right-aligned, contextual label for the field.
*
@@ -258,6 +252,22 @@ class FormField extends RequestHandler {
*/
protected $schemaData = [];
+ private static $casting = array(
+ 'FieldHolder' => 'HTMLFragment',
+ 'Field' => 'HTMLFragment',
+ 'AttributesHTML' => 'HTMLFragment',
+ 'Value' => 'Text',
+ 'extraClass' => 'Text',
+ 'ID' => 'Text',
+ 'isReadOnly' => 'Boolean',
+ 'HolderID' => 'Text',
+ 'Title' => 'Text',
+ 'RightTitle' => 'Text',
+ 'MessageType' => 'Text',
+ 'Message' => 'HTMLFragment',
+ 'Description' => 'HTMLFragment',
+ );
+
/**
* Structured schema state representing the FormField's current data and validation.
* Used to render the FormField as a ReactJS Component on the front-end.
@@ -483,13 +493,14 @@ public function Title() {
}
/**
- * @param string $title
+ * Set the title of this formfield.
+ * Note: This expects escaped HTML.
*
+ * @param string $title Escaped HTML for title
* @return $this
*/
public function setTitle($title) {
$this->title = $title;
-
return $this;
}
@@ -504,13 +515,14 @@ public function RightTitle() {
}
/**
- * @param string $rightTitle
+ * Sets the right title for this formfield
+ * Note: This expects escaped HTML.
*
+ * @param string $rightTitle Escaped HTML for title
* @return $this
*/
public function setRightTitle($rightTitle) {
$this->rightTitle = $rightTitle;
-
return $this;
}
@@ -928,7 +940,6 @@ public function setSmallFieldHolderTemplate($smallFieldHolderTemplate) {
* such as an input tag.
*
* @param array $properties
- *
* @return string
*/
public function Field($properties = array()) {
@@ -1366,31 +1377,9 @@ public function castedCopy($classOrCopy) {
$field->setAttribute($attributeKey, $attributeValue);
}
- $field->dontEscape = $this->dontEscape;
-
return $field;
}
- /**
- * Determine if escaping of this field should be disabled
- *
- * @param bool $dontEscape
- * @return $this
- */
- public function setDontEscape($dontEscape) {
- $this->dontEscape = $dontEscape;
- return $this;
- }
-
- /**
- * Determine if escaping is disabled
- *
- * @return bool
- */
- public function getDontEscape() {
- return $this->dontEscape;
- }
-
/**
* Sets the component type the FormField will be rendered as on the front-end.
*
diff --git a/forms/HTMLReadonlyField.php b/forms/HTMLReadonlyField.php
new file mode 100644
index 00000000000..34f077caf64
--- /dev/null
+++ b/forms/HTMLReadonlyField.php
@@ -0,0 +1,12 @@
+ 'HTMLFragment'
+ ];
+}
diff --git a/forms/HiddenField.php b/forms/HiddenField.php
index d03f76b18d2..36dc4e764a3 100644
--- a/forms/HiddenField.php
+++ b/forms/HiddenField.php
@@ -12,8 +12,7 @@ class HiddenField extends FormField {
/**
* @param array $properties
- *
- * @return HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
return $this->Field($properties);
diff --git a/forms/InlineFormAction.php b/forms/InlineFormAction.php
index 19b49109e97..abffa01b4b1 100644
--- a/forms/InlineFormAction.php
+++ b/forms/InlineFormAction.php
@@ -1,9 +1,5 @@
extraClass = ' '.$extraClass;
@@ -34,24 +31,23 @@ public function performReadonlyTransformation() {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
if($this->includeDefaultJS) {
- Requirements::javascriptTemplate(FRAMEWORK_DIR . '/client/dist/js/InlineFormAction.js',
- array('ID'=>$this->id()));
+ Requirements::javascriptTemplate(
+ FRAMEWORK_DIR . '/client/dist/js/InlineFormAction.js',
+ array('ID'=>$this->ID())
+ );
}
- return DBField::create_field(
- 'HTMLText',
- FormField::create_tag('input', array(
+ return FormField::create_tag('input', array(
'type' => 'submit',
'name' => sprintf('action_%s', $this->getName()),
'value' => $this->title,
'id' => $this->ID(),
'class' => sprintf('action%s', $this->extraClass),
- ))
- );
+ ));
}
public function Title() {
@@ -80,19 +76,17 @@ class InlineFormAction_ReadOnly extends FormField {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
- return DBField::create_field('HTMLText',
- FormField::create_tag('input', array(
+ return FormField::create_tag('input', array(
'type' => 'submit',
'name' => sprintf('action_%s', $this->name),
'value' => $this->title,
- 'id' => $this->id(),
+ 'id' => $this->ID(),
'disabled' => 'disabled',
'class' => 'action disabled ' . $this->extraClass,
- ))
- );
+ ));
}
public function Title() {
diff --git a/forms/LiteralField.php b/forms/LiteralField.php
index c2b576329c8..7c6dc8453cf 100644
--- a/forms/LiteralField.php
+++ b/forms/LiteralField.php
@@ -14,6 +14,11 @@
* @subpackage fields-dataless
*/
class LiteralField extends DatalessField {
+
+ private static $casting = [
+ 'Value' => 'HTMLFragment',
+ ];
+
/**
* @var string|FormField
*/
diff --git a/forms/LookupField.php b/forms/LookupField.php
index d6d15c94d1f..3aeebb3b324 100644
--- a/forms/LookupField.php
+++ b/forms/LookupField.php
@@ -1,4 +1,5 @@
dontEscape) {
- $attrValue = Convert::raw2xml($attrValue);
- }
-
+ $attrValue = Convert::raw2xml($attrValue);
$inputValue = implode(', ', array_values($values));
} else {
$attrValue = '('._t('FormField.NONE', 'none').')';
@@ -58,7 +56,7 @@ public function Field($properties = array()) {
}
$properties = array_merge($properties, array(
- 'AttrValue' => $attrValue,
+ 'AttrValue' => DBField::create_field('HTMLFragment', $attrValue),
'InputValue' => $inputValue
));
diff --git a/forms/MoneyField.php b/forms/MoneyField.php
index 64362be65bf..7c89c9b0e43 100644
--- a/forms/MoneyField.php
+++ b/forms/MoneyField.php
@@ -52,15 +52,14 @@ public function __construct($name, $title = null, $value = "") {
/**
* @param array
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
- return DBField::create_field('HTMLText',
+ return
"
" .
"
" . $this->fieldCurrency->SmallFieldHolder() . "
" .
"
" . $this->fieldAmount->SmallFieldHolder() . "
" .
- "
"
- );
+ "";
}
/**
diff --git a/forms/NullableField.php b/forms/NullableField.php
index 9e403a7f94b..e48a9a51c3c 100644
--- a/forms/NullableField.php
+++ b/forms/NullableField.php
@@ -106,7 +106,7 @@ public function getIsNullId() {
/**
* @param array $properties
*
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
if($this->isReadonly()) {
@@ -117,12 +117,12 @@ public function Field($properties = array()) {
$nullableCheckbox->setValue(is_null($this->dataValue()));
- return DBField::create_field('HTMLText', sprintf(
+ return sprintf(
'%s %s %s',
$this->valueField->Field(),
$nullableCheckbox->Field(),
$this->getIsNullLabel()
- ));
+ );
}
/**
diff --git a/forms/NumericField.php b/forms/NumericField.php
index 8d5a4a9f0a7..0a31a15e5b0 100644
--- a/forms/NumericField.php
+++ b/forms/NumericField.php
@@ -199,10 +199,10 @@ public function performReadonlyTransformation() {
* @return string
*/
public function Value() {
- if($this->value) {
- return Convert::raw2xml((string) $this->value);
- }
+ return $this->value ?: '0';
+ }
- return '0';
+ public function getValueCast() {
+ return 'Decimal';
}
}
diff --git a/forms/PhoneNumberField.php b/forms/PhoneNumberField.php
index 563ceec1041..1040cc90702 100644
--- a/forms/PhoneNumberField.php
+++ b/forms/PhoneNumberField.php
@@ -31,7 +31,7 @@ public function __construct($name, $title = null, $value = '', $extension = null
/**
* @param array $properties
- * @return FieldGroup|HTMLText
+ * @return string
*/
public function Field($properties = array()) {
$fields = new FieldGroup( $this->name );
@@ -60,14 +60,17 @@ public function Field($properties = array()) {
}
$description = $this->getDescription();
- if($description) $fields->getChildren()->First()->setDescription($description);
+ if($description) {
+ $fields->getChildren()->first()->setDescription($description);
+ }
foreach($fields as $field) {
+ /** @var FormField $field */
$field->setDisabled($this->isDisabled());
$field->setReadonly($this->isReadonly());
}
- return $fields;
+ return $fields->Field($properties);
}
public function setValue( $value ) {
@@ -150,8 +153,7 @@ public function validate($validator){
$validator->validationError(
$this->name,
_t('PhoneNumberField.VALIDATION', "Please enter a valid phone number"),
- "validation",
- false
+ "validation"
);
return false;
}
diff --git a/forms/ReadonlyField.php b/forms/ReadonlyField.php
index a75f90dcb6d..f9987337dfd 100644
--- a/forms/ReadonlyField.php
+++ b/forms/ReadonlyField.php
@@ -40,7 +40,7 @@ public function performReadonlyTransformation() {
/**
* @param array $properties
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
// Include a hidden field in the HTML
@@ -54,6 +54,31 @@ public function Field($properties = array()) {
}
}
+ public function getAttributes() {
+ return array_merge(
+ parent::getAttributes(),
+ array(
+ 'type' => 'hidden',
+ 'value' => $this->readonly ? null : $this->value,
+ )
+ );
+ }
+
+ public function Type() {
+ return 'readonly';
+ }
+
+ public function castingHelper($field) {
+ // Get dynamic cast for 'Value' field
+ if(strcasecmp($field, 'Value') === 0) {
+ return $this->getValueCast();
+ }
+
+ // Fall back to default casting
+ return parent::castingHelper($field);
+ }
+
+
/**
* If $dontEscape is true the returned value will be plain text
* and should be escaped in templates via .XML
@@ -64,34 +89,31 @@ public function Field($properties = array()) {
* @return mixed|string
*/
public function Value() {
- if($this->value) {
- if($this->dontEscape) {
- return $this->value;
- } else {
- return Convert::raw2xml($this->value);
- }
- } else {
- $value = '(' . _t('FormField.NONE', 'none') . ')';
- if($this->dontEscape) {
- return $value;
- } else {
- return ''.Convert::raw2xml($value).'';
- }
+ // Get raw value
+ $value = $this->dataValue();
+ if($value) {
+ return $value;
}
- }
- public function getAttributes() {
- return array_merge(
- parent::getAttributes(),
- array(
- 'type' => 'hidden',
- 'value' => $this->readonly ? null : $this->value,
- )
- );
+ // "none" text
+ $label = _t('FormField.NONE', 'none');
+ return "('{$label}')";
}
- public function Type() {
- return 'readonly';
+ /**
+ * Get custom cating helper for Value() field
+ *
+ * @return string
+ */
+ public function getValueCast() {
+ // Casting class for 'none' text
+ $value = $this->dataValue();
+ if(empty($value)) {
+ return 'HTMLFragment';
+ }
+
+ // Use default casting
+ return $this->config()->casting['Value'];
}
}
diff --git a/forms/SelectionGroup.php b/forms/SelectionGroup.php
index cfcb7f93196..fa424cd871d 100644
--- a/forms/SelectionGroup.php
+++ b/forms/SelectionGroup.php
@@ -1,6 +1,8 @@
ID() . '_' . (++$count);
$extra = array(
- "RadioButton" => FormField::create_tag(
+ "RadioButton" => DBField::create_field('HTMLFragment', FormField::create_tag(
'input',
array(
'class' => 'selector',
@@ -93,12 +95,12 @@ public function FieldList() {
'value' => $item->getValue(),
'checked' => $checked
)
- ),
- "RadioLabel" => FormField::create_tag(
+ )),
+ "RadioLabel" => DBField::create_field('HTMLFragment', FormField::create_tag(
'label',
array('for' => $itemID),
$item->getTitle()
- ),
+ )),
"Selected" => $firstSelected,
);
$newItems[] = $item->customise($extra);
diff --git a/forms/TextareaField.php b/forms/TextareaField.php
index 9110b3d014d..3d8f4710984 100644
--- a/forms/TextareaField.php
+++ b/forms/TextareaField.php
@@ -26,7 +26,7 @@ class TextareaField extends FormField {
*/
private static $casting = array(
'Value' => 'Text',
- 'ValueEntities' => 'HTMLText',
+ 'ValueEntities' => 'HTMLFragment',
);
/**
@@ -119,8 +119,6 @@ public function Type() {
/**
* Return value with all values encoded in html entities
*
- * Invoke with $ValueEntities.RAW to suppress HTMLText parsing shortcodes.
- *
* @return string Raw HTML
*/
public function ValueEntities() {
diff --git a/forms/ToggleCompositeField.php b/forms/ToggleCompositeField.php
index a4d626ab611..909272a39dd 100644
--- a/forms/ToggleCompositeField.php
+++ b/forms/ToggleCompositeField.php
@@ -35,8 +35,7 @@ public function __construct($name, $title, $children) {
* @inheritdoc
*
* @param array $properties
- *
- * @return string|HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php
index 673d3c6c62e..7aeef45feb4 100644
--- a/forms/TreeDropdownField.php
+++ b/forms/TreeDropdownField.php
@@ -216,7 +216,7 @@ public function setNumChildrenMethod($method) {
/**
* @param array $properties
- * @return DBHTMLText
+ * @return string
*/
public function Field($properties = array()) {
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang');
@@ -398,8 +398,9 @@ public function tree(SS_HTTPRequest $request) {
* Marking public function for the tree, which combines different filters sensibly.
* If a filter function has been set, that will be called. And if search text is set,
* filter on that too. Return true if all applicable conditions are true, false otherwise.
- * @param object $node
- * @return mixed
+ *
+ * @param mixed $node
+ * @return bool
*/
public function filterMarking($node) {
if ($this->filterCallback && !call_user_func($this->filterCallback, $node)) return false;
diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php
index 384f1f55605..a4b87f2645e 100644
--- a/forms/gridfield/GridField.php
+++ b/forms/gridfield/GridField.php
@@ -5,7 +5,7 @@
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObjectInterface;
-
+use SilverStripe\ORM\FieldType\DBHTMLText;
/**
* Displays a {@link SS_List} in a grid format.
@@ -290,8 +290,7 @@ public function getState($getData = true) {
* Returns the whole gridfield rendered with all the attached components.
*
* @param array $properties
- *
- * @return HTMLText
+ * @return string
*/
public function FieldHolder($properties = array()) {
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
@@ -512,14 +511,11 @@ public function FieldHolder($properties = array()) {
$header . "\n" . $footer . "\n" . $body
);
- $field = DBField::create_field('HTMLText', FormField::create_tag(
+ return FormField::create_tag(
'fieldset',
$fieldsetAttributes,
$content['before'] . $table . $content['after']
- ));
- $field->setOptions(array('shortcodes' => false));
-
- return $field;
+ );
}
/**
@@ -604,8 +600,7 @@ protected function newRowClasses($total, $index, $record) {
/**
* @param array $properties
- *
- * @return HTMLText
+ * @return string
*/
public function Field($properties = array()) {
$this->extend('onBeforeRender', $this);
diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php
index 16832a020d8..8f16724e98d 100644
--- a/forms/gridfield/GridFieldDetailForm.php
+++ b/forms/gridfield/GridFieldDetailForm.php
@@ -1,10 +1,11 @@
getToplevelController();
diff --git a/forms/htmleditor/HTMLEditorField.php b/forms/htmleditor/HTMLEditorField.php
index b913a64e55f..ef72a0299eb 100644
--- a/forms/htmleditor/HTMLEditorField.php
+++ b/forms/htmleditor/HTMLEditorField.php
@@ -14,6 +14,10 @@
*/
class HTMLEditorField extends TextareaField {
+ private static $casting = [
+ 'Value' => 'HTMLText',
+ ];
+
/**
* Use TinyMCE's GZIP compressor
*
@@ -128,10 +132,7 @@ public function setValue($value) {
* @return HTMLEditorField_Readonly
*/
public function performReadonlyTransformation() {
- $field = $this->castedCopy('HTMLEditorField_Readonly');
- $field->dontEscape = true;
-
- return $field;
+ return $this->castedCopy('HTMLEditorField_Readonly');
}
public function performDisabledTransformation() {
@@ -150,7 +151,11 @@ public function Field($properties = array()) {
* @package forms
* @subpackage fields-formattedinput
*/
-class HTMLEditorField_Readonly extends ReadonlyField {
+class HTMLEditorField_Readonly extends HTMLReadonlyField {
+ private static $casting = [
+ 'Value' => 'HTMLText'
+ ];
+
public function Field($properties = array()) {
$valforInput = $this->value ? Convert::raw2att($this->value) : "";
return "id() . "\">"
diff --git a/parsers/BBCodeParser.php b/parsers/BBCodeParser.php
index c59360c7076..03c6f47b1bb 100644
--- a/parsers/BBCodeParser.php
+++ b/parsers/BBCodeParser.php
@@ -1,6 +1,8 @@
smilies_location;
- }
-
- /**
- * @deprecated 4.0 Use the "BBCodeParser.smilies_location" config setting instead
- */
- public static function set_icon_folder($path) {
- Deprecation::notice('4.0', 'Use the "BBCodeParser.smilies_location" config setting instead');
- static::config()->smilies_location = $path;
- }
-
- /**
- * @deprecated 4.0 Use the "BBCodeParser.autolink_urls" config setting instead
- */
- public static function autolinkUrls() {
- Deprecation::notice('4.0', 'Use the "BBCodeParser.autolink_urls" config setting instead');
- return static::config()->autolink_urls;
- }
-
- /**
- * @deprecated 4.0 Use the "BBCodeParser.autolink_urls" config setting instead
- */
- public static function disable_autolink_urls($autolink = false) {
- Deprecation::notice('4.0', 'Use the "BBCodeParser.autolink_urls" config setting instead');
- static::config()->autolink_urls = $autolink;
- }
-
- /**
- * @deprecated 4.0 Use the "BBCodeParser.allow_smilies" config setting instead
- */
- public static function smiliesAllowed() {
- Deprecation::notice('4.0', 'Use the "BBCodeParser.allow_smilies" config setting instead');
- return static::config()->allow_smilies;
- }
-
- /**
- * @deprecated 4.0 Use the "BBCodeParser.allow_smilies" config setting instead
- */
- public static function enable_smilies() {
- Deprecation::notice('4.0', 'Use the "BBCodeParser.allow_smilies" config setting instead');
- static::config()->allow_similies = true;
- }
-
-
public static function usable_tags() {
return new ArrayList(
array(
@@ -167,7 +117,7 @@ public function useable_tagsHTML(){
* Main BBCode parser method. This takes plain jane content and
* runs it through so many filters
*
- * @return Text
+ * @return DBField
*/
public function parse() {
$this->content = str_replace(array('&', '<', '>'), array('&', '<', '>'), $this->content);
@@ -197,7 +147,9 @@ public function parse() {
);
$this->content = preg_replace(array_keys($smilies), array_values($smilies), $this->content);
}
- return $this->content;
+
+ // Ensure to return cast value
+ return DBField::create_field('HTMLFragment', $this->content);
}
}
diff --git a/parsers/TextParser.php b/parsers/TextParser.php
index e09e1ebb033..97ed2d68510 100644
--- a/parsers/TextParser.php
+++ b/parsers/TextParser.php
@@ -26,6 +26,10 @@
* @subpackage misc
*/
abstract class TextParser extends Object {
+
+ /**
+ * @var string
+ */
protected $content;
/**
@@ -34,12 +38,15 @@ abstract class TextParser extends Object {
* @param string $content The contents of the dbfield
*/
public function __construct($content = "") {
+ parent::__construct();
$this->content = $content;
parent::__construct();
}
/**
* Convenience method, shouldn't really be used, but it's here if you want it
+ *
+ * @param string $content
*/
public function setContent($content = "") {
$this->content = $content;
@@ -48,6 +55,8 @@ public function setContent($content = "") {
/**
* Define your own parse method to parse $this->content appropriately.
* See the class doc-block for more implementation details.
+ *
+ * @return DBField
*/
abstract public function parse();
}
diff --git a/templates/RSSFeed.ss b/templates/RSSFeed.ss
index f4bc1ff971a..4975bbe9d2d 100644
--- a/templates/RSSFeed.ss
+++ b/templates/RSSFeed.ss
@@ -9,8 +9,8 @@
<% loop $Entries %>
$Title.XML
- $AbsoluteLink
- <% if $Description %>$Description.AbsoluteLinks.XML<% end_if %>
+ $AbsoluteLink.XML
+ <% if $Description %>$Description.AbsoluteLinks.CDATA<% end_if %>
<% if $Date %>$Date.Rfc822
<% else %>$Created.Rfc822<% end_if %>
<% if $Author %>$Author.XML<% end_if %>
diff --git a/tests/api/RSSFeedTest.php b/tests/api/RSSFeedTest.php
index 70bcbdef680..2e593659f5e 100644
--- a/tests/api/RSSFeedTest.php
+++ b/tests/api/RSSFeedTest.php
@@ -64,7 +64,7 @@ public function testRSSFeedWithShortcode() {
$this->assertContains('ItemD', $content);
$this->assertContains(
- '<p>ItemD Content test shortcode output</p>',
+ 'ItemD Content test shortcode output]]>',
$content
);
}
@@ -168,7 +168,7 @@ class RSSFeedTest_ItemD extends ViewableData {
// ItemD test fields - all fields use casting but Content & AltContent cast as HTMLText
private static $casting = array(
'Title' => 'Varchar',
- 'Content' => 'HTMLText'
+ 'Content' => 'HTMLText', // Supports shortcodes
);
public $Title = 'ItemD';
diff --git a/tests/email/EmailTest.php b/tests/email/EmailTest.php
index 4e9dfaf3cac..ad44e1a6e25 100644
--- a/tests/email/EmailTest.php
+++ b/tests/email/EmailTest.php
@@ -165,7 +165,7 @@ public function testSendHTML() {
'from@example.com',
'to@example.com',
'Test send plain',
- 'Testing Email->sendPlain()',
+ 'Testing Email->send()',
null,
'cc@example.com',
'bcc@example.com'
@@ -180,7 +180,7 @@ public function testSendHTML() {
$this->assertEquals('to@example.com', $sent['to']);
$this->assertEquals('from@example.com', $sent['from']);
$this->assertEquals('Test send plain', $sent['subject']);
- $this->assertContains('Testing Email->sendPlain()', $sent['content']);
+ $this->assertContains('Testing Email->send()', $sent['content']);
$this->assertNull($sent['plaincontent']);
$this->assertEquals(
array(
diff --git a/tests/forms/LookupFieldTest.php b/tests/forms/LookupFieldTest.php
index 17558a93e2b..99112389e09 100644
--- a/tests/forms/LookupFieldTest.php
+++ b/tests/forms/LookupFieldTest.php
@@ -35,10 +35,9 @@ public function testStringValueWithNumericArraySource() {
public function testUnknownStringValueWithNumericArraySource() {
$source = array(1 => 'one', 2 => 'two', 3 => 'three');
$f = new LookupField('test', 'test', $source);
- $f->setValue('w00t');
- $f->dontEscape = true; // simulates CMSMain->compareversions()
+ $f->setValue('w00t');
$this->assertEquals(
- 'w00t',
+ 'w00t',
trim($f->Field()->getValue())
);
}
diff --git a/tests/model/DBFieldTest.php b/tests/model/DBFieldTest.php
index 6ad22393747..52046087c2f 100644
--- a/tests/model/DBFieldTest.php
+++ b/tests/model/DBFieldTest.php
@@ -211,7 +211,7 @@ public function testExists() {
public function testStringFieldsWithMultibyteData() {
$plainFields = array('Varchar', 'Text');
- $htmlFields = array('HTMLVarchar', 'HTMLText');
+ $htmlFields = array('HTMLVarchar', 'HTMLText', 'HTMLFragment');
$allFields = array_merge($plainFields, $htmlFields);
$value = 'üåäöÜÅÄÖ';
diff --git a/tests/model/DBHTMLTextTest.php b/tests/model/DBHTMLTextTest.php
index f1f53dddfb3..35e4c0ae943 100644
--- a/tests/model/DBHTMLTextTest.php
+++ b/tests/model/DBHTMLTextTest.php
@@ -185,14 +185,14 @@ function testExists() {
}
function testWhitelist() {
- $textObj = new DBHTMLText('Test', 'meta,link');
+ $textObj = new DBHTMLText('Test', 'whitelist=meta,link');
$this->assertEquals(
'',
$textObj->whitelistContent('
Remove
Remove Text'),
'Removes any elements not in whitelist excluding text elements'
);
- $textObj = new DBHTMLText('Test', 'meta,link,text()');
+ $textObj = new DBHTMLText('Test', 'whitelist=meta,link,text()');
$this->assertEquals(
'Keep Text',
$textObj->whitelistContent('
Remove
Keep Text'),
diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php
index 04b79601c29..6f604cb0664 100644
--- a/tests/security/MemberTest.php
+++ b/tests/security/MemberTest.php
@@ -236,7 +236,7 @@ public function testForgotPasswordEmaling() {
// Check existance of reset link
$this->assertEmailSent("testuser@example.com", null, 'Your password reset link',
- '/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/');
+ '/Security\/changepassword\?m='.$member->ID.'&t=[^"]+/');
}
/**
diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php
index 2f92f494e67..c5bb6297c2a 100644
--- a/tests/view/SSViewerTest.php
+++ b/tests/view/SSViewerTest.php
@@ -5,7 +5,7 @@
use SilverStripe\Security\Member;
use SilverStripe\Security\SecurityToken;
use SilverStripe\Security\Permission;
-
+use SilverStripe\Model\FieldType\DBField;
class SSViewerTest extends SapphireTest {
@@ -835,21 +835,17 @@ public function testCastingHelpers() {
$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
);
- // Uncasted value (falls back to ViewableData::$default_cast="HTMLText")
- $vd = new SSViewerTest_ViewableData(); // TODO Fix caching
+ // Uncasted value (falls back to ViewableData::$default_cast="Text")
+ $vd = new SSViewerTest_ViewableData();
$vd->UncastedValue = 'html';
$this->assertEquals(
- 'html',
+ '<b>html</b>',
$t = SSViewer::fromString('$UncastedValue')->process($vd)
);
- $vd = new SSViewerTest_ViewableData(); // TODO Fix caching
- $vd->UncastedValue = 'html';
$this->assertEquals(
'html',
$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
);
- $vd = new SSViewerTest_ViewableData(); // TODO Fix caching
- $vd->UncastedValue = 'html';
$this->assertEquals(
'<b>html</b>',
$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
@@ -1247,8 +1243,14 @@ public function testRewriteHashlinks() {