1 <?php
  2 
  3 /**
  4  * Abstract baseclass for REX_VARS.
  5  *
  6  * @package redaxo\core
  7  */
  8 abstract class rex_var
  9 {
 10     const ENV_FRONTEND = 1;
 11     const ENV_BACKEND = 2;
 12     const ENV_INPUT = 4;
 13     const ENV_OUTPUT = 8;
 14 
 15     private static $vars = [];
 16     private static $env = null;
 17     private static $context = null;
 18     private static $contextData = null;
 19 
 20     private $args = [];
 21 
 22     private static $variableIndex = 0;
 23 
 24     /**
 25      * Parses all REX_VARs in the given content.
 26      *
 27      * @param string $content     Content
 28      * @param int    $env         Environment
 29      * @param string $context     Context
 30      * @param mixed  $contextData Context data
 31      *
 32      * @return string
 33      */
 34     public static function parse($content, $env = null, $context = null, $contextData = null)
 35     {
 36         $env = (int) $env;
 37 
 38         if (($env & self::ENV_INPUT) != self::ENV_INPUT) {
 39             $env = $env | self::ENV_OUTPUT;
 40         }
 41 
 42         self::$env = $env;
 43         self::$context = $context;
 44         self::$contextData = $contextData;
 45 
 46         self::$variableIndex = 0;
 47 
 48         $tokens = token_get_all($content);
 49         $countTokens = count($tokens);
 50         $content = '';
 51         for ($i = 0; $i < $countTokens; ++$i) {
 52             $token = $tokens[$i];
 53             if (is_string($token)) {
 54                 $content .= $token;
 55                 continue;
 56             }
 57 
 58             if (!in_array($token[0], [T_INLINE_HTML, T_CONSTANT_ENCAPSED_STRING, T_STRING, T_START_HEREDOC])) {
 59                 $content .= $token[1];
 60                 continue;
 61             }
 62 
 63             $add = $token[1];
 64             switch ($token[0]) {
 65                 case T_INLINE_HTML:
 66                     $format = '<?= %s@@@INLINE_HTML_REPLACEMENT_END@@@';
 67                     $add = self::replaceVars($add, $format);
 68                     $add = preg_replace_callback('/@@@INLINE_HTML_REPLACEMENT_END@@@(\r?\n?)/', function (array $match) {
 69                         return $match[1]
 70                             ? ', "'.addcslashes($match[1], "\r\n").'" ?>'.$match[1]
 71                             : ' ?>';
 72                     }, $add);
 73                     break;
 74 
 75                 case T_CONSTANT_ENCAPSED_STRING:
 76                     $format = $token[1][0] == '"' ? '" . %s . "' : "' . %s . '";
 77                     $add = self::replaceVars($add, $format, false, $token[1][0]);
 78 
 79                     $start = substr($add, 0, 5);
 80                     $end = substr($add, -5);
 81                     if ($start == '"" . ' || $start == "'' . ") {
 82                         $add = substr($add, 5);
 83                     }
 84                     if ($end == ' . ""' || $end == " . ''") {
 85                         $add = substr($add, 0, -5);
 86                     }
 87                     break;
 88 
 89                 case T_STRING:
 90                     while (isset($tokens[++$i])
 91                         && (is_string($tokens[$i]) && in_array($tokens[$i], ['=', '[', ']'])
 92                             || in_array($tokens[$i][0], [T_WHITESPACE, T_STRING, T_CONSTANT_ENCAPSED_STRING, T_LNUMBER, T_ISSET]))
 93                     ) {
 94                         $add .= is_string($tokens[$i]) ? $tokens[$i] : $tokens[$i][1];
 95                     }
 96                     --$i;
 97                     $add = self::replaceVars($add);
 98                     break;
 99 
100                 case T_START_HEREDOC:
101                     while (isset($tokens[++$i]) && (is_string($tokens[$i]) || $tokens[$i][0] != T_END_HEREDOC)) {
102                         $add .= is_string($tokens[$i]) ? $tokens[$i] : $tokens[$i][1];
103                     }
104                     --$i;
105                     if (preg_match("/'(.*)'/", $token[1], $match)) { // nowdoc
106                         $format = "\n" . $match[1] . "\n. %s . <<<'" . $match[1] . "'\n";
107                         $add = self::replaceVars($add, $format);
108                     } else { // heredoc
109                         $add = self::replaceVars($add, '{%s}', true);
110                     }
111                     break;
112             }
113 
114             $content .= $add;
115         }
116         return $content;
117     }
118 
119     /**
120      * Returns a rex_var object for the given var name.
121      *
122      * @param string $var
123      *
124      * @return self
125      */
126     private static function getVar($var)
127     {
128         if (!isset(self::$vars[$var])) {
129             $class = 'rex_var_' . strtolower(substr($var, 4));
130             if (!class_exists($class) || !is_subclass_of($class, self::class)) {
131                 return false;
132             }
133             self::$vars[$var] = $class;
134         }
135         $class = self::$vars[$var];
136         return new $class();
137     }
138 
139     /**
140      * Replaces the REX_VARs.
141      *
142      * @param string $content
143      * @param string $format
144      * @param bool   $useVariables
145      * @param string $stripslashes
146      *
147      * @return mixed|string
148      */
149     private static function replaceVars($content, $format = '%s', $useVariables = false, $stripslashes = null)
150     {
151         $matches = self::getMatches($content);
152 
153         if (empty($matches)) {
154             return $content;
155         }
156 
157         $iterator = new AppendIterator();
158         $iterator->append(new ArrayIterator($matches));
159         $variables = [];
160         $replacements = [];
161 
162         foreach ($iterator as $match) {
163             if (isset($replacements[$match[0]])) {
164                 continue;
165             }
166 
167             $var = self::getVar($match[1]);
168             $replaced = false;
169 
170             if ($var !== false) {
171                 $args = str_replace(['\[', '\]'], ['@@@OPEN_BRACKET@@@', '@@@CLOSE_BRACKET@@@'], $match[2]);
172                 if ($stripslashes) {
173                     $args = str_replace(['\\' . $stripslashes, '\\' . $stripslashes], $stripslashes, $args);
174                 }
175                 $var->setArgs($args);
176                 if (($output = $var->getGlobalArgsOutput()) !== false) {
177                     $output .= str_repeat("\n", max(0, substr_count($match[0], "\n") - substr_count($output, "\n") - substr_count($format, "\n")));
178                     if ($useVariables) {
179                         $replace = '$__rex_var_content_' . ++self::$variableIndex;
180                         $variables[] = '/* '. $match[0] .' */ ' . $replace . ' = ' . $output;
181                     } else {
182                         $replace = '/* '. $match[0] .' */ '. $output;
183                     }
184 
185                     $replacements[$match[0]] = sprintf($format, $replace);
186                     $replaced = true;
187                 }
188             }
189 
190             if (!$replaced && $matches = self::getMatches($match[2])) {
191                 $iterator->append(new ArrayIterator($matches));
192             }
193         }
194 
195         if ($replacements) {
196             $content = strtr($content, $replacements);
197         }
198 
199         if ($useVariables && !empty($variables)) {
200             $content = 'rex_var::nothing(' . implode(', ', $variables) . ') . ' . $content;
201         }
202 
203         return $content;
204     }
205 
206     /**
207      * Returns the REX_VAR matches.
208      *
209      * @param string $content
210      *
211      * @return array
212      */
213     private static function getMatches($content)
214     {
215         preg_match_all('/(REX_[A-Z_]+)\[((?:[^\[\]]|\\\\[\[\]]|(?R))*)(?<!\\\\)\]/s', $content, $matches, PREG_SET_ORDER);
216         return $matches;
217     }
218 
219     /**
220      * Sets the arguments.
221      *
222      * @param string $arg_string
223      */
224     private function setArgs($arg_string)
225     {
226         $this->args = rex_string::split($arg_string);
227     }
228 
229     /**
230      * Checks whether the given arguments exists.
231      *
232      * @param string $key
233      * @param bool   $defaultArg
234      *
235      * @return bool
236      */
237     protected function hasArg($key, $defaultArg = false)
238     {
239         return isset($this->args[$key]) || $defaultArg && isset($this->args[0]);
240     }
241 
242     /**
243      * Returns the argument.
244      *
245      * @param string      $key
246      * @param null|string $default
247      * @param bool        $defaultArg
248      *
249      * @return null|string
250      */
251     protected function getArg($key, $default = null, $defaultArg = false)
252     {
253         if (!$this->hasArg($key, $defaultArg)) {
254             return $default;
255         }
256         return isset($this->args[$key]) ? $this->args[$key] : $this->args[0];
257     }
258 
259     /**
260      * Returns the (recursive) parsed argument.
261      *
262      * @param string      $key
263      * @param null|string $default
264      * @param bool        $defaultArg
265      *
266      * @return int|null|string
267      */
268     protected function getParsedArg($key, $default = null, $defaultArg = false)
269     {
270         if (!$this->hasArg($key, $defaultArg)) {
271             return $default;
272         }
273         $arg = isset($this->args[$key]) ? $this->args[$key] : $this->args[0];
274         $begin = '<<<addslashes>>>';
275         $end = '<<</addslashes>>>';
276         $arg = $begin . self::replaceVars($arg, $end . "' . %s . '" . $begin) . $end;
277         $arg = preg_replace_callback("@$begin(.*)$end@Us", function ($match) {
278             return addcslashes($match[1], "\'");
279         }, $arg);
280         $arg = str_replace(['@@@OPEN_BRACKET@@@', '@@@CLOSE_BRACKET@@@'], ['[', ']'], $arg);
281         return is_numeric($arg) ? $arg : "'$arg'";
282     }
283 
284     /**
285      * Checks whether the given envirenment is active.
286      *
287      * @param int $env Environment
288      *
289      * @return bool
290      */
291     protected function environmentIs($env)
292     {
293         return (self::$env & $env) == $env;
294     }
295 
296     /**
297      * Returns the context.
298      *
299      * @return string
300      */
301     protected function getContext()
302     {
303         return self::$context;
304     }
305 
306     /**
307      * Returns the context data.
308      *
309      * @return mixed
310      */
311     protected function getContextData()
312     {
313         return self::$contextData;
314     }
315 
316     /**
317      * Returns the output.
318      *
319      * @return bool|string
320      */
321     abstract protected function getOutput();
322 
323     /**
324      * Quotes the string for php context.
325      *
326      * @param string $string
327      *
328      * @return string
329      */
330     protected static function quote($string)
331     {
332         $string = addcslashes($string, "\\'");
333         $string = preg_replace('/\v+/', '\' . "$0" . \'', $string);
334         $string = addcslashes($string, "\r\n");
335         return "'" . $string . "'";
336     }
337 
338     /**
339      * Returns the output in consideration of the global args.
340      *
341      * @return bool|string
342      */
343     private function getGlobalArgsOutput()
344     {
345         if (($content = $this->getOutput()) === false) {
346             return false;
347         }
348 
349         if ($this->hasArg('callback')) {
350             $args = ["'subject' => " . $content];
351             foreach ($this->args as $key => $value) {
352                 $args[] = "'$key' => " . $this->getParsedArg($key);
353             }
354             $args = '[' . implode(', ', $args) . ']';
355             return 'call_user_func(' . $this->getParsedArg('callback') . ', ' . $args . ')';
356         }
357 
358         $prefix = $this->hasArg('prefix') ? $this->getParsedArg('prefix') . ' . ' : '';
359         $suffix = $this->hasArg('suffix') ? ' . ' . $this->getParsedArg('suffix') : '';
360         $instead = $this->hasArg('instead');
361         $ifempty = $this->hasArg('ifempty');
362         if ($prefix || $suffix || $instead || $ifempty) {
363             if ($instead) {
364                 $if = $content;
365                 $then = $this->getParsedArg('instead');
366             } else {
367                 $if = '$__rex_var_content_' . ++self::$variableIndex . ' = ' . $content;
368                 $then = '$__rex_var_content_' . self::$variableIndex;
369             }
370             if ($ifempty) {
371                 return $prefix . '((' . $if . ') ? ' . $then . ' : ' . $this->getParsedArg('ifempty') . ')' . $suffix;
372             }
373             return '((' . $if . ') ? ' . $prefix . $then . $suffix . " : '')";
374         }
375         return $content;
376     }
377 
378     /**
379      * Converts a REX_VAR content to a PHP array.
380      *
381      * @param string $value
382      *
383      * @return array|null
384      */
385     public static function toArray($value)
386     {
387         $value = json_decode(htmlspecialchars_decode($value), true);
388         return is_array($value) ? $value : null;
389     }
390 
391     /**
392      * Returns empty string.
393      *
394      * @return string
395      */
396     public static function nothing()
397     {
398         return '';
399     }
400 }
401