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

ContextAwareEscapingSubscriber::escapeUrls()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 22
ccs 13
cts 13
cp 1
rs 8.6737
cc 5
eloc 11
nc 4
nop 2
crap 5
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