Completed
Push — master ( 7c46c5...e7020a )
by Asmir
05:29
created

ContextAwareEscapingSubscriber   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 101
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 2

Test Coverage

Coverage 98%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 15
c 3
b 0
f 0
cbo 2
dl 0
loc 101
ccs 49
cts 50
cp 0.98
rs 10

7 Methods

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