true, 'IF_NON_PASSED' => true, 'IF_PASSED_AND_TRUE' => true, 'IF_NON_PASSED_OR_FALSE' => true, 'PARAM_INFO' => true, 'IF_NOT_IN_ARRAY' => true, 'IF_IN_ARRAY' => true, 'IMPLODE' => true, 'COUNT' => true, 'IF_ARRAY_EMPTY' => true, 'IF_ARRAY_NON_EMPTY' => true, 'OF' => true, 'INCLUDE' => true, 'LOOP' => true, 'SET_NOPREEVAL' => true, 'PARAMS_JSON' => true]; } /** * Helper function or use getting line numbers. * * @param array $bits Compiler tokens * @param integer $i How far we are through the token list * @return integer The sum length of tokens passed * * @ignore */ function _length_so_far(array $bits, int $i) : int { $len = 0; foreach ($bits as $_i => $x) { if ($_i === $i) { break; } $len += strlen($x); } return $len; } /** * Take some Tempcode and pre-process it for Tempcode portions encapsulated within comments (or similar). * This is done so syntax-highlighters don't break, and WYSIWYG-editors don't corrupt the Tempcode. * * @param string $data Input Tempcode * @return string Output Tempcode */ function substitute_comment_encapsulated_tempcode(string $data) : string { // HTML comment $data = cms_preg_replace_safe('#).)*)\}\s*-->#', '{${1}}', $data); // HTML attribute $data = preg_replace('#\sx-tempcode(-\w+)?="\{([^"]*)\}"#', '{${2}}', $data); // CSS/JS comment $data = cms_preg_replace_safe('#/\*\s*\{([^a-z0-9])(((?!\*/).)*)\}\s*\*/#', '{${1}${2}}', $data); return $data; } /** * Compile a template into a list of appendable outputs, for the closure-style Tempcode implementation. * * @param string $data The template file contents * @param ID_TEXT $template_name The name of the template (blank: not from a file) * @param ID_TEXT $theme The name of the theme * @param ID_TEXT $lang The language it is for * @param boolean $tolerate_errors Whether to tolerate errors * @param ?array $parameters Parameters to hard-code in during compilation (null: no hard-coding) * @param ?array $parameters_used Parameters used in final Tempcode will be written into here (null: don't) * @param ?string $suffix File type suffix of template file (e.g. .tpl) (null: not from a file) * @set .tpl .js .xml .txt .css * @param ?string $directory Subdirectory type to look in (null: not from a file) * @return array A pair: array Compiled result structure, array preprocessable bits (special stuff needing attention that is referenced within the template) */ function compile_template(string $data, string $template_name, string $theme, string $lang, bool $tolerate_errors = false, ?array &$parameters = null, ?array &$parameters_used = null, ?string $suffix = null, ?string $directory = null) : array { raise_php_memory_limit(); if (strpos($data, '/*{$,parser hint: pure}*/') !== false) { return [['"' . php_addslashes(preg_replace('#\{\$,.*\}#U', '', str_replace('/*{$,parser hint: pure}*/', '/*no minify*/', $data))) . '"'], []]; } $override_hooks = find_all_hook_obs('systems', 'contentious_overrides', 'Hook_contentious_overrides_'); foreach ($override_hooks as $hook_ob) { if (method_exists($hook_ob, 'compile_template')) { $hook_ob->compile_template(/*passed by reference*/$data, $template_name, $theme, $lang, $suffix, $directory); } } if ($parameters !== null) { $parameter = null; foreach ($parameters as $key => $parameter) { if (is_bool($parameter)) { $parameters[$key] = $parameter ? '1' : '0'; } elseif ($parameter === null) { unset($parameters[$key]); } elseif (isset($parameter->is_all_static)) { $parameters[$key] = $parameters[$key]->evaluate(); } } } $data = substitute_comment_encapsulated_tempcode($data); $data = preg_replace('#<\?php(.*)\?' . '>#sU', '{+START,PHP}${1}{+END}', $data); global $STUCK_ABORT_SIGNAL; $sas_bak = $STUCK_ABORT_SIGNAL; require_code('lang'); require_code('urls'); $cl = fallback_lang(); $bits = array_values(preg_split('#(?]*))|((?]:?#', '', $_first_param)); // :? is so that the ":" in language string codenames does not get considered an escape $escaped = []; $no_preprocess = false; foreach ($_escaped as $e) { switch ($e) { case '`': $escaped[] = NULL_ESCAPED; break; case '%': $escaped[] = NAUGHTY_ESCAPED; break; case '*': $escaped[] = ENTITY_ESCAPED; break; case '=': $escaped[] = FORCIBLY_ENTITY_ESCAPED; break; case ';': $escaped[] = SQ_ESCAPED; break; case '#': $escaped[] = DQ_ESCAPED; break; case '~': // New lines disappear $escaped[] = NL_ESCAPED; break; case '^': $escaped[] = NL2_ESCAPED; // New lines go to \n break; case '|': $escaped[] = ID_ESCAPED; break; case '\'': $escaped[] = CSS_ESCAPED; break; case '!': // If & is not wanted as WYSIWYG editor may break it case '&': $escaped[] = UL_ESCAPED; break; case '.': $escaped[] = UL2_ESCAPED; break; case '/': $escaped[] = JSHTML_ESCAPED; break; case '@': $escaped[] = CC_ESCAPED; break; case '+': $escaped[] = PURE_STRING; // A performance marker break; case '>': $escaped[] = NO_OUTPUT; // Process but do not output break; // This is used as a hint to not preprocess case '-': $no_preprocess = true; // NB: we're out of ASCII symbols now. We want to avoid []()<>" brackets, whitespace characters, and control codes, and others are used for Tempcode grammar or are valid identifier characters. // Actually +/$/! can be used at the end (+ and ! is already taken, and $ messes with Tempcode compilation) break; } } $_opener_params = ''; foreach ($opener_params as $oi => &$oparam) { if (empty($oparam)) { $oparam = ['""']; if (!isset($opener_params[$oi + 1])) { unset($opener_params[$oi]); break; } } if ($_opener_params !== '') { $_opener_params .= ','; } $_opener_params .= implode('.', $oparam); } $first_param = preg_replace('#[`%*=;\#\-~\^|\'!&./@+>]+(")?$#', '$1', $_first_param); switch ($past_level_mode) { case PARSE_SYMBOL: if (!$no_preprocess) { switch ($first_param) { // These need preprocessing case '""': array_splice($preprocessable_bits, $num_preprocessable_bits); // Remove anything preprocessable marked inside the comment break; case '"REQUIRE_CSS"': case '"REQUIRE_JAVASCRIPT"': case '"JS_TEMPCODE"': case '"CSS_TEMPCODE"': case '"SET"': case '"BLOCK"': case '"LOAD_PAGE"': case '"LOAD_PANEL"': case '"CATALOGUE_ENTRY_FOR"': case '"METADATA"': case '"METADATA_IMAGE_EXTRACT"': case '"CANONICAL_URL"': case '"INSERT_SPAMMER_BLACKHOLE"': foreach ($stack as $level_test) { // Make sure if it's a LOOP then we evaluate the parameters early, as these have extra bindings we don't know about if (($level_test[4] === PARSE_DIRECTIVE) && (isset($level_test[6][1], $level_test[6][1][0])) && ($level_test[6][1][0] === '"LOOP"')) { // For a loop, we need to do full evaluation of symbol parameters as it may be bound to a loop variable $eval_openers = tc_eval_opener_params($_opener_params); if (is_array($eval_openers)) { $pp_bit = [[], TC_SYMBOL, str_replace('"', '', $first_param), $eval_openers]; $preprocessable_bits[] = $pp_bit; } break 2; } } $symbol_params = []; foreach ($opener_params as $param) { $myfunc = 'tcpfunc_' . fast_uniqid(); $funcdef = build_closure_function($myfunc, $param); $symbol_params[] = new Tempcode([[$myfunc => $funcdef], [[[$myfunc, [/* Is currently unbound */], TC_KNOWN, '', '']]]]); // Parameters will be bound in later. } $pp_bit = [[], TC_SYMBOL, str_replace('"', '', $first_param), $symbol_params]; $preprocessable_bits[] = $pp_bit; break; } } // Special case: Needed to ensure correct binding if ((($first_param === '"IMG"') || ($first_param === '"IMG_INLINE"')) && (strpos($_opener_params, ',') === false)) { // Needed to ensure correct binding $_opener_params .= ',"0","' . php_addslashes($theme) . '"'; } // Optimise simple PHP-compatible operators foreach (['EQ' => '==', 'NEQ' => '!='] as $symbol_op => $php_op) { if (($first_param === '"' . $symbol_op . '"') && (count($opener_params) === 2)) { $new_line = '(((' . implode('.', $opener_params[0]) . ')' . $php_op . '(' . implode('.', $opener_params[1]) . '))?"1":"0")'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break 2; } } foreach (['AND' => '&&', 'OR' => '||'] as $symbol_op => $php_op) { if (($first_param === '"' . $symbol_op . '"') && (count($opener_params) === 2)) { $new_line = '(((' . implode('.', $opener_params[0]) . ')=="1")' . $php_op . '(' . implode('.', $opener_params[1]) . '=="1")?"1":"0")'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break 2; } } if (($first_param === '"?"') && (count($opener_params) === 3) && (count($escaped) === 0)) { $new_line = '(((' . implode('.', $opener_params[0]) . ')=="1")?(' . implode('.', $opener_params[1]) . '):(' . implode('.', $opener_params[2]) . '))'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break 2; } // Okay, a fully dynamic symbol $name = preg_replace('#(^")|("$)#', '', $first_param); if ($name === '?') { $name = 'TERNARY'; } $new_line = 'ecv($cl,[' . implode(',', array_map('strval', $escaped)) . '],' . strval(TC_SYMBOL) . ',' . $first_param . ',[' . $_opener_params . '])'; if ((may_optimise_out_symbol(trim($first_param, '"'))) && (tc_is_all_static($_opener_params))) { // Can optimise out? $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $new_line . ';', $tpl_funcs, [], $cl); $new_line = '"' . php_addslashes($eval) . '"'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); } else { tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } break; case PARSE_LANGUAGE_REFERENCE: $new_line = 'ecv($cl,[' . implode(',', array_map('strval', $escaped)) . '],' . strval(TC_LANGUAGE_REFERENCE) . ',' . $first_param . ',[' . $_opener_params . '])'; if (tc_is_all_static($_opener_params)) { // Optimise out for simple case? $tpl_funcs = []; $looked_up = tempcode_compiler_eval('return ' . $new_line . ';', $tpl_funcs, [], $cl); if (!cms_empty_safe($looked_up)) { $new_line = '"' . php_addslashes($looked_up) . '"'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); } } else { tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } break; case PARSE_PARAMETER: $parameter = trim($first_param, '"\''); if ($parameters_used !== null) { $parameters_used[$parameter] = true; } // Optimise out as parameter is known if ((isset($parameters[$parameter])) && (is_string($parameters[$parameter]))) { $new_line = '"' . php_addslashes(apply_tempcode_escaping_inline($escaped, $parameters[$parameter])) . '"'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); break; } $parameter = preg_replace('#[^\w]#', '', $parameter); // security to stop PHP injection if (is_numeric($parameter)) { $new_line = '"{' . php_addslashes($parameter) . '}"'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); } elseif ($escaped === [PURE_STRING]) { $new_line = '$bound_' . php_addslashes($parameter); tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } else { $temp = 'otp(isset($bound_' . php_addslashes($parameter) . ')?$bound_' . php_addslashes($parameter) . ':null'; if ((!function_exists('get_value')) || (get_value('shortened_tempcode') !== '1')) { $temp .= ',"' . php_addslashes($template_name . ':' . $parameter) . '"'; } $temp .= ')'; if (empty($escaped)) { tc_add_to_current_level_data($current_level_data, $just_done_string, $temp); } else { $s_escaped = ''; foreach ($escaped as $esc) { if ($s_escaped !== '') { $s_escaped .= ','; } $s_escaped .= strval($esc); } if (($s_escaped === strval(ENTITY_ESCAPED)) && (!$GLOBALS['XSS_DETECT'])) { $new_line = '(empty($bound_' . $parameter . '->pure_lang)?@htmlspecialchars(' . $temp . ',ENT_QUOTES | ENT_SUBSTITUTE,get_charset()):' . $temp . ')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } else { if ($s_escaped === strval(ENTITY_ESCAPED)) { $new_line = '(empty($bound_' . $parameter . '->pure_lang)?apply_tempcode_escaping_inline([' . $s_escaped . '],' . $temp . '):' . $temp . ')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } else { $new_line = 'apply_tempcode_escaping_inline([' . $s_escaped . '],' . $temp . ')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } } } } break; } // Handle directive nesting if ($past_level_mode === PARSE_DIRECTIVE) { $tpl_funcs = []; $eval = trim(tempcode_compiler_eval('return ' . $first_param . ';', $tpl_funcs, [], $cl)); if (!is_string($eval)) { $eval = ''; } if ($eval === 'START') { // START // Open a new directive level $stack[] = [$current_level_mode, $current_level_data, $just_done_string, $current_level_params, $past_level_mode, $past_level_data, $past_level_params, count($preprocessable_bits), $first_param, $escaped, $no_preprocess]; $current_level_data = []; $just_done_string = false; $current_level_params = []; $current_level_mode = PARSE_DIRECTIVE_INNER; if ($opener_params === [['"NO_PREPROCESSING"']]) { array_push($preprocessable_bits_stack, $preprocessable_bits); // So anything inside will end up being thrown away when we pop back to what we had before in $preprocessable_bits } } elseif ($first_param === '"END"') { // END // Test that the top stack does represent a started directive, and close directive level $past_level_data = $current_level_data; if (empty($past_level_data)) { $past_level_data = ['""']; } $past_level_params = $current_level_params; $past_level_mode = $current_level_mode; if (empty($stack)) { if ($tolerate_errors) { continue 2; } warn_exit(do_lang_tempcode('TEMPCODE_TOO_MANY_CLOSES', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))), false, true); } list($current_level_mode, $current_level_data, $just_done_string, $current_level_params, $directive_level_mode, $directive_level_data, $directive_level_params, $num_preprocessable_bits, $opening_tag, $escaped, $no_preprocess) = array_pop($stack); if (!is_array($directive_level_params)) { if ($tolerate_errors) { continue 2; } warn_exit(do_lang_tempcode('UNCLOSED_DIRECTIVE_OR_BRACE', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))), false, true); } $directive_opener_params = array_merge($directive_level_params, [$directive_level_data]); if (($directive_level_mode !== PARSE_DIRECTIVE) || ($opening_tag !== '"START"')) { if ($tolerate_errors) { continue 2; } warn_exit(do_lang_tempcode('TEMPCODE_TOO_MANY_CLOSES', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))), false, true); } if (count($directive_opener_params) === 1) { if ($tolerate_errors) { continue 2; } warn_exit(do_lang_tempcode('NO_DIRECTIVE_TYPE', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))), false, true); } // Work out parameters $directive_params = ''; $first_directive_param = '""'; if (empty($directive_opener_params[1])) { $directive_opener_params[1] = ['""']; } $count_directive_opener_params = count($directive_opener_params); for ($j = 2; $j < $count_directive_opener_params; $j++) { if (empty($directive_opener_params[$j])) { $directive_opener_params[$j] = ['""']; } if ($directive_params !== '') { $directive_params .= ','; } $directive_params .= implode('.', $directive_opener_params[$j]); if ($j === 2) { $first_directive_param = implode('.', $directive_opener_params[$j]); } } $directive_internal = implode('.', $past_level_data); $directive_params_with_internal = $directive_params; if ($directive_params_with_internal !== '') { $directive_params_with_internal .= ','; } $directive_params_with_internal .= $directive_internal; $directive_params_with_internal_with_faux = $directive_params; if ($directive_params_with_internal_with_faux !== '') { $directive_params_with_internal_with_faux .= ','; } $directive_params_with_internal_with_faux .= '"xxx"'; // Work out name $tpl_funcs = []; $eval = trim(tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[1]) . ';', $tpl_funcs, [], $cl)); if (!is_string($eval)) { $eval = ''; } $directive_name = $eval; // Special case: Some nested pre-processables $added_preprocessable_bits = false; switch ($directive_name) { case 'INCLUDE': case 'FRACTIONAL_EDITABLE': $eval = tc_eval_opener_params($directive_params); if (is_array($eval)) { $pp_bit = [[], TC_DIRECTIVE, str_replace('"', '', $directive_name), $eval]; $preprocessable_bits[] = $pp_bit; $added_preprocessable_bits = true; } break; } // Special case: Needs to be dynamic as NO_PREPROCESSING also implies avoid internal caching if ($directive_name === 'SET_NOPREEVAL') { $myfunc = 'do_runtime_' . uniqid('', true)/*fast_uniqid()*/; $_past_level_data = $directive_internal; if (strpos($_past_level_data, 'isset($bound') !== false) { // Horrible but efficient code needed to allow IF_PASSED/IF_NON_PASSED to keep working when templates are put adjacent to each other, where some have it, and don't. This is needed as eval does not set a scope block. $reset_code = "eval(\\\$FULL_RESET_VAR_CODE); "; } elseif (strpos($_past_level_data, '$bound') !== false) { $reset_code = "eval(\\\$RESET_VAR_CODE); "; } else { $reset_code = ''; } $funcdef = /*if (!isset(\$tpl_funcs['$myfunc']))\n\t*/"\$tpl_funcs['$myfunc']=\"{$reset_code}echo " . php_addslashes($_past_level_data) . ";\";\n"; $past_level_data = ['new Tempcode([[\'' . $myfunc . '\'=>"' . php_addslashes($funcdef) . '"],[[["' . $myfunc . '",[],' . strval(TC_KNOWN) . ',\'\',\'\']]]])']; } // Generate standard PHP code for directive if (isset($GLOBALS['DIRECTIVES_NEEDING_VARS'][$directive_name])) { $regular_code = 'ecv($cl,[],' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',[' . $directive_params_with_internal . ',\'vars\'=>$parameters],"' . php_addslashes($template_name) . '")'; } else { $regular_code = 'ecv($cl,[],' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',[' . $directive_params_with_internal . '],"' . php_addslashes($template_name) . '")'; } if (isset($GLOBALS['DIRECTIVES_NEEDING_VARS'][$directive_name])) { $regular_code_with_faux = 'ecv($cl,[],' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',[' . $directive_params_with_internal_with_faux . ',\'vars\'=>$parameters],"' . php_addslashes($template_name) . '")'; } else { $regular_code_with_faux = 'ecv($cl,[],' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',[' . $directive_params_with_internal_with_faux . '],"' . php_addslashes($template_name) . '")'; } // See if we can completely optimise out a directive if (tc_is_all_static($directive_params)) { switch ($directive_name) { case 'IF': case 'IF_EMPTY': case 'IF_NON_EMPTY': case 'BOX': $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $regular_code_with_faux . ';', $tpl_funcs, [], $cl); if ($eval !== '') { tc_add_to_current_level_data($current_level_data, $just_done_string, $directive_internal); } else { // Nothing will render from under here, so wipe out preprocessable bits that were under there $preprocessable_bits = []; } break 2; } } // Code generation, with some smart PHP-equivalent substitutions switch ($directive_name) { case 'COMMENT': break; case 'NO_PREPROCESSING': tc_add_to_current_level_data($current_level_data, $just_done_string, $directive_internal); $preprocessable_bits = array_pop($preprocessable_bits_stack); $num_preprocessable_bits = count($preprocessable_bits); break; case 'IF': // Optimise simple expressions to PHP $matches = []; if (preg_match('#^\((\(\([^()]+\)(==|!=)\([^()]+\))\)\?"1":"0"\)\)$#', $first_directive_param, $matches) !== 0) { $new_line = '(' . $matches[1] . '?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; } // Normal IF then (actually it's implemented as ternary un PHP) $new_line = '((' . $first_directive_param . '=="1")?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_EMPTY': $new_line = '((' . $first_directive_param . '==\'\')?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_NON_EMPTY': $new_line = '((' . $first_directive_param . '!=\'\')?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_PASSED': $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($eval)) { $eval = ''; } if ($parameters_used !== null) { $parameters_used[$eval] = true; } $new_line = '(isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ')?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_NON_PASSED': $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($eval)) { $eval = ''; } if ($parameters_used !== null) { $parameters_used[$eval] = true; } $new_line = '(!isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ')?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_PASSED_AND_TRUE': $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($eval)) { $eval = ''; } if ($parameters_used !== null) { $parameters_used[$eval] = true; } $new_line = '((isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ') && (otp($bound_' . preg_replace('#[^\w]#', '', $eval) . ')=="1"))?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'IF_NON_PASSED_OR_FALSE': $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($eval)) { $eval = ''; } if ($parameters_used !== null) { $parameters_used[$eval] = true; } $new_line = '((!isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ') || (otp($bound_' . preg_replace('#[^\w]#', '', $eval) . ')!="1"))?(' . $directive_internal . '):\'\')'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'WHILE': $new_line = 'closure_while_loop([$parameters,$cl],' . "\n" . 'recall_named_function(\'' . uniqid('', true) . '\',\'$parameters,$cl\',"extract(\$parameters,EXTR_PREFIX_ALL,\'bound\'); return (' . php_addslashes($first_directive_param) . ')==\"1\";"),' . "\n" . 'recall_named_function(\'' . uniqid('', true) . '\',\'$parameters,$cl\',"extract(\$parameters,EXTR_PREFIX_ALL,\'bound\'); return ' . php_addslashes($directive_internal) . ';"))'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'LOOP': if (!empty($directive_params)) { $new_line = 'closure_loop([' . $directive_params . ',\'vars\'=>$parameters],[$parameters,$cl],' . "\n" . 'recall_named_function(\'' . uniqid('', true) . '\',\'$parameters,$cl\',"extract(\$parameters,EXTR_PREFIX_ALL,\'bound\'); return ' . php_addslashes($directive_internal) . ';"))'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); $parameter = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($parameter)) { $parameter = ''; } if ($parameters_used !== null) { $parameters_used[$parameter] = true; } } break; case 'PHP': $new_line = 'closure_eval(' . $directive_internal . ',$parameters)'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); break; case 'PARAMS_JSON': if (!empty($directive_params)) { $new_line = 'closure_params_json([' . $directive_params . ',\'vars\'=>$parameters],[$parameters,$cl],' . "\n" . 'recall_named_function(\'' . uniqid('', true) . '\',\'$parameters,$cl\',"extract(\$parameters,EXTR_PREFIX_ALL,\'bound\'); return ' . php_addslashes($directive_internal) . ';"))'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); if ($parameters_used !== null) { foreach ($directive_opener_params as $directive_param) { $eval = tempcode_compiler_eval('return ' . implode('.', $directive_param) . ';', $tpl_funcs, [], $cl); if (is_string($eval)) { $parameters_used[$eval] = true; } } } } break; case 'INCLUDE': global $FILE_ARRAY; $tpl_funcs = []; $included_template_name = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($included_template_name)) { $included_template_name = ''; } $tpl_params = []; $var_data = tempcode_compiler_eval('return ' . implode('.', $past_level_data) . ';', $tpl_funcs, [], $cl); $substitutions = parse_tempcode_include($var_data, $tpl_params); if ((!$no_preprocess) && (empty($tpl_params)) && (empty($substitutions)) && (!isset($FILE_ARRAY))) { // Simple case where no separate binding context of variables needed $_ex = isset($directive_opener_params[1 + 1 + 2]) ? tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[1 + 2]) . ';', $tpl_funcs, [], $cl) : ''; if (!is_string($_ex)) { $_ex = ''; } if ($_ex == '') { $_ex = '.tpl'; } $_td = isset($directive_opener_params[1 + 2 + 2]) ? tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[2 + 2]) . ';', $tpl_funcs, [], $cl) : ''; if (!is_string($_td)) { $_td = ''; } if ($_td == '') { $_td = 'templates'; } $_theme = isset($directive_opener_params[1 + 3 + 2]) ? tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[3 + 2]) . ';', $tpl_funcs, [], $cl) : ''; if (!is_string($_theme)) { $_theme = ''; } if ($_theme == '') { $_theme = $theme; } $_force_original = isset($directive_opener_params[1 + 4 + 2]) ? tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[4 + 2]) . ';', $tpl_funcs, [], $cl) : ''; if (!is_string($_force_original)) { $_force_original = ''; } if ($_force_original != '1') { $_force_original = '0'; } $found = find_template_place($included_template_name, $_theme, $_ex, $_td, ($template_name === $included_template_name) || ($_force_original == '1')); if (($found !== null) && ($found[1] !== null)) { $_theme = $found[0]; $full_path = get_custom_file_base() . '/themes/' . $_theme . $found[1] . $included_template_name . $found[2]; if (!is_file($full_path)) { $full_path = get_file_base() . '/themes/' . $_theme . $found[1] . $included_template_name . $found[2]; } if (is_file($full_path)) { $file_contents = cms_file_get_contents_safe($full_path, FILE_READ_LOCK | FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM); } else { $file_contents = ''; } list($_current_level_data, $_preprocessable_bits) = compile_template($file_contents, $included_template_name, $theme, $lang, $tolerate_errors, $parameters, $parameters_used, $found[2], $found[1]); foreach ($_current_level_data as $new_line) { tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } if ($added_preprocessable_bits) { array_pop($preprocessable_bits); } $preprocessable_bits = array_merge($preprocessable_bits, $_preprocessable_bits); break; } } else { $parameters_used = null; // We don't know what the INCLUDE directive may need } // no break case 'IF_IN_ARRAY': case 'IF_NOT_IN_ARRAY': case 'IF_ARRAY_EMPTY': case 'IF_ARRAY_NON_EMPTY': case 'COUNT': case 'IMPLODE': case 'OF': if ($parameters_used !== null) { if ($directive_name == 'IF_IN_ARRAY' || $directive_name == 'NOT_IN_ARRAY' || $directive_name == 'IF_ARRAY_EMPTY' || $directive_name == 'IF_ARRAY_NON_EMPTY' || $directive_name == 'COUNT') { $parameter = tempcode_compiler_eval('return ' . $first_directive_param . ';', $tpl_funcs, [], $cl); if (!is_string($parameter)) { $parameter = ''; } $parameters_used[$parameter] = true; } if ($directive_name == 'IMPLODE') { if (isset($directive_opener_params[3])) { $parameter = tempcode_compiler_eval('return ' . implode('.', $directive_opener_params[3]) . ';', $tpl_funcs, [], $cl); if (!is_string($parameter)) { $parameter = ''; } $parameters_used[$parameter] = true; } } if ($directive_name == 'OF') { $eval = tempcode_compiler_eval('return ' . $directive_internal . ';', $tpl_funcs, [], $cl); if (is_string($eval)) { $parameters_used[$eval] = true; } } } // no break default: tc_add_to_current_level_data($current_level_data, $just_done_string, $regular_code); break; } } else { $tpl_funcs = []; $eval = tempcode_compiler_eval('return ' . $first_param . ';', $tpl_funcs, [], $cl); if (!is_string($eval)) { $eval = ''; } $directive_name = $eval; if (isset($GLOBALS['DIRECTIVES_NEEDING_VARS'][$directive_name])) { $new_line = 'ecv($cl,[' . implode(',', array_map('strval', $escaped)) . '],' . strval(TC_DIRECTIVE) . ',' . $first_param . ',[' . $_opener_params . ',\'vars\'=>$parameters],"' . php_addslashes($template_name) . '")'; } else { $new_line = 'ecv($cl,[' . implode(',', array_map('strval', $escaped)) . '],' . strval(TC_DIRECTIVE) . ',' . $first_param . ',[' . $_opener_params . '],"' . php_addslashes($template_name) . '")'; } tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line); } } break; case ',': // NB: Escaping via "\," was handled in our regexp split switch ($current_level_mode) { case PARSE_NO_MANS_LAND: case PARSE_DIRECTIVE_INNER: $new_line = '","'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); break; default: $current_level_params[] = $current_level_data; $current_level_data = []; $just_done_string = false; break; } break; default: $literal = php_addslashes(str_replace(['\,', '\}', '\{'], [',', '}', '{'], $next_token)); if ($GLOBALS['XSS_DETECT']) { ocp_mark_as_escaped($literal); } $new_line = '"' . $literal . '"'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); break; } } require_code('comcode'); if (!peek_lax_comcode()) { if (!empty($stack)) { if (!$tolerate_errors) { warn_exit(do_lang_tempcode('UNCLOSED_DIRECTIVE_OR_BRACE', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))), false, true); } } } if ($current_level_data === ['']) { $current_level_data = []; $just_done_string = false; $new_line = '""'; tc_add_to_current_level_data($current_level_data, $just_done_string, $new_line, true); } $STUCK_ABORT_SIGNAL = $sas_bak; return [$current_level_data, $preprocessable_bits]; } /** * Append some data to the current level of the Tempcode stack. * * @param array $current_level_data The data being appended together on the current level of the stack * @param boolean $just_done_string Whether the most recent thing appended to $current_level_data was a simple string literal * @param string $new_line More data to be appended on the current level of the stack (not technically a line, but we call it that) * @param boolean $doing_string Whether the new data is known to be a simple string literal */ function tc_add_to_current_level_data(array &$current_level_data, bool &$just_done_string, string $new_line, bool $doing_string = false) { if ($just_done_string && $doing_string) { $last_i = count($current_level_data) - 1; $previous_line = $current_level_data[$last_i]; $current_level_data[$last_i] = substr($previous_line, 0, strlen($previous_line) - 1) . substr($new_line, 1); } else { $current_level_data[] = $new_line; } $just_done_string = $doing_string; } /** * Find some opening params as a PHP array, for temporary use within the compiler logic. * * @param string $_opener_params The parameters in PHP code format * @return ?array Parameters (null: could not evaluate, probably due to dynamism) */ function tc_eval_opener_params(string $_opener_params) : ?array { $cl = fallback_lang(); $tpl_funcs = []; return tempcode_compiler_eval('return [' . $_opener_params . '];', $tpl_funcs, [], $cl); } /** * Find if some opening parameters are definitely static. * * @param string $_opener_params The parameters in PHP code format * @return boolean Is static */ function tc_is_all_static(string $_opener_params) : bool { if (strpos($_opener_params, '$bound_') !== false) { return false; } if (strpos($_opener_params, 'ecv(') !== false) { return false; } return true; } /** * Find whether the symbol may be optimised out. * * @param string $symbol The symbol * @return boolean May optimise */ function may_optimise_out_symbol(string $symbol) : bool { global $SITE_INFO; $symbol = get_symbol_hook_name($symbol); $hook_ob = get_hook_ob('systems', 'symbols', filter_naughty_harsh($symbol), 'Hook_symbol_', true); if ($hook_ob === null) { return false; } $info = $hook_ob->info(); if (($info === null) || (!isset($info['compile']))) { return false; } $v = $info['compile']; $end_ret = false; if (($v & SYMBOL_COMPILE_STATIC_IF_AGGRESSIVE) !== 0) { $end_ret = true; if ((!function_exists('get_value')) || (get_value('aggressive_tempcode_compilation') !== '1')) { return false; } } if (($v & SYMBOL_COMPILE_STATIC_SAFE_SIMPLE_KEEP) !== 0) { $end_ret = true; if ((!isset($SITE_INFO['no_keep_params'])) || ($SITE_INFO['no_keep_params'] === '0')) { return false; } } if (($v & SYMBOL_COMPILE_STATIC_SAFE_SIMPLE_BASE_URLS) !== 0) { $end_ret = true; if ($GLOBALS['DEV_MODE']) { return false; // May be experimenting with different base URLs, e.g. both http and https } if (running_script('install')) { return false; } foreach (array_keys($SITE_INFO) as $key) { if (substr($key, 0, 13) === 'ZONE_MAPPING_') { return false; } } } if (($v & SYMBOL_COMPILE_STATIC_SAFE_SIMPLE_JAVASCRIPT) !== 0) { $end_ret = true; if ((!function_exists('get_option')) || (get_option('detect_javascript') === '1')) { return false; } } return $end_ret; } /** * A template has not been structurally cached, so compile it and store in the cache. * * @param ID_TEXT $theme The theme the template is in the context of * @param string $directory Subdirectory type to look in. Surrounded by '/', unlike with $directory parameters to most other functions (performance reasons) * @param ID_TEXT $codename The codename of the template * @param ID_TEXT $_codename The actual codename to use for the template in the cache (e.g. foo_mobile) * @param LANGUAGE_NAME $lang The language the template is in the context of * @param string $suffix File type suffix of template file (e.g. .tpl) * @set .tpl .js .xml .txt .css * @param ?ID_TEXT $theme_orig The theme to cache in (null: main theme) * @param ?array $parameters Parameters to hard-code in during compilation (null: no hard-coding) * @param boolean $non_custom_only Whether we only searched in the default templates * @return Tempcode The compiled Tempcode * * @ignore */ function _do_template(string $theme, string $directory, string $codename, string $_codename, string $lang, string $suffix, ?string $theme_orig = null, ?array &$parameters = null, bool $non_custom_only = false) : object { if ($theme_orig === null) { $theme_orig = $theme; } $base_dir = get_custom_file_base() . '/themes/'; if (!is_file($base_dir . $theme . $directory . $codename . $suffix)) { $base_dir = get_file_base() . '/themes/'; } global $CACHE_TEMPLATES, $FILE_ARRAY, $IS_TEMPLATE_PREVIEW_OP_CACHE, $SITE_INFO; if ($IS_TEMPLATE_PREVIEW_OP_CACHE === null) { fill_template_preview_op_cache(); } // Load file if (isset($FILE_ARRAY)) { $template_contents = unixify_line_format(handle_string_bom(file_array_get('themes/' . $theme . $directory . $codename . $suffix))); } else { $_path = $base_dir . filter_naughty($theme . $directory . $codename) . $suffix; $template_contents = cms_file_get_contents_safe($_path, FILE_READ_UNIXIFIED_TEXT | FILE_READ_BOM); } // Special case: HTML template file $matches = []; if (!$GLOBALS['IN_MINIKERNEL_VERSION'] && ($GLOBALS['SEMI_DEV_MODE']) && ($suffix === '.tpl') && (preg_match('#]*>.*<\/script>#is', $template_contents, $matches) > 0)) { if (strpos($matches[0], 'CSP_NONCE_HTML') === false) { attach_message(do_lang_tempcode('DO_NOT_USE_INLINE_SCRIPT_TAGS', escape_html($codename)), 'warn', false, true); } } // Strip off trailing final lines from single lines templates. Editors often put these in, and it causes annoying "visible space" issues if ((substr($template_contents, -1, 1) === "\n") && (substr_count($template_contents, "\n") === 1)) { $template_contents = substr($template_contents, 0, strlen($template_contents) - 1); } // Special case: Template previews if ($IS_TEMPLATE_PREVIEW_OP_CACHE) { $_template_file = str_replace('_custom', '', trim($directory, '/')) . '/' . $codename . $suffix; $preview_post_param_key = 'e_' . get_dynamic_file_parameter($_template_file); $test = post_param_string($preview_post_param_key, null); if ($test !== null) { $template_contents = $test; } } // Do compilation cms_profile_start_for('_do_template'); $result = template_to_tempcode($template_contents, 0, false, $codename, $theme_orig, $lang, false, $parameters, $suffix, $directory); cms_profile_end_for('_do_template', $codename . $suffix); // Save into cache if (($CACHE_TEMPLATES) && (has_caching_for('template', $codename)) && ($parameters === null) && (!$IS_TEMPLATE_PREVIEW_OP_CACHE)) { $path2 = get_custom_file_base() . '/themes/' . $theme_orig . '/templates_cached/' . filter_naughty($lang); $_path2 = $path2 . '/' . filter_naughty($_codename) . ($non_custom_only ? '_non_custom_only' : '') . $suffix . '.tcp'; require_code('files'); $data_to_write = '<' . '?php' . "\n" . $result->to_assembly($lang) . "\n"; cms_file_put_contents_safe($_path2, $data_to_write, FILE_WRITE_FAILURE_SOFT | FILE_WRITE_FIX_PERMISSIONS); } return $result; } /** * Convert template text into Tempcode format. * * @param string $text The template text * @param integer $symbol_pos The position we are looking at in the text * @param boolean $inside_directive Whether this text is in fact a directive, about to be put in the context of a wider template * @param ID_TEXT $codename The codename of the template (blank: not from a file) * @param ?ID_TEXT $theme The theme it is for (null: current theme) * @param ?ID_TEXT $lang The language it is for (null: current language) * @param boolean $tolerate_errors Whether to tolerate errors * @param ?array $parameters Parameters to hard-code in during compilation (null: no hard-coding) * @param ?string $suffix File type suffix of template file (e.g. .tpl) (null: not from a file) * @set .tpl .js .xml .txt .css * @param ?string $directory Subdirectory type to look in. Surrounded by '/', unlike with $directory parameters to most other functions (performance reasons) (null: not from a file) * @return mixed The converted/compiled template as Tempcode, OR if a directive, encoded directive information */ function template_to_tempcode(string $text, int $symbol_pos = 0, bool $inside_directive = false, string $codename = '', ?string $theme = null, ?string $lang = null, bool $tolerate_errors = false, ?array &$parameters = null, ?string $suffix = null, ?string $directory = null) { if ($theme === null) { $theme = isset($GLOBALS['FORUM_DRIVER']) ? $GLOBALS['FORUM_DRIVER']->get_theme() : 'default'; } if ($lang === null) { $lang = user_lang(); } $parameters_used = null; if ($parameters !== null) { $parameters_used = []; } list($parts, $preprocessable_bits) = compile_template(substr($text, $symbol_pos), $codename, $theme, $lang, $tolerate_errors, $parameters, $parameters_used, $suffix, $directory); if (($parameters !== null) && ($parameters_used !== null)) { foreach ($parameters as $key => $parameter) { if (!isset($parameters_used[$key])) { unset($parameters[$key]); } } } if (count($parts) === 0) { return new Tempcode(); } $is_all_static = true; $parts_groups = []; $parts_group = []; foreach ($parts as $part) { $parts_group[] = $part; if (!tc_is_all_static($part)) { $is_all_static = false; } } if (!empty($parts_group)) { $parts_groups[] = $parts_group; } $funcdefs = []; $seq_parts = []; foreach ($parts_groups as $parts_group) { $myfunc = 'tcpfunc_' . fast_uniqid() . '_' . strval(count($seq_parts) + 1); $funcdef = build_closure_function($myfunc, $parts_group); $funcdefs[$myfunc] = $funcdef; $seq_parts[] = [[$myfunc, [/* Is currently unbound */], TC_KNOWN, '', '']]; } $ret = new Tempcode([$funcdefs, $seq_parts]); // Parameters will be bound in later. if (!empty($preprocessable_bits)) { if (!isset($ret->preprocessable_bits)) { $ret->preprocessable_bits = []; } $ret->preprocessable_bits = array_merge($ret->preprocessable_bits, $preprocessable_bits); } $ret->codename = $codename; if ($is_all_static) { if ($parameters !== null) { foreach ($parameters as $parameter) { if (is_object($parameter) && !isset($parameter->is_all_static)) { $is_all_static = false; } } } if ($is_all_static) { $ret->is_all_static = true; } } return $ret; } /** * Build a closure function for a compiled template. * * @param string $myfunc The function name * @param array $parts An array of lines to be output, each one in PHP format * @return string Finished PHP code */ function build_closure_function(string $myfunc, array $parts) : string { if (empty($parts)) { $parts = ['""']; } $code = ''; foreach ($parts as $part) { if ($code !== '') { $code .= ",\n\t"; } $code .= $part; } if (strpos($code, '$bound') === false) { $funcdef = "\$tpl_funcs['$myfunc']=\$KEEP_TPL_FUNCS['$myfunc']=recall_named_function('" . uniqid('', true) . "','\$parameters,\$cl',\"echo " . php_addslashes($code) . ";\");"; } else { $funcdef = "\$tpl_funcs['$myfunc']=\$KEEP_TPL_FUNCS['$myfunc']=recall_named_function('" . uniqid('', true) . "','\$parameters,\$cl',\"extract(\\\$parameters,EXTR_PREFIX_ALL,'bound'); echo " . php_addslashes($code) . ";\");"; } // Eval version also works. Easier to debug. Less performant due to re-parse requirement each time it is called if ($GLOBALS['DEV_MODE']) { if (strpos($code, 'isset($bound') !== false) { // Horrible but efficient code needed to allow IF_PASSED/IF_NON_PASSED to keep working when templates are put adjacent to each other, where some have it, and don't. This is needed as eval does not set a scope block. $reset_code = "eval(\\\$FULL_RESET_VAR_CODE); "; } elseif (strpos($code, '$bound') !== false) { $reset_code = "eval(\\\$RESET_VAR_CODE); "; } else { $reset_code = ''; } $funcdef = "\$tpl_funcs['$myfunc']=\"{$reset_code}echo " . php_addslashes($code) . ";\";"; } return $funcdef; } /** * Evaluate some Tempcode PHP, with ability to better debug. * * @param ?string $code Code to evaluate (null: code not found) * @param ?array $tpl_funcs Evaluation code context (null: N/A) * @param ?array $parameters Evaluation parameters (null: N/A) * @param ?ID_TEXT $cl Language (null: N/A) * @return mixed Result * * @ignore */ function tempcode_compiler_eval(?string $code, ?array &$tpl_funcs = null, ?array $parameters = null, ?string $cl = null) { global $NO_EVAL_CACHE, $XSS_DETECT, $KEEP_TPL_FUNCS, $FULL_RESET_VAR_CODE, $RESET_VAR_CODE; if ($code === '') { return ''; } $result = @eval($code); // Simple error suppressing because we totally expect this to sometimes fail. We can't always set the full Tempcode context correctly. return $result; }