Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\Template;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Render\PlainTextOutput;
|
Chris@0
|
6 use Drupal\Component\Render\MarkupInterface;
|
Chris@0
|
7
|
Chris@0
|
8 /**
|
Chris@0
|
9 * Collects, sanitizes, and renders HTML attributes.
|
Chris@0
|
10 *
|
Chris@0
|
11 * To use, optionally pass in an associative array of defined attributes, or
|
Chris@0
|
12 * add attributes using array syntax. For example:
|
Chris@0
|
13 * @code
|
Chris@0
|
14 * $attributes = new Attribute(array('id' => 'socks'));
|
Chris@0
|
15 * $attributes['class'] = array('black-cat', 'white-cat');
|
Chris@0
|
16 * $attributes['class'][] = 'black-white-cat';
|
Chris@0
|
17 * echo '<cat' . $attributes . '>';
|
Chris@0
|
18 * // Produces <cat id="socks" class="black-cat white-cat black-white-cat">
|
Chris@0
|
19 * @endcode
|
Chris@0
|
20 *
|
Chris@0
|
21 * $attributes always prints out all the attributes. For example:
|
Chris@0
|
22 * @code
|
Chris@0
|
23 * $attributes = new Attribute(array('id' => 'socks'));
|
Chris@0
|
24 * $attributes['class'] = array('black-cat', 'white-cat');
|
Chris@0
|
25 * $attributes['class'][] = 'black-white-cat';
|
Chris@0
|
26 * echo '<cat class="cat ' . $attributes['class'] . '"' . $attributes . '>';
|
Chris@0
|
27 * // Produces <cat class="cat black-cat white-cat black-white-cat" id="socks" class="cat black-cat white-cat black-white-cat">
|
Chris@0
|
28 * @endcode
|
Chris@0
|
29 *
|
Chris@0
|
30 * When printing out individual attributes to customize them within a Twig
|
Chris@0
|
31 * template, use the "without" filter to prevent attributes that have already
|
Chris@0
|
32 * been printed from being printed again. For example:
|
Chris@0
|
33 * @code
|
Chris@0
|
34 * <cat class="{{ attributes.class }} my-custom-class"{{ attributes|without('class') }}>
|
Chris@0
|
35 * {# Produces <cat class="cat black-cat white-cat black-white-cat my-custom-class" id="socks"> #}
|
Chris@0
|
36 * @endcode
|
Chris@0
|
37 *
|
Chris@0
|
38 * The attribute keys and values are automatically escaped for output with
|
Chris@0
|
39 * Html::escape(). No protocol filtering is applied, so when using user-entered
|
Chris@0
|
40 * input as a value for an attribute that expects an URI (href, src, ...),
|
Chris@0
|
41 * UrlHelper::stripDangerousProtocols() should be used to ensure dangerous
|
Chris@0
|
42 * protocols (such as 'javascript:') are removed. For example:
|
Chris@0
|
43 * @code
|
Chris@0
|
44 * $path = 'javascript:alert("xss");';
|
Chris@0
|
45 * $path = UrlHelper::stripDangerousProtocols($path);
|
Chris@0
|
46 * $attributes = new Attribute(array('href' => $path));
|
Chris@0
|
47 * echo '<a' . $attributes . '>';
|
Chris@0
|
48 * // Produces <a href="alert("xss");">
|
Chris@0
|
49 * @endcode
|
Chris@0
|
50 *
|
Chris@0
|
51 * The attribute values are considered plain text and are treated as such. If a
|
Chris@0
|
52 * safe HTML string is detected, it is converted to plain text with
|
Chris@0
|
53 * PlainTextOutput::renderFromHtml() before being escaped. For example:
|
Chris@0
|
54 * @code
|
Chris@0
|
55 * $value = t('Highlight the @tag tag', ['@tag' => '<em>']);
|
Chris@0
|
56 * $attributes = new Attribute(['value' => $value]);
|
Chris@0
|
57 * echo '<input' . $attributes . '>';
|
Chris@0
|
58 * // Produces <input value="Highlight the <em> tag">
|
Chris@0
|
59 * @endcode
|
Chris@0
|
60 *
|
Chris@0
|
61 * @see \Drupal\Component\Utility\Html::escape()
|
Chris@0
|
62 * @see \Drupal\Component\Render\PlainTextOutput::renderFromHtml()
|
Chris@0
|
63 * @see \Drupal\Component\Utility\UrlHelper::stripDangerousProtocols()
|
Chris@0
|
64 */
|
Chris@0
|
65 class Attribute implements \ArrayAccess, \IteratorAggregate, MarkupInterface {
|
Chris@0
|
66
|
Chris@0
|
67 /**
|
Chris@0
|
68 * Stores the attribute data.
|
Chris@0
|
69 *
|
Chris@0
|
70 * @var \Drupal\Core\Template\AttributeValueBase[]
|
Chris@0
|
71 */
|
Chris@0
|
72 protected $storage = [];
|
Chris@0
|
73
|
Chris@0
|
74 /**
|
Chris@0
|
75 * Constructs a \Drupal\Core\Template\Attribute object.
|
Chris@0
|
76 *
|
Chris@0
|
77 * @param array $attributes
|
Chris@0
|
78 * An associative array of key-value pairs to be converted to attributes.
|
Chris@0
|
79 */
|
Chris@0
|
80 public function __construct($attributes = []) {
|
Chris@0
|
81 foreach ($attributes as $name => $value) {
|
Chris@0
|
82 $this->offsetSet($name, $value);
|
Chris@0
|
83 }
|
Chris@0
|
84 }
|
Chris@0
|
85
|
Chris@0
|
86 /**
|
Chris@0
|
87 * {@inheritdoc}
|
Chris@0
|
88 */
|
Chris@0
|
89 public function offsetGet($name) {
|
Chris@0
|
90 if (isset($this->storage[$name])) {
|
Chris@0
|
91 return $this->storage[$name];
|
Chris@0
|
92 }
|
Chris@0
|
93 }
|
Chris@0
|
94
|
Chris@0
|
95 /**
|
Chris@0
|
96 * {@inheritdoc}
|
Chris@0
|
97 */
|
Chris@0
|
98 public function offsetSet($name, $value) {
|
Chris@0
|
99 $this->storage[$name] = $this->createAttributeValue($name, $value);
|
Chris@0
|
100 }
|
Chris@0
|
101
|
Chris@0
|
102 /**
|
Chris@0
|
103 * Creates the different types of attribute values.
|
Chris@0
|
104 *
|
Chris@0
|
105 * @param string $name
|
Chris@0
|
106 * The attribute name.
|
Chris@0
|
107 * @param mixed $value
|
Chris@0
|
108 * The attribute value.
|
Chris@0
|
109 *
|
Chris@0
|
110 * @return \Drupal\Core\Template\AttributeValueBase
|
Chris@0
|
111 * An AttributeValueBase representation of the attribute's value.
|
Chris@0
|
112 */
|
Chris@0
|
113 protected function createAttributeValue($name, $value) {
|
Chris@0
|
114 // If the value is already an AttributeValueBase object,
|
Chris@0
|
115 // return a new instance of the same class, but with the new name.
|
Chris@0
|
116 if ($value instanceof AttributeValueBase) {
|
Chris@0
|
117 $class = get_class($value);
|
Chris@0
|
118 return new $class($name, $value->value());
|
Chris@0
|
119 }
|
Chris@0
|
120 // An array value or 'class' attribute name are forced to always be an
|
Chris@0
|
121 // AttributeArray value for consistency.
|
Chris@0
|
122 if ($name == 'class' && !is_array($value)) {
|
Chris@0
|
123 // Cast the value to string in case it implements MarkupInterface.
|
Chris@0
|
124 $value = [(string) $value];
|
Chris@0
|
125 }
|
Chris@0
|
126 if (is_array($value)) {
|
Chris@0
|
127 // Cast the value to an array if the value was passed in as a string.
|
Chris@0
|
128 // @todo Decide to fix all the broken instances of class as a string
|
Chris@0
|
129 // in core or cast them.
|
Chris@0
|
130 $value = new AttributeArray($name, $value);
|
Chris@0
|
131 }
|
Chris@0
|
132 elseif (is_bool($value)) {
|
Chris@0
|
133 $value = new AttributeBoolean($name, $value);
|
Chris@0
|
134 }
|
Chris@0
|
135 // As a development aid, we allow the value to be a safe string object.
|
Chris@0
|
136 elseif ($value instanceof MarkupInterface) {
|
Chris@0
|
137 // Attributes are not supposed to display HTML markup, so we just convert
|
Chris@0
|
138 // the value to plain text.
|
Chris@0
|
139 $value = PlainTextOutput::renderFromHtml($value);
|
Chris@0
|
140 $value = new AttributeString($name, $value);
|
Chris@0
|
141 }
|
Chris@0
|
142 elseif (!is_object($value)) {
|
Chris@0
|
143 $value = new AttributeString($name, $value);
|
Chris@0
|
144 }
|
Chris@0
|
145 return $value;
|
Chris@0
|
146 }
|
Chris@0
|
147
|
Chris@0
|
148 /**
|
Chris@0
|
149 * {@inheritdoc}
|
Chris@0
|
150 */
|
Chris@0
|
151 public function offsetUnset($name) {
|
Chris@0
|
152 unset($this->storage[$name]);
|
Chris@0
|
153 }
|
Chris@0
|
154
|
Chris@0
|
155 /**
|
Chris@0
|
156 * {@inheritdoc}
|
Chris@0
|
157 */
|
Chris@0
|
158 public function offsetExists($name) {
|
Chris@0
|
159 return isset($this->storage[$name]);
|
Chris@0
|
160 }
|
Chris@0
|
161
|
Chris@0
|
162 /**
|
Chris@0
|
163 * Adds classes or merges them on to array of existing CSS classes.
|
Chris@0
|
164 *
|
Chris@0
|
165 * @param string|array ...
|
Chris@0
|
166 * CSS classes to add to the class attribute array.
|
Chris@0
|
167 *
|
Chris@0
|
168 * @return $this
|
Chris@0
|
169 */
|
Chris@0
|
170 public function addClass() {
|
Chris@0
|
171 $args = func_get_args();
|
Chris@0
|
172 if ($args) {
|
Chris@0
|
173 $classes = [];
|
Chris@0
|
174 foreach ($args as $arg) {
|
Chris@0
|
175 // Merge the values passed in from the classes array.
|
Chris@0
|
176 // The argument is cast to an array to support comma separated single
|
Chris@0
|
177 // values or one or more array arguments.
|
Chris@0
|
178 $classes = array_merge($classes, (array) $arg);
|
Chris@0
|
179 }
|
Chris@0
|
180
|
Chris@0
|
181 // Merge if there are values, just add them otherwise.
|
Chris@0
|
182 if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
|
Chris@0
|
183 // Merge the values passed in from the class value array.
|
Chris@0
|
184 $classes = array_merge($this->storage['class']->value(), $classes);
|
Chris@0
|
185 $this->storage['class']->exchangeArray($classes);
|
Chris@0
|
186 }
|
Chris@0
|
187 else {
|
Chris@0
|
188 $this->offsetSet('class', $classes);
|
Chris@0
|
189 }
|
Chris@0
|
190 }
|
Chris@0
|
191
|
Chris@0
|
192 return $this;
|
Chris@0
|
193 }
|
Chris@0
|
194
|
Chris@0
|
195 /**
|
Chris@0
|
196 * Sets values for an attribute key.
|
Chris@0
|
197 *
|
Chris@0
|
198 * @param string $attribute
|
Chris@0
|
199 * Name of the attribute.
|
Chris@0
|
200 * @param string|array $value
|
Chris@0
|
201 * Value(s) to set for the given attribute key.
|
Chris@0
|
202 *
|
Chris@0
|
203 * @return $this
|
Chris@0
|
204 */
|
Chris@0
|
205 public function setAttribute($attribute, $value) {
|
Chris@0
|
206 $this->offsetSet($attribute, $value);
|
Chris@0
|
207
|
Chris@0
|
208 return $this;
|
Chris@0
|
209 }
|
Chris@0
|
210
|
Chris@0
|
211 /**
|
Chris@0
|
212 * Removes an attribute from an Attribute object.
|
Chris@0
|
213 *
|
Chris@0
|
214 * @param string|array ...
|
Chris@0
|
215 * Attributes to remove from the attribute array.
|
Chris@0
|
216 *
|
Chris@0
|
217 * @return $this
|
Chris@0
|
218 */
|
Chris@0
|
219 public function removeAttribute() {
|
Chris@0
|
220 $args = func_get_args();
|
Chris@0
|
221 foreach ($args as $arg) {
|
Chris@0
|
222 // Support arrays or multiple arguments.
|
Chris@0
|
223 if (is_array($arg)) {
|
Chris@0
|
224 foreach ($arg as $value) {
|
Chris@0
|
225 unset($this->storage[$value]);
|
Chris@0
|
226 }
|
Chris@0
|
227 }
|
Chris@0
|
228 else {
|
Chris@0
|
229 unset($this->storage[$arg]);
|
Chris@0
|
230 }
|
Chris@0
|
231 }
|
Chris@0
|
232
|
Chris@0
|
233 return $this;
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 /**
|
Chris@0
|
237 * Removes argument values from array of existing CSS classes.
|
Chris@0
|
238 *
|
Chris@0
|
239 * @param string|array ...
|
Chris@0
|
240 * CSS classes to remove from the class attribute array.
|
Chris@0
|
241 *
|
Chris@0
|
242 * @return $this
|
Chris@0
|
243 */
|
Chris@0
|
244 public function removeClass() {
|
Chris@0
|
245 // With no class attribute, there is no need to remove.
|
Chris@0
|
246 if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
|
Chris@0
|
247 $args = func_get_args();
|
Chris@0
|
248 $classes = [];
|
Chris@0
|
249 foreach ($args as $arg) {
|
Chris@0
|
250 // Merge the values passed in from the classes array.
|
Chris@0
|
251 // The argument is cast to an array to support comma separated single
|
Chris@0
|
252 // values or one or more array arguments.
|
Chris@0
|
253 $classes = array_merge($classes, (array) $arg);
|
Chris@0
|
254 }
|
Chris@0
|
255
|
Chris@0
|
256 // Remove the values passed in from the value array. Use array_values() to
|
Chris@0
|
257 // ensure that the array index remains sequential.
|
Chris@0
|
258 $classes = array_values(array_diff($this->storage['class']->value(), $classes));
|
Chris@0
|
259 $this->storage['class']->exchangeArray($classes);
|
Chris@0
|
260 }
|
Chris@0
|
261 return $this;
|
Chris@0
|
262 }
|
Chris@0
|
263
|
Chris@0
|
264 /**
|
Chris@18
|
265 * Gets the class attribute value if set.
|
Chris@18
|
266 *
|
Chris@18
|
267 * This method is implemented to take precedence over hasClass() for Twig 2.0.
|
Chris@18
|
268 *
|
Chris@18
|
269 * @return \Drupal\Core\Template\AttributeValueBase
|
Chris@18
|
270 * The class attribute value if set.
|
Chris@18
|
271 *
|
Chris@18
|
272 * @see twig_get_attribute()
|
Chris@18
|
273 */
|
Chris@18
|
274 public function getClass() {
|
Chris@18
|
275 return $this->offsetGet('class');
|
Chris@18
|
276 }
|
Chris@18
|
277
|
Chris@18
|
278 /**
|
Chris@0
|
279 * Checks if the class array has the given CSS class.
|
Chris@0
|
280 *
|
Chris@0
|
281 * @param string $class
|
Chris@0
|
282 * The CSS class to check for.
|
Chris@0
|
283 *
|
Chris@0
|
284 * @return bool
|
Chris@0
|
285 * Returns TRUE if the class exists, or FALSE otherwise.
|
Chris@0
|
286 */
|
Chris@0
|
287 public function hasClass($class) {
|
Chris@0
|
288 if (isset($this->storage['class']) && $this->storage['class'] instanceof AttributeArray) {
|
Chris@0
|
289 return in_array($class, $this->storage['class']->value());
|
Chris@0
|
290 }
|
Chris@0
|
291 else {
|
Chris@0
|
292 return FALSE;
|
Chris@0
|
293 }
|
Chris@0
|
294 }
|
Chris@0
|
295
|
Chris@0
|
296 /**
|
Chris@0
|
297 * Implements the magic __toString() method.
|
Chris@0
|
298 */
|
Chris@0
|
299 public function __toString() {
|
Chris@0
|
300 $return = '';
|
Chris@0
|
301 /** @var \Drupal\Core\Template\AttributeValueBase $value */
|
Chris@0
|
302 foreach ($this->storage as $name => $value) {
|
Chris@0
|
303 $rendered = $value->render();
|
Chris@0
|
304 if ($rendered) {
|
Chris@0
|
305 $return .= ' ' . $rendered;
|
Chris@0
|
306 }
|
Chris@0
|
307 }
|
Chris@0
|
308 return $return;
|
Chris@0
|
309 }
|
Chris@0
|
310
|
Chris@0
|
311 /**
|
Chris@0
|
312 * Returns all storage elements as an array.
|
Chris@0
|
313 *
|
Chris@0
|
314 * @return array
|
Chris@0
|
315 * An associative array of attributes.
|
Chris@0
|
316 */
|
Chris@0
|
317 public function toArray() {
|
Chris@0
|
318 $return = [];
|
Chris@0
|
319 foreach ($this->storage as $name => $value) {
|
Chris@0
|
320 $return[$name] = $value->value();
|
Chris@0
|
321 }
|
Chris@0
|
322
|
Chris@0
|
323 return $return;
|
Chris@0
|
324 }
|
Chris@0
|
325
|
Chris@0
|
326 /**
|
Chris@0
|
327 * Implements the magic __clone() method.
|
Chris@0
|
328 */
|
Chris@0
|
329 public function __clone() {
|
Chris@0
|
330 foreach ($this->storage as $name => $value) {
|
Chris@0
|
331 $this->storage[$name] = clone $value;
|
Chris@0
|
332 }
|
Chris@0
|
333 }
|
Chris@0
|
334
|
Chris@0
|
335 /**
|
Chris@0
|
336 * {@inheritdoc}
|
Chris@0
|
337 */
|
Chris@0
|
338 public function getIterator() {
|
Chris@0
|
339 return new \ArrayIterator($this->storage);
|
Chris@0
|
340 }
|
Chris@0
|
341
|
Chris@0
|
342 /**
|
Chris@0
|
343 * Returns the whole array.
|
Chris@0
|
344 */
|
Chris@0
|
345 public function storage() {
|
Chris@0
|
346 return $this->storage;
|
Chris@0
|
347 }
|
Chris@0
|
348
|
Chris@0
|
349 /**
|
Chris@0
|
350 * Returns a representation of the object for use in JSON serialization.
|
Chris@0
|
351 *
|
Chris@0
|
352 * @return string
|
Chris@0
|
353 * The safe string content.
|
Chris@0
|
354 */
|
Chris@0
|
355 public function jsonSerialize() {
|
Chris@0
|
356 return (string) $this;
|
Chris@0
|
357 }
|
Chris@0
|
358
|
Chris@0
|
359 }
|