Skip to content

Commit

Permalink
API Enforce default_cast for all field usages
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Damian Mooyman committed Jun 17, 2016
1 parent 9b9bef7 commit b7dfeb6
Show file tree
Hide file tree
Showing 72 changed files with 1,510 additions and 1,341 deletions.
4 changes: 4 additions & 0 deletions _config/model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Injector:
class: SilverStripe\Model\FieldType\DBForeignKey
HTMLText:
class: SilverStripe\Model\FieldType\DBHTMLText
properties:
ProcessShortcodes: true
HTMLFragment:
class: SilverStripe\Model\FieldType\DBHTMLText
HTMLVarchar:
class: SilverStripe\Model\FieldType\DBHTMLVarchar
Int:
Expand Down
30 changes: 13 additions & 17 deletions api/RSSFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,45 +295,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;
}

/**
Expand Down
17 changes: 11 additions & 6 deletions api/XMLDataFormatter.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php
use SilverStripe\Model\FieldType\DBHTMLText;

/**
* @package framework
* @subpackage formatters
Expand Down Expand Up @@ -50,18 +52,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('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $fieldValue));
} else {
$fieldValue = Convert::raw2xml($fieldValue);
}
$xml .= "<$fieldName>$fieldValue</$fieldName>\n";
}
Expand Down
48 changes: 30 additions & 18 deletions core/CustomMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,7 @@ the database. However, the template engine knows to escape fields without the `
to prevent them from rendering HTML interpreted by browsers. This escaping prevents attacks like CSRF or XSS (see
"[security](../security)"), which is important if these fields store user-provided data.

<div class="hint" markdown="1">
You can disable this auto-escaping by using the `$MyField.RAW` escaping hints, or explicitly request escaping of HTML
content via `$MyHtmlField.XML`.
</div>
See the [Template casting](/developer_guides/templates/casting) section for controlling casting in your templates.

## Overloading

Expand Down Expand Up @@ -220,4 +217,4 @@ database column using `dbObject`.
## API Documentation

* [api:DataObject]
* [api:DBField]
* [api:DBField]
52 changes: 46 additions & 6 deletions docs/en/02_Developer_Guides/01_Templates/09_Casting.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,50 @@ this purpose.
There's some exceptions to this rule, see the ["security" guide](../security).
</div>

In case you want to explicitly allow un-escaped HTML input, the property can be cast as [api:HTMLText]. The following
example takes the `Content` field in a `SiteTree` class, which is of this type. It forces the content into an explicitly
escaped format.
For every field used in templates, a casting helper will be applied. This will first check for any
`casting` helper on your model specific to that field, and will fall back to the `default_cast` config
in case none are specified.

By default, `ViewableData.default_cast` is set to `Text`, which will ensure all fields have special
characters HTML escaped by default.

The most common casting types are:

* `Text` Which is a plain text string, and will be safely encoded via HTML entities when placed into
a template.
* `Varchar` which is the same as `Text` but for single-line text that should not have line breaks.
* `HTMLFragment` is a block of raw HTML, which should not be escaped. Take care to sanitise any HTML
value saved into the database.
* `HTMLText` is a `HTMLFragment`, but has shortcodes enabled. This should only be used for content
that is modified via a TinyMCE editor, which will insert shortcodes.
* `Int` for integers.
* `Decimal` for floating point values.
* `Boolean` For boolean values.
* `Datetime` for date and time.

See the [Model data types and casting](/developer_guides/model/data_types_and_casting) section for
instructions on configuring your model to declare casting types for fields.

## Escape methods in templates

Within the template, fields can have their encoding customised at a certain level with format methods.
See [api:DBField] for the specific implementation, but they will generally follow the below rules:

* `$Field` with no format method supplied will correctly cast itself for the HTML template, as defined
by the casting helper for that field. In most cases this is the best method to use for templates.
* `$Field.XML` Will invoke `htmlentities` on special characters in the value, even if it's already
cast as HTML.
* `$Field.ATT` will ensure the field is XML encoded for placement inside a HTML element property.
This will invoke `htmlentities` on the value (even if already cast as HTML) and will escape quotes.
* `Field.JS` will cast this value as a javascript string. E.g. `var fieldVal = '$Field.JS';` can
be used in javascript defined in templates to encode values safely.
* `$Field.CDATA` will cast this value safely for insertion as a literal string in an XML file.
E.g. `<element>$Field.CDATA</element>` will ensure that the `<element>` body is safely escaped
as a string.

<div class="warning" markdown="1">
Note: Take care when using `.XML` on `HTMLText` fields, as this will result in double-encoded
html. To ensure that the correct encoding is used for that field in a template, simply use
`$Field` by itself to allow the casting helper to determine the best encoding itself.
</div>

:::ss
$Content.XML
// transforms e.g. "<em>alert</em>" to "&lt;em&gt;alert&lt;/em&gt;"
66 changes: 65 additions & 1 deletion docs/en/04_Changelogs/4.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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

Expand Down Expand Up @@ -98,6 +103,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

Expand Down Expand Up @@ -167,7 +173,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
Expand Down Expand Up @@ -301,6 +307,64 @@ E.g.


## Upgrading

### Explicit text casting is now enforced on all template variables

Now whenever a `$Variable` is used in a template, regardless of whether any casts or methods are
suffixed to the reference, it will be cast to either an explicit DBField for that field, or
the value declared by the `default_cast` on the parent object.

The default value of `default_cast` is `Text`, meaning that now many cases where a field was
left un-uncoded, this will now be safely encoded via `Convert::raw2xml`. In cases where
un-cast fields were used to place raw HTML into templates, this will now encode this until
explicitly cast for that field.

You can resolve this in your model by adding an explicit cast to HTML for those fields.


Before:


:::ss
<div>
$SomeHTML
</div>


:::php
class MyObject extends ViewableData {
public function getSomeHTML {
$title = Convert::raw2xml($this->Title);
return "<h1>{$title}</h1>";
}
}


After:


:::ss
<div>
$SomeHTML
</div>


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

If you need to encode a field (such as HTMLText) for use in html attributes, use `.ATT`
instead, or if used in an actual XML file use `.CDATA`.

See the [Template casting](/developer_guides/templates/casting) section for specific details.

### Update code that uses SQLQuery

Expand Down
12 changes: 7 additions & 5 deletions filesystem/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
);

private static $casting = array (
'TreeTitle' => 'HTMLText'
'TreeTitle' => 'HTMLFragment'
);

/**
Expand Down Expand Up @@ -449,12 +449,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('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->Link())
)
->setDontEscape(true),
),
new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
new DateField_Disabled("LastEdited", _t('AssetTableField.LASTEDIT','Last changed') . ':')
)
Expand Down Expand Up @@ -623,7 +622,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;
Expand Down
Loading

0 comments on commit b7dfeb6

Please sign in to comment.