1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\View\Helper;
11
12use stdClass;
13use Zend\View;
14use Zend\View\Exception;
15
16/**
17 * Zend\View\Helper\HeadMeta
18 *
19 * @see http://www.w3.org/TR/xhtml1/dtds.html
20 *
21 * Allows the following 'virtual' methods:
22 * @method HeadMeta appendName($keyValue, $content, $modifiers = array())
23 * @method HeadMeta offsetGetName($index, $keyValue, $content, $modifiers = array())
24 * @method HeadMeta prependName($keyValue, $content, $modifiers = array())
25 * @method HeadMeta setName($keyValue, $content, $modifiers = array())
26 * @method HeadMeta appendHttpEquiv($keyValue, $content, $modifiers = array())
27 * @method HeadMeta offsetGetHttpEquiv($index, $keyValue, $content, $modifiers = array())
28 * @method HeadMeta prependHttpEquiv($keyValue, $content, $modifiers = array())
29 * @method HeadMeta setHttpEquiv($keyValue, $content, $modifiers = array())
30 * @method HeadMeta appendProperty($keyValue, $content, $modifiers = array())
31 * @method HeadMeta offsetGetProperty($index, $keyValue, $content, $modifiers = array())
32 * @method HeadMeta prependProperty($keyValue, $content, $modifiers = array())
33 * @method HeadMeta setProperty($keyValue, $content, $modifiers = array())
34 */
35class HeadMeta extends Placeholder\Container\AbstractStandalone
36{
37    /**
38     * Allowed key types
39     *
40     * @var array
41     */
42    protected $typeKeys = array('name', 'http-equiv', 'charset', 'property', 'itemprop');
43
44    /**
45     * Required attributes for meta tag
46     *
47     * @var array
48     */
49    protected $requiredKeys = array('content');
50
51    /**
52     * Allowed modifier keys
53     *
54     * @var array
55     */
56    protected $modifierKeys = array('lang', 'scheme');
57
58    /**
59     * Registry key for placeholder
60     *
61     * @var string
62     */
63    protected $regKey = 'Zend_View_Helper_HeadMeta';
64
65    /**
66     * Constructor
67     *
68     * Set separator to PHP_EOL
69     *
70     */
71    public function __construct()
72    {
73        parent::__construct();
74
75        $this->setSeparator(PHP_EOL);
76    }
77
78    /**
79     * Retrieve object instance; optionally add meta tag
80     *
81     * @param  string $content
82     * @param  string $keyValue
83     * @param  string $keyType
84     * @param  array  $modifiers
85     * @param  string $placement
86     * @return HeadMeta
87     */
88    public function __invoke(
89        $content = null,
90        $keyValue = null,
91        $keyType = 'name',
92        $modifiers = array(),
93        $placement = Placeholder\Container\AbstractContainer::APPEND
94    ) {
95        if ((null !== $content) && (null !== $keyValue)) {
96            $item   = $this->createData($keyType, $keyValue, $content, $modifiers);
97            $action = strtolower($placement);
98            switch ($action) {
99                case 'append':
100                case 'prepend':
101                case 'set':
102                    $this->$action($item);
103                    break;
104                default:
105                    $this->append($item);
106                    break;
107            }
108        }
109
110        return $this;
111    }
112
113    /**
114     * Overload method access
115     *
116     * @param  string $method
117     * @param  array  $args
118     * @throws Exception\BadMethodCallException
119     * @return HeadMeta
120     */
121    public function __call($method, $args)
122    {
123        if (preg_match('/^(?P<action>set|(pre|ap)pend|offsetSet)(?P<type>Name|HttpEquiv|Property|Itemprop)$/', $method, $matches)) {
124            $action = $matches['action'];
125            $type   = $this->normalizeType($matches['type']);
126            $argc   = count($args);
127            $index  = null;
128
129            if ('offsetSet' == $action) {
130                if (0 < $argc) {
131                    $index = array_shift($args);
132                    --$argc;
133                }
134            }
135
136            if (2 > $argc) {
137                throw new Exception\BadMethodCallException(
138                    'Too few arguments provided; requires key value, and content'
139                );
140            }
141
142            if (3 > $argc) {
143                $args[] = array();
144            }
145
146            $item  = $this->createData($type, $args[0], $args[1], $args[2]);
147
148            if ('offsetSet' == $action) {
149                return $this->offsetSet($index, $item);
150            }
151
152            $this->$action($item);
153
154            return $this;
155        }
156
157        return parent::__call($method, $args);
158    }
159
160    /**
161     * Render placeholder as string
162     *
163     * @param  string|int $indent
164     * @return string
165     */
166    public function toString($indent = null)
167    {
168        $indent = (null !== $indent)
169            ? $this->getWhitespace($indent)
170            : $this->getIndent();
171
172        $items = array();
173        $this->getContainer()->ksort();
174
175        try {
176            foreach ($this as $item) {
177                $items[] = $this->itemToString($item);
178            }
179        } catch (Exception\InvalidArgumentException $e) {
180            trigger_error($e->getMessage(), E_USER_WARNING);
181            return '';
182        }
183
184        return $indent . implode($this->escape($this->getSeparator()) . $indent, $items);
185    }
186
187    /**
188     * Create data item for inserting into stack
189     *
190     * @param  string $type
191     * @param  string $typeValue
192     * @param  string $content
193     * @param  array  $modifiers
194     * @return stdClass
195     */
196    public function createData($type, $typeValue, $content, array $modifiers)
197    {
198        $data            = new stdClass;
199        $data->type      = $type;
200        $data->$type     = $typeValue;
201        $data->content   = $content;
202        $data->modifiers = $modifiers;
203
204        return $data;
205    }
206
207    /**
208     * Build meta HTML string
209     *
210     * @param  stdClass $item
211     * @throws Exception\InvalidArgumentException
212     * @return string
213     */
214    public function itemToString(stdClass $item)
215    {
216        if (!in_array($item->type, $this->typeKeys)) {
217            throw new Exception\InvalidArgumentException(sprintf(
218                'Invalid type "%s" provided for meta',
219                $item->type
220            ));
221        }
222        $type = $item->type;
223
224        $modifiersString = '';
225        foreach ($item->modifiers as $key => $value) {
226            if ($this->view->plugin('doctype')->isHtml5()
227                && $key == 'scheme'
228            ) {
229                throw new Exception\InvalidArgumentException(
230                    'Invalid modifier "scheme" provided; not supported by HTML5'
231                );
232            }
233            if (!in_array($key, $this->modifierKeys)) {
234                continue;
235            }
236            $modifiersString .= $key . '="' . $this->escape($value) . '" ';
237        }
238
239        $modifiersString = rtrim($modifiersString);
240
241        if ('' != $modifiersString) {
242            $modifiersString = ' ' . $modifiersString;
243        }
244
245        if (method_exists($this->view, 'plugin')) {
246            if ($this->view->plugin('doctype')->isHtml5()
247                && $type == 'charset'
248            ) {
249                $tpl = ($this->view->plugin('doctype')->isXhtml())
250                    ? '<meta %s="%s"/>'
251                    : '<meta %s="%s">';
252            } elseif ($this->view->plugin('doctype')->isXhtml()) {
253                $tpl = '<meta %s="%s" content="%s"%s />';
254            } else {
255                $tpl = '<meta %s="%s" content="%s"%s>';
256            }
257        } else {
258            $tpl = '<meta %s="%s" content="%s"%s />';
259        }
260
261        $meta = sprintf(
262            $tpl,
263            $type,
264            $this->escape($item->$type),
265            $this->escape($item->content),
266            $modifiersString
267        );
268
269        if (isset($item->modifiers['conditional'])
270            && !empty($item->modifiers['conditional'])
271            && is_string($item->modifiers['conditional'])
272        ) {
273            // inner wrap with comment end and start if !IE
274            if (str_replace(' ', '', $item->modifiers['conditional']) === '!IE') {
275                $meta = '<!-->' . $meta . '<!--';
276            }
277            $meta = '<!--[if ' . $this->escape($item->modifiers['conditional']) . ']>' . $meta . '<![endif]-->';
278        }
279
280        return $meta;
281    }
282
283    /**
284     * Normalize type attribute of meta
285     *
286     * @param  string $type type in CamelCase
287     * @throws Exception\DomainException
288     * @return string
289     */
290    protected function normalizeType($type)
291    {
292        switch ($type) {
293            case 'Name':
294                return 'name';
295            case 'HttpEquiv':
296                return 'http-equiv';
297            case 'Property':
298                return 'property';
299            case 'Itemprop':
300                return 'itemprop';
301            default:
302                throw new Exception\DomainException(sprintf(
303                    'Invalid type "%s" passed to normalizeType',
304                    $type
305                ));
306        }
307    }
308
309    /**
310     * Determine if item is valid
311     *
312     * @param  mixed $item
313     * @return bool
314     */
315    protected function isValid($item)
316    {
317        if ((!$item instanceof stdClass)
318            || !isset($item->type)
319            || !isset($item->modifiers)
320        ) {
321            return false;
322        }
323
324        if (!isset($item->content)
325            && (! $this->view->plugin('doctype')->isHtml5()
326            || (! $this->view->plugin('doctype')->isHtml5() && $item->type !== 'charset'))
327        ) {
328            return false;
329        }
330
331        // <meta itemprop= ... /> is only supported with doctype html
332        if (! $this->view->plugin('doctype')->isHtml5()
333            && $item->type === 'itemprop'
334        ) {
335            return false;
336        }
337
338        // <meta property= ... /> is only supported with doctype RDFa
339        if (!$this->view->plugin('doctype')->isRdfa()
340            && $item->type === 'property'
341        ) {
342            return false;
343        }
344
345        return true;
346    }
347
348    /**
349     * Append
350     *
351     * @param  string $value
352     * @return void
353     * @throws Exception\InvalidArgumentException
354     */
355    public function append($value)
356    {
357        if (!$this->isValid($value)) {
358            throw new Exception\InvalidArgumentException(
359                'Invalid value passed to append; please use appendMeta()'
360            );
361        }
362
363        return $this->getContainer()->append($value);
364    }
365
366    /**
367     * OffsetSet
368     *
369     * @param  string|int $index
370     * @param  string     $value
371     * @throws Exception\InvalidArgumentException
372     * @return void
373     */
374    public function offsetSet($index, $value)
375    {
376        if (!$this->isValid($value)) {
377            throw  new Exception\InvalidArgumentException(
378                'Invalid value passed to offsetSet; please use offsetSetName() or offsetSetHttpEquiv()'
379            );
380        }
381
382        return $this->getContainer()->offsetSet($index, $value);
383    }
384
385    /**
386     * OffsetUnset
387     *
388     * @param  string|int $index
389     * @throws Exception\InvalidArgumentException
390     * @return void
391     */
392    public function offsetUnset($index)
393    {
394        if (!in_array($index, $this->getContainer()->getKeys())) {
395            throw new Exception\InvalidArgumentException('Invalid index passed to offsetUnset()');
396        }
397
398        return $this->getContainer()->offsetUnset($index);
399    }
400
401    /**
402     * Prepend
403     *
404     * @param  string $value
405     * @throws Exception\InvalidArgumentException
406     * @return void
407     */
408    public function prepend($value)
409    {
410        if (!$this->isValid($value)) {
411            throw new Exception\InvalidArgumentException(
412                'Invalid value passed to prepend; please use prependMeta()'
413            );
414        }
415
416        return $this->getContainer()->prepend($value);
417    }
418
419    /**
420     * Set
421     *
422     * @param  string $value
423     * @throws Exception\InvalidArgumentException
424     * @return void
425     */
426    public function set($value)
427    {
428        if (!$this->isValid($value)) {
429            throw new Exception\InvalidArgumentException('Invalid value passed to set; please use setMeta()');
430        }
431
432        $container = $this->getContainer();
433        foreach ($container->getArrayCopy() as $index => $item) {
434            if ($item->type == $value->type && $item->{$item->type} == $value->{$value->type}) {
435                $this->offsetUnset($index);
436            }
437        }
438
439        return $this->append($value);
440    }
441
442    /**
443     * Create an HTML5-style meta charset tag. Something like <meta charset="utf-8">
444     *
445     * Not valid in a non-HTML5 doctype
446     *
447     * @param  string $charset
448     * @return HeadMeta Provides a fluent interface
449     */
450    public function setCharset($charset)
451    {
452        $item = new stdClass;
453        $item->type = 'charset';
454        $item->charset = $charset;
455        $item->content = null;
456        $item->modifiers = array();
457        $this->set($item);
458
459        return $this;
460    }
461}
462