Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Contains \Drupal\Core\Template\Attribute.
Dries Buytaert
committed
*/
namespace Drupal\Core\Template;
Alex Bronstein
committed
use Drupal\Component\Render\PlainTextOutput;
catch
committed
use Drupal\Component\Utility\SafeMarkup;
Alex Bronstein
committed
use Drupal\Component\Render\MarkupInterface;
Dries Buytaert
committed
/**
* Collects, sanitizes, and renders HTML attributes.
Dries Buytaert
committed
*
* To use, optionally pass in an associative array of defined attributes, or
* add attributes using array syntax. For example:
Dries Buytaert
committed
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
Alex Pott
committed
* echo '<cat' . $attributes . '>';
Dries Buytaert
committed
* // Produces <cat id="socks" class="black-cat white-cat black-white-cat">
* @endcode
*
* $attributes always prints out all the attributes. For example:
Dries Buytaert
committed
* @code
* $attributes = new Attribute(array('id' => 'socks'));
* $attributes['class'] = array('black-cat', 'white-cat');
* $attributes['class'][] = 'black-white-cat';
Alex Pott
committed
* echo '<cat class="cat ' . $attributes['class'] . '"' . $attributes . '>';
* // Produces <cat class="cat black-cat white-cat black-white-cat" id="socks" class="cat black-cat white-cat black-white-cat">
Dries Buytaert
committed
* @endcode
*
* When printing out individual attributes to customize them within a Twig
* template, use the "without" filter to prevent attributes that have already
* been printed from being printed again. For example:
* @code
* <cat class="{{ attributes.class }} my-custom-class"{{ attributes|without('class') }}>
* {# Produces <cat class="cat black-cat white-cat black-white-cat my-custom-class" id="socks"> #}
* @endcode
*
* The attribute keys and values are automatically escaped for output with
* Html::escape(). No protocol filtering is applied, so when using user-entered
* input as a value for an attribute that expects an URI (href, src, ...),
* UrlHelper::stripDangerousProtocols() should be used to ensure dangerous
* protocols (such as 'javascript:') are removed. For example:
* @code
* $path = 'javascript:alert("xss");';
* $path = UrlHelper::stripDangerousProtocols($path);
* $attributes = new Attribute(array('href' => $path));
* echo '<a' . $attributes . '>';
* // Produces <a href="alert("xss");">
* @endcode
catch
committed
*
catch
committed
* The attribute values are considered plain text and are treated as such. If a
* safe HTML string is detected, it is converted to plain text with
* PlainTextOutput::renderFromHtml() before being escaped. For example:
* @code
* $value = t('Highlight the @tag tag', ['@tag' => '<em>']);
* $attributes = new Attribute(['value' => $value]);
* echo '<input' . $attributes . '>';
* // Produces <input value="Highlight the <em> tag">
* @endcode
*
catch
committed
* @see \Drupal\Component\Utility\Html::escape()
Alex Bronstein
committed
* @see \Drupal\Component\Render\PlainTextOutput::renderFromHtml()
* @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
Dries Buytaert
committed
*/
Alex Bronstein
committed
class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
Dries Buytaert
committed
/**
* Stores the attribute data.
*
* @var \Drupal\Core\Template\AttributeValueBase[]
Dries Buytaert
committed
*/
protected $storage = array();
/**
* Constructs a \Drupal\Core\Template\Attribute object.
*
* @param array $attributes
* An associative array of key-value pairs to be converted to attributes.
*/
public function __construct($attributes = array()) {
foreach ($attributes as $name => $value) {
$this->offsetSet($name, $value);
}
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function offsetGet($name) {
if (isset($this->storage[$name])) {
return $this->storage[$name];
}
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function offsetSet($name, $value) {
Alex Pott
committed
$this->storage[$name] = $this->createAttributeValue($name, $value);
}
/**
* Creates the different types of attribute values.
*
* @param string $name
* The attribute name.
* @param mixed $value
* The attribute value.
*
* @return \Drupal\Core\Template\AttributeValueBase
* An AttributeValueBase representation of the attribute's value.
*/
protected function createAttributeValue($name, $value) {
// If the value is already an AttributeValueBase object, return it
// straight away.
if ($value instanceof AttributeValueBase) {
return $value;
}
// An array value or 'class' attribute name are forced to always be an
// AttributeArray value for consistency.
if ($name == 'class' && !is_array($value)) {
Alex Bronstein
committed
// Cast the value to string in case it implements MarkupInterface.
$value = [(string) $value];
}
if (is_array($value)) {
// Cast the value to an array if the value was passed in as a string.
// @todo Decide to fix all the broken instances of class as a string
// in core or cast them.
$value = new AttributeArray($name, $value);
Dries Buytaert
committed
}
elseif (is_bool($value)) {
$value = new AttributeBoolean($name, $value);
}
Alex Bronstein
committed
// As a development aid, we allow the value to be a safe string object.
catch
committed
elseif (SafeMarkup::isSafe($value)) {
// Attributes are not supposed to display HTML markup, so we just convert
// the value to plain text.
$value = PlainTextOutput::renderFromHtml($value);
$value = new AttributeString($name, $value);
}
elseif (!is_object($value)) {
Dries Buytaert
committed
$value = new AttributeString($name, $value);
}
Alex Pott
committed
return $value;
Dries Buytaert
committed
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function offsetUnset($name) {
unset($this->storage[$name]);
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function offsetExists($name) {
return isset($this->storage[$name]);
}
/**
* Adds classes or merges them on to array of existing CSS classes.
*
* @param string|array ...
* CSS classes to add to the class attribute array.
*
* @return $this
*/
public function addClass() {
$args = func_get_args();
if ($args) {
$classes = array();
foreach ($args as $arg) {
// Merge the values passed in from the classes array.
// The argument is cast to an array to support comma separated single
// values or one or more array arguments.
$classes = array_merge($classes, (array) $arg);
}
// Merge if there are values, just add them otherwise.
if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
// Merge the values passed in from the class value array.
$classes = array_merge($this->storage['class']->value(), $classes);
$this->storage['class']->exchangeArray($classes);
}
else {
$this->offsetSet('class', $classes);
}
}
return $this;
}
Alex Pott
committed
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/**
* Sets values for an attribute key.
*
* @param string $attribute
* Name of the attribute.
* @param string|array $value
* Value(s) to set for the given attribute key.
*
* @return $this
*/
public function setAttribute($attribute, $value) {
$this->offsetSet($attribute, $value);
return $this;
}
/**
* Removes an attribute from an Attribute object.
*
* @param string|array ...
* Attributes to remove from the attribute array.
*
* @return $this
*/
public function removeAttribute() {
$args = func_get_args();
foreach ($args as $arg) {
// Support arrays or multiple arguments.
if (is_array($arg)) {
foreach ($arg as $value) {
unset($this->storage[$value]);
}
}
else {
unset($this->storage[$arg]);
}
}
return $this;
}
/**
* Removes argument values from array of existing CSS classes.
*
* @param string|array ...
* CSS classes to remove from the class attribute array.
*
* @return $this
*/
public function removeClass() {
// With no class attribute, there is no need to remove.
if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
$args = func_get_args();
$classes = array();
foreach ($args as $arg) {
// Merge the values passed in from the classes array.
// The argument is cast to an array to support comma separated single
// values or one or more array arguments.
$classes = array_merge($classes, (array) $arg);
}
// Remove the values passed in from the value array. Use array_values() to
// ensure that the array index remains sequential.
$classes = array_values(array_diff($this->storage['class']->value(), $classes));
$this->storage['class']->exchangeArray($classes);
}
return $this;
}
/**
* Checks if the class array has the given CSS class.
*
* @param string $class
* The CSS class to check for.
*
* @return bool
* Returns TRUE if the class exists, or FALSE otherwise.
*/
public function hasClass($class) {
if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
return in_array($class, $this->storage['class']->value());
}
else {
return FALSE;
}
}
Dries Buytaert
committed
/**
* Implements the magic __toString() method.
*/
public function __toString() {
$return = '';
/** @var \Drupal\Core\Template\AttributeValueBase $value */
Dries Buytaert
committed
foreach ($this->storage as $name => $value) {
$rendered = $value->render();
if ($rendered) {
$return .= ' ' . $rendered;
Dries Buytaert
committed
}
}
return $return;
Dries Buytaert
committed
}
/**
* Returns all storage elements as an array.
*
* @return array
* An associative array of attributes.
*/
public function toArray() {
$return = [];
foreach ($this->storage as $name => $value) {
$return[$name] = $value->value();
}
return $return;
}
Dries Buytaert
committed
/**
* Implements the magic __clone() method.
*/
public function __clone() {
foreach ($this->storage as $name => $value) {
Alex Pott
committed
$this->storage[$name] = clone $value;
Dries Buytaert
committed
}
}
/**
* {@inheritdoc}
Dries Buytaert
committed
*/
public function getIterator() {
return new \ArrayIterator($this->storage);
Dries Buytaert
committed
}
/**
* Returns the whole array.
*/
public function storage() {
return $this->storage;
Dries Buytaert
committed
}
/**
* Returns a representation of the object for use in JSON serialization.
*
* @return string
* The safe string content.
*/
public function jsonSerialize() {
return (string) $this;
}