ContextAwareEscapingSubscriber   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 100
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 2

Test Coverage

Coverage 98.04%

Importance

Changes 0
Metric Value
wmc 15
cbo 2
dl 0
loc 100
c 0
b 0
f 0
ccs 50
cts 51
cp 0.9804
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A xpathQuery() 0 8 3
A addEscaping() 0 11 1
A escapeUrls() 0 22 5
A escapeStyle() 0 12 2
A escapeScript() 0 11 2
A __construct() 0 12 1
A getSubscribedEvents() 0 6 1
1
<?php
2
namespace Goetas\Twital\EventSubscriber;
3
4
use Goetas\Twital\EventDispatcher\CompilerEvents;
5
use Goetas\Twital\EventDispatcher\TemplateEvent;
6
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7
8
/**
9
 *
10
 * @author Asmir Mustafic <[email protected]>
11
 *
12
 */
13
class ContextAwareEscapingSubscriber implements EventSubscriberInterface
14
{
15
    const REGEX_STRING = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
16
17
    protected $options = array();
18
    protected $placeholder = array();
19
20 487
    public function __construct(array $placeholder = array('[_TWITAL_[', ']_TWITAL_]'), array $options = array())
21
    {
22 487
        $this->placeholder = array(
23 487
            '[_TWITAL_[',
24
            ']_TWITAL_]'
25 487
        );
26
27 487
        $this->options = array_merge(array(
28 487
            'tag_block' => array('{%', '%}'),
29 487
            'tag_variable' => array('{{', '}}'),
30 487
        ), $options);
31 487
    }
32
33 487
    public static function getSubscribedEvents()
34
    {
35
        return array(
36 487
            CompilerEvents::PRE_DUMP => 'addEscaping'
37 487
        );
38
    }
39
40 484
    public function addEscaping(TemplateEvent $event)
41
    {
42 484
        $doc = $event->getTemplate()->getDocument();
43
44 484
        $xp = new \DOMXPath($doc);
45 484
        $xp->registerNamespace("xh", "http://www.w3.org/1999/xhtml");
46
47 484
        $this->escapeScript($doc, $xp);
48 484
        $this->escapeStyle($doc, $xp);
49 484
        $this->escapeUrls($doc, $xp);
50 484
    }
51
52
    /**
53
     *
54
     * Used only to achieve HHVM compatibility. Sett https://github.com/facebook/hhvm/issues/2810
55
     */
56 484
    private function xpathQuery(\DOMXPath $xp, $expression, \DOMNode $contextnode = null, $registerNodeNS = true)
57
    {
58 484
        if (defined('HHVM_VERSION') && HHVM_VERSION_ID < 30500) {
59
            return $xp->query($expression, $contextnode);
60
        } else {
61 484
            return $xp->query($expression, $contextnode, $registerNodeNS);
62
        }
63
    }
64
65 484
    private function escapeUrls(\DOMDocument $doc, \DOMXPath $xp)
66
    {
67 484
        $regex = '{' . preg_quote($this->options['tag_variable'][0]) . '((' . self::REGEX_STRING . '|[^"\']*)+)' . preg_quote($this->options['tag_variable'][1]) . '}siuU';
68
69
        // special attr escaping
70 484
        $res = $this->xpathQuery($xp, "(//xh:*/@href|//xh:*/@src)[contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
71 484
        foreach ($res as $node) {
72
73
            // if the twig variable is at the beginning of attribute, we should skip it
74 24
            if (preg_match('{^' . preg_quote($this->options['tag_variable'][0]) . '((' . self::REGEX_STRING . '|[^"\']*)+)' . preg_quote($this->options['tag_variable'][1]) . '}siuU', str_replace($this->placeholder, '', $node->value))) {
75 12
                continue;
76
            }
77
78 12
            if (substr($node->value, 0, 11) == "javascript:" && $node->name == "href") {
79 4
                $newValue = preg_replace($regex, "{$this->options['tag_variable'][0]} (\\1)  | escape('js') {$this->options['tag_variable'][1]}", $node->value);
80 4
            } else {
81 8
                $newValue = preg_replace($regex, "{$this->options['tag_variable'][0]} (\\1)  | escape('url') {$this->options['tag_variable'][1]}", $node->value);
82
            }
83
84 12
            $node->value = htmlspecialchars($newValue, ENT_COMPAT, 'UTF-8');
85 484
        }
86 484
    }
87
88 484
    private function escapeStyle(\DOMDocument $doc, \DOMXPath $xp)
89
    {
90
        /**
91
         * @var \DOMNode[] $res
92
         */
93 484
        $res = $this->xpathQuery($xp, "//xh:style[not(@type) or @type = 'text/css'][contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
94
95 484
        foreach ($res as $node) {
96 12
            $node->insertBefore($doc->createTextNode("{$this->options['tag_block'][0]} autoescape 'css' {$this->options['tag_block'][1]}"), $node->firstChild);
97 12
            $node->appendChild($doc->createTextNode("{$this->options['tag_block'][0]} endautoescape {$this->options['tag_block'][1]}"));
98 484
        }
99 484
    }
100
101 484
    private function escapeScript(\DOMDocument $doc, \DOMXPath $xp)
102
    {
103
        /**
104
         * @var \DOMNode[] $res
105
         */
106 484
        $res = $this->xpathQuery($xp, "//xh:script[not(@type) or @type = 'text/javascript'][contains(., '{$this->options['tag_variable'][0]}') and contains(., '{$this->options['tag_variable'][1]}')]", $doc, false);
107 484
        foreach ($res as $node) {
108 12
            $node->insertBefore($doc->createTextNode("{$this->options['tag_block'][0]} autoescape 'js' {$this->options['tag_block'][1]}"), $node->firstChild);
109 12
            $node->appendChild($doc->createTextNode("{$this->options['tag_block'][0]} endautoescape {$this->options['tag_block'][1]}"));
110 484
        }
111 484
    }
112
}
113