Attribute.php 10.2 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Template\Attribute.
6
7
8
9
 */

namespace Drupal\Core\Template;

10
use Drupal\Component\Render\PlainTextOutput;
11
use Drupal\Component\Utility\SafeMarkup;
12
use Drupal\Component\Render\MarkupInterface;
13
14

/**
15
 * Collects, sanitizes, and renders HTML attributes.
16
 *
17
18
 * To use, optionally pass in an associative array of defined attributes, or
 * add attributes using array syntax. For example:
19
20
21
22
 * @code
 *  $attributes = new Attribute(array('id' => 'socks'));
 *  $attributes['class'] = array('black-cat', 'white-cat');
 *  $attributes['class'][] = 'black-white-cat';
23
 *  echo '<cat' . $attributes . '>';
24
25
26
 *  // Produces <cat id="socks" class="black-cat white-cat black-white-cat">
 * @endcode
 *
27
 * $attributes always prints out all the attributes. For example:
28
29
30
31
 * @code
 *  $attributes = new Attribute(array('id' => 'socks'));
 *  $attributes['class'] = array('black-cat', 'white-cat');
 *  $attributes['class'][] = 'black-white-cat';
32
 *  echo '<cat class="cat ' . $attributes['class'] . '"' . $attributes . '>';
33
 *  // Produces <cat class="cat black-cat white-cat black-white-cat" id="socks" class="cat black-cat white-cat black-white-cat">
34
 * @endcode
35
36
37
38
39
40
41
42
43
 *
 * 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
 *
44
45
46
47
48
49
50
51
52
53
54
55
 * 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(&quot;xss&quot;);">
 * @endcode
56
 *
57
58
59
60
61
62
63
64
65
66
 * 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 &lt;em&gt; tag">
 * @endcode
 *
67
 * @see \Drupal\Component\Utility\Html::escape()
68
 * @see \Drupal\Component\Render\PlainTextOutput::renderFromHtml()
69
 * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
70
 */
71
class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
72

73
74
75
  /**
   * Stores the attribute data.
   *
76
   * @var \Drupal\Core\Template\AttributeValueBase[]
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
   */
  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);
    }
  }

  /**
   * Implements ArrayAccess::offsetGet().
   */
  public function offsetGet($name) {
    if (isset($this->storage[$name])) {
      return $this->storage[$name];
    }
  }

  /**
   * Implements ArrayAccess::offsetSet().
   */
  public function offsetSet($name, $value) {
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    $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) {
120
121
    // If the value is already an AttributeValueBase object, return it
    // straight away.
122
    if ($value instanceof AttributeValueBase) {
123
124
125
126
      return $value;
    }
    // An array value or 'class' attribute name are forced to always be an
    // AttributeArray value for consistency.
127
    if ($name == 'class' && !is_array($value)) {
128
      // Cast the value to string in case it implements MarkupInterface.
129
130
131
      $value = [(string) $value];
    }
    if (is_array($value)) {
132
133
134
      // 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.
135
      $value = new AttributeArray($name, $value);
136
137
138
139
    }
    elseif (is_bool($value)) {
      $value = new AttributeBoolean($name, $value);
    }
140
    // As a development aid, we allow the value to be a safe string object.
141
142
143
144
145
146
147
    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)) {
148
149
      $value = new AttributeString($name, $value);
    }
150
    return $value;
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
  }

  /**
   * Implements ArrayAccess::offsetUnset().
   */
  public function offsetUnset($name) {
    unset($this->storage[$name]);
  }

  /**
   * Implements ArrayAccess::offsetExists().
   */
  public function offsetExists($name) {
    return isset($this->storage[$name]);
  }

167
  /**
168
   * Adds classes or merges them on to array of existing CSS classes.
169
170
171
172
173
174
175
176
   *
   * @param string|array ...
   *   CSS classes to add to the class attribute array.
   *
   * @return $this
   */
  public function addClass() {
    $args = func_get_args();
177
178
179
180
181
182
183
184
    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);
      }
185

186
      // Merge if there are values, just add them otherwise.
187
      if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
188
189
190
191
192
193
194
        // 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);
      }
195
196
197
198
199
    }

    return $this;
  }

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;
  }

241
242
243
244
245
246
247
248
249
250
  /**
   * 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.
251
    if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
252
253
254
255
256
257
258
259
260
      $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);
      }

261
262
263
      // 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));
264
265
266
267
268
      $this->storage['class']->exchangeArray($classes);
    }
    return $this;
  }

269
270
271
272
273
274
275
276
277
278
  /**
   * 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) {
279
    if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
280
281
282
283
284
285
286
      return in_array($class, $this->storage['class']->value());
    }
    else {
      return FALSE;
    }
  }

287
288
289
290
291
  /**
   * Implements the magic __toString() method.
   */
  public function __toString() {
    $return = '';
292
    /** @var \Drupal\Core\Template\AttributeValueBase $value */
293
    foreach ($this->storage as $name => $value) {
294
295
296
      $rendered = $value->render();
      if ($rendered) {
        $return .= ' ' . $rendered;
297
298
      }
    }
299
    return $return;
300
301
  }

302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
  /**
   * 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;
  }

317
318
319
320
321
  /**
   * Implements the magic __clone() method.
   */
  public function  __clone() {
    foreach ($this->storage as $name => $value) {
322
      $this->storage[$name] = clone $value;
323
324
325
326
327
328
329
    }
  }

  /**
   * Implements IteratorAggregate::getIterator().
   */
  public function getIterator() {
330
    return new \ArrayIterator($this->storage);
331
332
333
334
335
  }

  /**
   * Returns the whole array.
   */
336
337
  public function storage() {
    return $this->storage;
338
339
  }

340
341
342
343
344
345
346
347
348
349
  /**
   * Returns a representation of the object for use in JSON serialization.
   *
   * @return string
   *   The safe string content.
   */
  public function jsonSerialize() {
    return (string) $this;
  }

350
}