Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
lexer
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
8 / 8
16
100.00% covered (success)
100.00%
1 / 1
 tokenize
100.00% covered (success)
100.00%
66 / 66
100.00% covered (success)
100.00%
1 / 1
1
 strip_surrounding_quotes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fix_inline_variable_tokens
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 add_surrounding_quotes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fix_begin_tokens
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
8
 fix_if_tokens
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 fix_define_tokens
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 replace_twig_tag_masks
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\template\twig;
15
16class lexer extends \Twig\Lexer
17{
18    public function tokenize(\Twig\Source $source) : \Twig\TokenStream
19    {
20        $code = $source->getCode();
21        $filename = $source->getName();
22
23        // Our phpBB tags
24        // Commented out tokens are handled separately from the main replace
25        $phpbb_tags = array(
26            /*'BEGIN',
27            'BEGINELSE',
28            'END',
29            'IF',
30            'ELSE',
31            'ELSEIF',
32            'ENDIF',
33            'DEFINE',
34            'UNDEFINE',*/
35            'ENDDEFINE',
36            'INCLUDE',
37            'INCLUDEJS',
38            'INCLUDECSS',
39            'EVENT',
40        );
41
42        // Twig tag masks
43        $twig_tags = array(
44            'autoescape',
45            'endautoescape',
46            'if',
47            'elseif',
48            'else',
49            'endif',
50            'block',
51            'endblock',
52            'use',
53            'extends',
54            'embed',
55            'filter',
56            'endfilter',
57            'flush',
58            'for',
59            'endfor',
60            'macro',
61            'endmacro',
62            'import',
63            'from',
64            'sandbox',
65            'endsandbox',
66            'set',
67            'endset',
68            'spaceless',
69            'endspaceless',
70            'verbatim',
71            'endverbatim',
72            'apply',
73            'endapply',
74        );
75
76        // Fix tokens that may have inline variables (e.g. <!-- DEFINE $TEST = '{FOO}')
77        $code = $this->strip_surrounding_quotes(array(
78            'INCLUDE',
79            'INCLUDEJS',
80            'INCLUDECSS',
81        ), $code);
82        $code = $this->fix_inline_variable_tokens(array(
83            'DEFINE \$[a-zA-Z0-9_]+ =',
84            'INCLUDE',
85            'INCLUDEJS',
86            'INCLUDECSS',
87        ), $code);
88        $code = $this->add_surrounding_quotes(array(
89            'INCLUDE',
90            'INCLUDEJS',
91            'INCLUDECSS',
92        ), $code);
93
94        // Fix our BEGIN statements
95        $code = $this->fix_begin_tokens($code);
96
97        // Fix our IF tokens
98        $code = $this->fix_if_tokens($code);
99
100        // Fix our DEFINE tokens
101        $code = $this->fix_define_tokens($code);
102
103        // Replace all of our starting tokens, <!-- TOKEN --> with Twig style, {% TOKEN %}
104        // This also strips outer parenthesis, <!-- IF (blah) --> becomes <!-- IF blah -->
105        $code = preg_replace('#<!-- (' . implode('|', $phpbb_tags) . ')(?: (.*?) ?)?-->#', '{% $1 $2 %}', $code);
106
107        // Replace all of our twig masks with Twig code (e.g. <!-- BLOCK .+ --> with {% block $1 %})
108        $code = $this->replace_twig_tag_masks($code, $twig_tags);
109
110        // Replace all of our language variables, {L_VARNAME}, with Twig style, {{ lang('NAME') }}
111        // Appends any filters after lang()
112        $code = preg_replace('#{L_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2 }}', $code);
113
114        // Replace all of our escaped language variables, {LA_VARNAME}, with Twig style, {{ lang('NAME')|escape('js') }}
115        // Appends any filters after lang(), but before escape('js')
116        $code = preg_replace('#{LA_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2|escape(\'js\') }}', $code);
117
118        // Replace all of our variables, {VARNAME}, with Twig style, {{ VARNAME }}
119        // Appends any filters
120        $code = preg_replace('#{([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ $1$2 }}', $code);
121
122        // Tokenize \Twig\Source instance
123        return parent::tokenize(new \Twig\Source($code, $filename));
124    }
125
126    /**
127    * Strip surrounding quotes
128    *
129    * First step to fix tokens that may have inline variables
130    * E.g. <!-- INCLUDE '{TEST}.html' to <!-- INCLUDE {TEST}.html
131    *
132    * @param array $tokens array of tokens to search for (imploded to a regular expression)
133    * @param string $code
134    * @return string
135    */
136    protected function strip_surrounding_quotes($tokens, $code)
137    {
138        // Remove matching quotes at the beginning/end if a statement;
139        // E.g. 'asdf'"' -> asdf'"
140        // E.g. "asdf'"" -> asdf'"
141        // E.g. 'asdf'" -> 'asdf'"
142        return preg_replace('#<!-- (' . implode('|', $tokens) . ') (([\'"])?(.*?)\1) -->#', '<!-- $1 $2 -->', $code);
143    }
144
145    /**
146    * Fix tokens that may have inline variables
147    *
148    * Second step to fix tokens that may have inline variables
149    * E.g. <!-- INCLUDE '{TEST}.html' to <!-- INCLUDE ' ~ {TEST} ~ '.html
150    *
151    * @param array $tokens array of tokens to search for (imploded to a regular expression)
152    * @param string $code
153    * @return string
154    */
155    protected function fix_inline_variable_tokens($tokens, $code)
156    {
157        $callback = function($matches)
158        {
159            // Replace template variables with start/end to parse variables (' ~ TEST ~ '.html)
160            $matches[2] = preg_replace('#{([a-zA-Z0-9_\.$]+)}#', "'~ \$1 ~'", $matches[2]);
161
162            return "<!-- {$matches[1]} {$matches[2]} -->";
163        };
164
165        return preg_replace_callback('#<!-- (' . implode('|', $tokens) . ') (.+?) -->#', $callback, $code);
166    }
167
168    /**
169    * Add surrounding quotes
170    *
171    * Last step to fix tokens that may have inline variables
172    * E.g. <!-- INCLUDE '{TEST}.html' to <!-- INCLUDE '' ~ {TEST} ~ '.html'
173    *
174    * @param array $tokens array of tokens to search for (imploded to a regular expression)
175    * @param string $code
176    * @return string
177    */
178    protected function add_surrounding_quotes($tokens, $code)
179    {
180        return preg_replace('#<!-- (' . implode('|', $tokens) . ') (.+?) -->#', '<!-- $1 \'$2\' -->', $code);
181    }
182
183    /**
184    * Fix begin tokens (convert our BEGIN to Twig for)
185    *
186    * Not meant to be used outside of this context, public because the anonymous function calls this
187    *
188    * @param string $code
189    * @param array $parent_nodes (used in recursion)
190    * @return string
191    */
192    public function fix_begin_tokens($code, $parent_nodes = array())
193    {
194        // PHP 5.3 cannot use $this in an anonymous function, so use this as a work-around
195        $parent_class = $this;
196        $callback = function ($matches) use ($parent_class, $parent_nodes)
197        {
198            $hard_parents = explode('.', $matches[1]);
199            array_pop($hard_parents); // ends with .
200            if ($hard_parents)
201            {
202                $parent_nodes = array_merge($hard_parents, $parent_nodes);
203            }
204
205            $name = $matches[2];
206            $subset = trim(substr($matches[3], 1, -1)); // Remove parenthesis
207            $body = $matches[4];
208
209            // Replace <!-- BEGINELSE -->
210            $body = str_replace('<!-- BEGINELSE -->', '{% else %}', $body);
211
212            // Is the designer wanting to call another loop in a loop?
213            // <!-- BEGIN loop -->
214            // <!-- BEGIN !loop2 -->
215            // <!-- END !loop2 -->
216            // <!-- END loop -->
217            // 'loop2' is actually on the same nesting level as 'loop' you assign
218            // variables to it with template->assign_block_vars('loop2', array(...))
219            if (strpos($name, '!') === 0)
220            {
221                // Count the number if ! occurrences
222                $count = substr_count($name, '!');
223                for ($i = 0; $i < $count; $i++)
224                {
225                    array_pop($parent_nodes);
226                    $name = substr($name, 1);
227                }
228            }
229
230            // Remove all parent nodes, e.g. foo, bar from foo.bar.foobar.VAR
231            foreach ($parent_nodes as $node)
232            {
233                $body = preg_replace('#([^a-zA-Z0-9_])' . $node . '\.([a-zA-Z0-9_]+)\.#', '$1$2.', $body);
234            }
235
236            // Add current node to list of parent nodes for child nodes
237            $parent_nodes[] = $name;
238
239            // Recursive...fix any child nodes
240            $body = $parent_class->fix_begin_tokens($body, $parent_nodes);
241
242            // Need the parent variable name
243            array_pop($parent_nodes);
244            $parent = (!empty($parent_nodes)) ? end($parent_nodes) . '.' : '';
245
246            if ($subset !== '')
247            {
248                $subset = '|subset(' . $subset . ')';
249            }
250
251            $parent = ($parent) ?: 'loops.';
252            // Turn into a Twig for loop
253            return "{% for {$name} in {$parent}{$name}{$subset} %}{$body}{% endfor %}";
254        };
255
256        return preg_replace_callback('#<!-- BEGIN ((?:[a-zA-Z0-9_]+\.)*)([!a-zA-Z0-9_]+)(\([0-9,\-]+\))? -->(.+?)<!-- END \1\2 -->#s', $callback, $code);
257    }
258
259    /**
260    * Fix IF statements
261    *
262    * @param string $code
263    * @return string
264    */
265    protected function fix_if_tokens($code)
266    {
267        // Replace ELSE IF with ELSEIF
268        $code = preg_replace('#<!-- ELSE IF (.+?) -->#', '<!-- ELSEIF $1 -->', $code);
269
270        // Replace our "div by" with Twig's divisibleby (Twig does not like test names with spaces)
271        $code = preg_replace('# div by ([0-9]+)#', ' divisibleby($1)', $code);
272
273        $callback = function($matches)
274        {
275            $inner = $matches[2];
276            // Replace $TEST with definition.TEST
277            $inner = preg_replace('#(\s\(*!?)\$([a-zA-Z_0-9]+)#', '$1definition.$2', $inner);
278
279            // Replace .foo with loops.foo|length
280            $inner = preg_replace('#(\s\(*!?)\.([a-zA-Z_0-9]+)([^a-zA-Z_0-9\.])#', '$1loops.$2|length$3', $inner);
281
282            // Replace .foo.bar with foo.bar|length
283            $inner = preg_replace('#(\s\(*!?)\.([a-zA-Z_0-9\.]+)([^a-zA-Z_0-9\.])#', '$1$2|length$3', $inner);
284
285            return "<!-- {$matches[1]}IF{$inner}-->";
286        };
287
288        return preg_replace_callback('#<!-- (ELSE)?IF((.*?) (?:\(*!?[\$|\.]([^\s]+)(.*?))?)-->#', $callback, $code);
289    }
290
291    /**
292    * Fix DEFINE statements and {$VARNAME} variables
293    *
294    * @param string $code
295    * @return string
296    */
297    protected function fix_define_tokens($code)
298    {
299        /**
300        * Changing $VARNAME to definition.varname because set is only local
301        * context (e.g. DEFINE $TEST will only make $TEST available in current
302        * template and any child templates, but not any parent templates).
303        *
304        * DEFINE handles setting it properly to definition in its node, but the
305        * variables reading FROM it need to be altered to definition.VARNAME
306        *
307        * Setting up definition as a class in the array passed to Twig
308        * ($context) makes set definition.TEST available in the global context
309        */
310
311        // Replace <!-- DEFINE $NAME with {% DEFINE definition.NAME
312        $code = preg_replace('#<!-- DEFINE \$(.*?) -->#', '{% DEFINE $1 %}', $code);
313
314        // Changing UNDEFINE NAME to DEFINE NAME = null to save from creating an extra token parser/node
315        $code = preg_replace('#<!-- UNDEFINE \$(.*?)-->#', '{% DEFINE $1= null %}', $code);
316
317        // Replace all of our variables, {$VARNAME}, with Twig style, {{ definition.VARNAME }}
318        $code = preg_replace('#{\$([a-zA-Z0-9_\.]+)}#', '{{ definition.$1 }}', $code);
319
320        // Replace all of our variables, ~ $VARNAME ~, with Twig style, ~ definition.VARNAME ~
321        $code = preg_replace('#~ \$([a-zA-Z0-9_\.]+) ~#', '~ definition.$1 ~', $code);
322
323        return $code;
324    }
325
326    /**
327    * Replace Twig tag masks with Twig tag calls
328    *
329    * E.g. <!-- BLOCK foo --> with {% block foo %}
330    *
331    * @param string $code
332    * @param array $twig_tags All tags we want to create a mask for
333    * @return string
334    */
335    protected function replace_twig_tag_masks($code, $twig_tags)
336    {
337        $callback = function ($matches)
338        {
339            $matches[1] = strtolower($matches[1]);
340
341            return "{% {$matches[1]}{$matches[2]}%}";
342        };
343
344        foreach ($twig_tags as &$tag)
345        {
346            $tag = strtoupper($tag);
347        }
348
349        // twig_tags is an array of the twig tags, which are all lowercase, but we use all uppercase tags
350        $code = preg_replace_callback('#<!-- (' . implode('|', $twig_tags) . ')(.*?)-->#',$callback, $code);
351
352        return $code;
353    }
354}