<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core
 */

/*EXTRA FUNCTIONS: ILess_Autoloader|ILess_Parser|ILess_Cache_FileSystem|less_proxy_compile|proc_.*|escapeshellarg|stream_get_contents*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__tempcode_compiler()
{
    if (!defined('PARSE_NO_MANS_LAND')) {
        define('PARSE_NO_MANS_LAND', 0);
        define('PARSE_DIRECTIVE', 1);
        define('PARSE_SYMBOL', 2);
        define('PARSE_LANGUAGE_REFERENCE', 3);
        define('PARSE_PARAMETER', 4);
        define('PARSE_DIRECTIVE_INNER', 5);
    }

    global $DIRECTIVES_NEEDING_VARS;
    $DIRECTIVES_NEEDING_VARS = array('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);

    // Work out what symbols may be compiled out (look at patterns at top of caches3.php if changing this)
    global $COMPILABLE_SYMBOLS;
    $_compilable_symbols = array(
        'LANG',
        'THEME',
        'VERSION_NUMBER',
        'VERSION',
        'SITE_NAME',
        'CHARSET',
        'ADDON_INSTALLED',
        'CONFIG_OPTION',
        'VALUE_OPTION',
        'COPYRIGHT',
        'BASE_URL_NOHTTP',
        'CUSTOM_BASE_URL_NOHTTP',
        'BRAND_NAME',
        'BRAND_BASE_URL',
        'CNS',
        'VALID_FILE_TYPES',
        'COOKIE_PATH',
        'COOKIE_DOMAIN',
        'SESSION_COOKIE_NAME',
        'INLINE_STATS',
        'CURRENCY_SYMBOL',
        'DOMAIN',
        'STAFF_ADDRESS',
        'STAFF_ADDRESS_PURE',
        'SHOW_DOCS',
        'SITE_SCOPE',
        'IMG_WIDTH',
        'IMG_HEIGHT',
        'IMG_INLINE',
        'SSW',
        'MAILTO',
    );
    global $SITE_INFO;
    if ((isset($SITE_INFO['no_keep_params'])) && ($SITE_INFO['no_keep_params'] == '1')) {
        $_compilable_symbols[] = 'KEEP';
        $_compilable_symbols[] = 'PAGE_LINK';
        $_compilable_symbols[] = 'FIND_SCRIPT';
    }
    if (!addon_installed('ssl')) {
        $_compilable_symbols[] = 'IMG';
        $_compilable_symbols[] = 'BASE_URL';
        $_compilable_symbols[] = 'CUSTOM_BASE_URL';
    }
    if ((function_exists('get_option')) && (get_option('detect_javascript') == '0')) {
        $_compilable_symbols[] = 'JS_ON';
    }
    $COMPILABLE_SYMBOLS = array();
    foreach ($_compilable_symbols as $s) {
        $COMPILABLE_SYMBOLS['"' . $s . '"'] = 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($bits, $i)
{
    $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($data)
{
    // HTML comment
    $data = cms_preg_replace_safe('#<!--\s*\{(((?!-->).)*)\}\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
 * @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
 * @return array A pair: array Compiled result structure, array preprocessable bits (special stuff needing attention that is referenced within the template)
 */
function compile_template($data, $template_name, $theme, $lang, $tolerate_errors = false)
{
    if (strpos($data, '/*{$,Parser hint: pure}*/') !== false) {
        return array(array('"' . php_addslashes(preg_replace('#\{\$,.*\}#U', '', str_replace('/*{$,Parser hint: pure}*/', '/*no minify*/', $data))) . '"'), array());
    }

    $data = substitute_comment_encapsulated_tempcode($data);

    $data = preg_replace('#<\?php(.*)\?' . '>#sU', '{+START,PHP}${1}{+END}', $data);

    global $COMPILABLE_SYMBOLS, $STUCK_ABORT_SIGNAL;

    $sas_bak = $STUCK_ABORT_SIGNAL;
    require_code('lang');
    require_code('urls');
    $cl = fallback_lang();
    $bits = array_values(preg_split('#(?<!\\\\)(\{(?![A-Z][a-z])(?=[\dA-Z\$\+\!\_]+[\.`%\*=\;\#\-~\^\|\'&/@+]*))|((?<!\\\\)\,)|((?<!\\\\)\})#', $data, -1, PREG_SPLIT_DELIM_CAPTURE));  // One error mail showed on a server it had weird indexes, somehow. Hence the array_values call to reindex it
    $count = count($bits);
    $stack = array();
    $current_level_mode = PARSE_NO_MANS_LAND;
    $current_level_data = array();
    $current_level_params = array();
    $preprocessable_bits = array();
    $num_preprocessable_bits = 0;
    $preprocessable_bits_stack = array();
    for ($i = 0; $i < $count; $i++) {
        $next_token = $bits[$i];
        if ($next_token === '') {
            continue;
        }
        if (($i !== $count - 1) && ($next_token === '{') && (preg_match('#^[\dA-Z\$\+\!\_]#', $bits[$i + 1]) === 0)) {
            $current_level_data[] = '"{}"';
            continue;
        }

        switch ($next_token) {
            case '{':
                // Open a new level
                $stack[] = array($current_level_mode, $current_level_data, $current_level_params, null, null, null, count($preprocessable_bits), null, null, null);
                ++$i;
                $next_token = isset($bits[$i]) ? $bits[$i] : null;
                if ($next_token === null) {
                    if ($tolerate_errors) {
                        continue 2;
                    }
                    warn_exit(do_lang_tempcode('ABRUPTED_DIRECTIVE_OR_BRACE', escape_html($template_name), escape_html(integer_format(1 + substr_count(substr($data, 0, _length_so_far($bits, $i)), "\n")))));
                }
                $current_level_data = array();
                switch (isset($next_token[0]) ? $next_token[0] : '') {
                    case '$':
                        $current_level_mode = PARSE_SYMBOL;
                        $current_level_data[] = '"' . php_addslashes(($next_token === '$') ? '' : trim(substr($next_token, 1))) . '"';
                        break;
                    case '+':
                        $current_level_mode = PARSE_DIRECTIVE;
                        $current_level_data[] = '"' . php_addslashes(($next_token === '+') ? '' : trim(substr($next_token, 1))) . '"';
                        break;
                    case '!':
                        $current_level_mode = PARSE_LANGUAGE_REFERENCE;
                        $current_level_data[] = '"' . php_addslashes(($next_token === '!') ? '' : trim(substr($next_token, 1))) . '"';
                        break;
                    default:
                        $current_level_mode = PARSE_PARAMETER;
                        $current_level_data[] = '"' . php_addslashes(trim($next_token)) . '"';
                        break;
                }
                $current_level_params = array();
                break;

            case '}':
                if (($stack === array()) || ($current_level_mode === PARSE_DIRECTIVE_INNER)) {
                    $literal = php_addslashes($next_token);

                    if ($GLOBALS['XSS_DETECT']) {
                        ocp_mark_as_escaped($literal);
                    }

                    $current_level_data[] = '"' . $literal . '"';
                    break;
                }

                $opener_params = array_merge($current_level_params, array($current_level_data));
                $__first_param = array_shift($opener_params);
                if (count($__first_param) !== 1) {
                    warn_exit(do_lang_tempcode('COMPLEX_FIRST_PARAMETER'));
                }
                $_first_param = $__first_param[0];

                if (($bits[$i - 1] === '') && (count($current_level_data) === 0)) {
                    $current_level_data[] = '""';
                }

                // Return to the previous level
                $past_level_data = $current_level_data;
                $past_level_params = $current_level_params;
                $past_level_mode = $current_level_mode;
                if ($stack === array()) {
                    if (!$tolerate_errors) {
                        warn_exit(do_lang_tempcode('TEMPCODE_TOO_MANY_CLOSES', escape_html($template_name), escape_html(integer_format(1 + _length_so_far($bits, $i)))));
                    }
                } else {
                    list($current_level_mode, $current_level_data, $current_level_params, , , , $num_preprocessable_bits) = array_pop($stack);
                }

                // Handle the level we just closed
                $_escaped = str_split(preg_replace('#[^:\.`%\*=\;\#\-~\^\|\'&/@+]:?#', '', $_first_param)); // :? is so that the ":" in language string IDs does not get considered an escape
                $escaped = array();
                $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 '&':
                            $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;

                        // 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 (+ is already taken)
                    }
                }
                $_opener_params = '';
                foreach ($opener_params as $oi => &$oparam) {
                    if ($oparam === array()) {
                        $oparam = array('""');
                        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"':
                                    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[3] === PARSE_DIRECTIVE) && (isset($level_test[5][1])) && (isset($level_test[5][1][0])) && ($level_test[5][1][0] === '"LOOP"')) { // For a loop, we need to do full evaluation of symbol parameters as it may be bound to a loop variable
                                            $tpl_funcs = array();
                                            $eval = debug_eval('return array(' . $_opener_params . ');', $tpl_funcs, array(), $cl);
                                            if (is_array($eval)) {
                                                $pp_bit = array(array(), TC_SYMBOL, str_replace('"', '', $first_param), $eval);
                                                $preprocessable_bits[] = $pp_bit;
                                            }
                                            break 2;
                                        }
                                    }

                                    $symbol_params = array();
                                    foreach ($opener_params as $param) {
                                        $myfunc = 'tcpfunc_' . fast_uniqid();
                                        $funcdef = build_closure_function($myfunc, $param);
                                        $symbol_params[] = new Tempcode(array(array($myfunc => $funcdef), array(array(array($myfunc, array(/* Is currently unbound */), TC_KNOWN, '', ''))))); // Parameters will be bound in later.
                                    }

                                    $pp_bit = array(array(), TC_SYMBOL, str_replace('"', '', $first_param), $symbol_params);

                                    $preprocessable_bits[] = $pp_bit;
                                    break;
                            }
                        }

                        if ((($first_param === '"IMG"') || ($first_param === '"IMG_INLINE"')) && (strpos($_opener_params, ',') === false)) { // Needed to ensure correct binding
                            $_opener_params .= ',"0","' . php_addslashes($theme) . '"';
                        }

                        if ($first_param === '"?"') { // Optimise out static ternary
                            if (implode('.', $opener_params[0]) === '"1"') {
                                if (isset($opener_params[1])) {
                                    $current_level_data[] = implode('.', $opener_params[1]);
                                }
                                break;
                            }
                            if (implode('.', $opener_params[0]) === '"0"') {
                                if (isset($opener_params[2])) {
                                    $current_level_data[] = implode('.', $opener_params[2]);
                                }
                                break;
                            }
                        }

                        if ($first_param === '""') { // Optimise out comments
                            break;
                        }

                        // Optimise simple PHP-compatible operators
                        foreach (array('EQ' => '==', 'NEQ' => '!=') as $symbol_op => $php_op) {
                            if (($first_param === '"' . $symbol_op . '"') && (count($opener_params) === 2)) {
                                $current_level_data[] = '(((' . implode('.', $opener_params[0]) . ')' . $php_op . '(' . implode('.', $opener_params[1]) . '))?"1":"0")';
                                break 2;
                            }
                        }
                        foreach (array('AND' => '&&', 'OR' => '||') as $symbol_op => $php_op) {
                            if (($first_param === '"' . $symbol_op . '"') && (count($opener_params) === 2)) {
                                $current_level_data[] = '(((' . implode('.', $opener_params[0]) . ')=="1")' . $php_op . '(' . implode('.', $opener_params[1]) . '=="1")?"1":"0")';
                                break 2;
                            }
                        }
                        if (($first_param === '"?"') && (count($opener_params) === 3) && (count($escaped) === 0)) {
                            $current_level_data[] = '(((' . implode('.', $opener_params[0]) . ')=="1")?(' . implode('.', $opener_params[1]) . '):(' . implode('.', $opener_params[2]) . '))';
                            break 2;
                        }

                        // Okay, a fully dynamic symbol
                        $name = preg_replace('#(^")|("$)#', '', $first_param);
                        if ($name === '?') {
                            $name = 'TERNARY';
                        }
                        if (function_exists('ecv_' . $name)) {
                            $new_line = 'ecv_' . $name . '($cl,array(' . implode(',', $escaped) . '),array(' . $_opener_params . '))';
                        } else {
                            $new_line = 'ecv($cl,array(' . implode(',', $escaped) . '),' . strval(TC_SYMBOL) . ',' . $first_param . ',array(' . $_opener_params . '))';
                        }
                        if ((isset($COMPILABLE_SYMBOLS[$first_param])) && (preg_match('#^[^\(\)]*$#', $_opener_params) !== 0)) { // Can optimise out?
                            $tpl_funcs = array();
                            $new_line = '"' . php_addslashes(debug_eval('return ' . $new_line . ';', $tpl_funcs, array(), $cl)) . '"';
                        } else {
                            // We want the benefit's of keep_ variables but not with having to do lots of individual URL moniker lookup queries - so use a static URL and KEEP_ symbol combination
                            if (($GLOBALS['OUTPUT_STREAMING']) && ($first_param === '"PAGE_LINK"') && (count($opener_params) === 1) && (preg_match('#^[^\(\)]*$#', $_opener_params) !== 0) && (function_exists('has_submit_permission')/*needed for moniker hooks to load up*/)) {
                                $tmp = $_GET;
                                foreach (array_keys($_GET) as $key) {
                                    if (substr($key, 0, 5) === 'keep_') {
                                        unset($_GET[$key]);
                                    }
                                }
                                $tpl_funcs = array();
                                $new_line = '"' . php_addslashes(debug_eval('return ' . $new_line . ';', $tpl_funcs, array(), $cl)) . '"';
                                $_GET = $tmp;
                                $current_level_data[] = $new_line;
                                $current_level_data[] = 'ecv_KEEP($cl,array(' . implode(',', $escaped) . '),array("' . ((strpos($new_line, '?') === false) ? '1' : '0') . '"))';
                                $GLOBALS['HAS_KEEP_IN_URL_CACHE'] = null; // The temporary $_GET change can cause this to go wrong
                                break;
                            }
                        }
                        $current_level_data[] = $new_line;
                        break;

                    case PARSE_LANGUAGE_REFERENCE:
                        $new_line = 'ecv($cl,array(' . implode(',', $escaped) . '),' . strval(TC_LANGUAGE_REFERENCE) . ',' . $first_param . ',array(' . $_opener_params . '))';
                        if (($_opener_params === '') && ($escaped === array())) { // Optimise it out for simple case?
                            $tpl_funcs = array();
                            $looked_up = do_lang(debug_eval('return ' . $first_param . ';', $tpl_funcs, array(), $cl), null, null, null, $lang, false);
                            if ($looked_up !== null) {
                                if (apply_tempcode_escaping($escaped, $_) === $looked_up) {
                                    $new_line = '"' . php_addslashes($looked_up) . '"';
                                }
                            }
                        }
                        $current_level_data[] = $new_line;
                        break;

                    case PARSE_PARAMETER:
                        $parameter = str_replace('"', '', str_replace("'", '', $first_param));
                        $parameter = preg_replace('#[^\w]#', '', $parameter); // security to stop PHP injection
                        if ($escaped === array(PURE_STRING)) {
                            $current_level_data[] = '$bound_' . php_addslashes($parameter);
                        } 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 ($escaped === array()) {
                                $current_level_data[] = $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'])) {
                                    $current_level_data[] = '(empty($bound_' . $parameter . '->pure_lang)?@htmlspecialchars(' . $temp . ',ENT_QUOTES | ENT_SUBSTITUTE,get_charset()):' . $temp . ')';
                                } else {
                                    if ($s_escaped === strval(ENTITY_ESCAPED)) {
                                        $current_level_data[] = '(empty($bound_' . $parameter . '->pure_lang)?apply_tempcode_escaping_inline(array(' . $s_escaped . '),' . $temp . '):' . $temp . ')';
                                    } else {
                                        $current_level_data[] = 'apply_tempcode_escaping_inline(array(' . $s_escaped . '),' . $temp . ')';
                                    }
                                }
                            }
                        }
                        break;
                }

                // Handle directive nesting
                if ($past_level_mode === PARSE_DIRECTIVE) {
                    if ($first_param === '"START"') { // START
                        // Open a new directive level
                        $stack[] = array($current_level_mode, $current_level_data, $current_level_params, $past_level_mode, $past_level_data, $past_level_params, count($preprocessable_bits), $first_param, $escaped, $no_preprocess);
                        $current_level_data = array();
                        $current_level_params = array();
                        $current_level_mode = PARSE_DIRECTIVE_INNER;
                        if ($opener_params === array(array('"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 ($past_level_data === array()) {
                            $past_level_data = array('""');
                        }
                        $past_level_params = $current_level_params;
                        $past_level_mode = $current_level_mode;
                        if ($stack === array()) {
                            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")))));
                        }
                        list($current_level_mode, $current_level_data, $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")))));
                        }
                        $directive_opener_params = array_merge($directive_level_params, array($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")))));
                        }

                        // Handle directive
                        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")))));
                        }
                        $directive_params = '';
                        $first_directive_param = '""';
                        if ($directive_opener_params[1] === array()) {
                            $directive_opener_params[1] = array('""');
                        }
                        $count_directive_opener_params = count($directive_opener_params);
                        for ($j = 2; $j < $count_directive_opener_params; $j++) {
                            if ($directive_opener_params[$j] === array()) {
                                $directive_opener_params[$j] = array('""');
                            }

                            if ($directive_params !== '') {
                                $directive_params .= ',';
                            }
                            $directive_params .= implode('.', $directive_opener_params[$j]);

                            if ($j === 2) {
                                $first_directive_param = implode('.', $directive_opener_params[$j]);
                            }
                        }
                        $tpl_funcs = array();
                        $eval = trim(debug_eval('return ' . implode('.', $directive_opener_params[1]) . ';', $tpl_funcs, array(), $cl));
                        if (!is_string($eval)) {
                            $eval = '';
                        }
                        $directive_name = $eval;
                        switch ($directive_name) {
                            case 'INCLUDE':
                            case 'FRACTIONAL_EDITABLE':
                                $tpl_funcs = array();
                                $eval = debug_eval('return array(' . $directive_params . ');', $tpl_funcs, array(), $cl);
                                if (is_array($eval)) {
                                    $pp_bit = array(array(), TC_DIRECTIVE, str_replace('"', '', $directive_name), $eval);
                                    $preprocessable_bits[] = $pp_bit;
                                }
                                break;
                        }
                        switch ($directive_name) {
                            case 'COMMENT':
                                break;

                            case 'NO_PREPROCESSING':
                                $current_level_data[] = implode('.', $past_level_data);
                                $preprocessable_bits = array_pop($preprocessable_bits_stack);
                                $num_preprocessable_bits = count($preprocessable_bits);
                                break;

                            case 'IF':
                                // Optimise out static NOT expressions
                                if (preg_match('#^ecv\(\$cl,array\(\),0,"NOT",array\("1"\)\)$#', $first_directive_param) !== 0) {
                                    $first_directive_param = '"0"';
                                }
                                if (preg_match('#^ecv\(\$cl,array\(\),0,"NOT",array\("0"\)\)$#', $first_directive_param) !== 0) {
                                    $first_directive_param = '"1"';
                                }

                                // Optimise out static boolean expressions
                                if (($first_directive_param === '((("1")==("1"))?"1":"0")') || ($first_directive_param === '(("0"=="0")?"1":"0")')) {
                                    $first_directive_param = '"1"';
                                } elseif (($first_directive_param === '((("1")==("0"))?"1":"0")') || ($first_directive_param === '(("0"=="1")?"1":"0")')) {
                                    $first_directive_param = '"0"';
                                }

                                // Optimise simple expressions to PHP
                                $matches = array();
                                if (preg_match('#^\((\(\([^()]+\)(==|!=)\([^()]+\))\)\?"1":"0"\)\)$#', $first_directive_param, $matches) !== 0) {
                                    $current_level_data[] = '(' . $matches[1] . '?(' . implode('.', $past_level_data) . '):\'\')';
                                    break;
                                }

                                // Optimise out static IFs
                                if ($first_directive_param === '"0"') {
                                    break;
                                }
                                if ($first_directive_param === '"1"') {
                                    $current_level_data[] = '(' . implode('.', $past_level_data) . ')';
                                    break;
                                }

                                // Normal IF then (actually it's implemented as ternary un PHP)
                                $current_level_data[] = '((' . $first_directive_param . '=="1")?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_EMPTY':
                                $current_level_data[] = '((' . $first_directive_param . '==\'\')?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_NON_EMPTY':
                                $current_level_data[] = '((' . $first_directive_param . '!=\'\')?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_PASSED':
                                $tpl_funcs = array();
                                $eval = debug_eval('return ' . $first_directive_param . ';', $tpl_funcs, array(), $cl);
                                if (!is_string($eval)) {
                                    $eval = '';
                                }
                                $current_level_data[] = '(isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ')?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_NON_PASSED':
                                $tpl_funcs = array();
                                $eval = debug_eval('return ' . $first_directive_param . ';', $tpl_funcs, array(), $cl);
                                if (!is_string($eval)) {
                                    $eval = '';
                                }
                                $current_level_data[] = '(!isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ')?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_PASSED_AND_TRUE':
                                $tpl_funcs = array();
                                $eval = debug_eval('return ' . $first_directive_param . ';', $tpl_funcs, array(), $cl);
                                if (!is_string($eval)) {
                                    $eval = '';
                                }
                                $current_level_data[] = '((isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ') && (otp($bound_' . preg_replace('#[^\w]#', '', $eval) . ')=="1"))?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'IF_NON_PASSED_OR_FALSE':
                                $tpl_funcs = array();
                                $eval = debug_eval('return ' . $first_directive_param . ';', $tpl_funcs, array(), $cl);
                                if (!is_string($eval)) {
                                    $eval = '';
                                }
                                $current_level_data[] = '((!isset($bound_' . preg_replace('#[^\w]#', '', $eval) . ') || (otp($bound_' . preg_replace('#[^\w]#', '', $eval) . ')!="1"))?(' . implode('.', $past_level_data) . '):\'\')';
                                break;

                            case 'WHILE':
                                $current_level_data[] = 'closure_while_loop(array($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(implode('.', $past_level_data)) . ';"))';
                                break;

                            case 'LOOP':
                                $current_level_data[] = 'closure_loop(array(' . $directive_params . ',\'vars\'=>$parameters),array($parameters,$cl),' . "\n" . 'recall_named_function(\'' . uniqid('', true) . '\',\'$parameters,$cl\',"extract(\$parameters,EXTR_PREFIX_ALL,\'bound\'); return ' . php_addslashes(implode('.', $past_level_data)) . ';"))';
                                break;

                            case 'PHP':
                                $current_level_data[] = 'closure_eval(' . implode('.', $past_level_data) . ',$parameters)';
                                break;

                            case 'INCLUDE':
                                global $FILE_ARRAY;
                                $tpl_funcs = array();
                                $eval = debug_eval('return ' . $first_directive_param . ';', $tpl_funcs, array(), $cl);
                                if (!is_string($eval)) {
                                    $eval = '';
                                }
                                if (((!$no_preprocess) && ($template_name === $eval)) || ((!$no_preprocess) && (get_param_string('special_page_type', '') === '')) && ($count_directive_opener_params === 3) && ($past_level_data === array('""')) && (!isset($FILE_ARRAY))) { // Simple case
                                    $found = find_template_place($eval, '', $theme, '.tpl', 'templates', $template_name === $eval);
                                    if (($found !== null) && ($found[1] !== null)) {
                                        $_theme = $found[0];
                                        $full_path = get_custom_file_base() . '/themes/' . $_theme . $found[1] . $eval . $found[2];
                                        if (!is_file($full_path)) {
                                            $full_path = get_file_base() . '/themes/' . $_theme . $found[1] . $eval . $found[2];
                                        }
                                        if (is_file($full_path)) {
                                            $filecontents = cms_file_get_contents_safe($full_path);
                                        } else {
                                            $filecontents = '';
                                        }
                                        list($_current_level_data, $_preprocessable_bits) = compile_template($filecontents, $eval, $theme, $lang);
                                        $current_level_data = array_merge($current_level_data, $_current_level_data);
                                        $preprocessable_bits = array_merge($preprocessable_bits, $_preprocessable_bits);
                                        break;
                                    }
                                }
                            // intentionally rolls on...

                            default:
                                if ($directive_params !== '') {
                                    $directive_params .= ',';
                                }
                                $directive_params .= implode('.', $past_level_data);

                                if (isset($GLOBALS['DIRECTIVES_NEEDING_VARS'][$directive_name])) {
                                    $current_level_data[] = 'ecv($cl,array(),' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',array(' . $directive_params . ',\'vars\'=>$parameters))';
                                } else {
                                    $current_level_data[] = 'ecv($cl,array(),' . strval(TC_DIRECTIVE) . ',' . implode('.', $directive_opener_params[1]) . ',array(' . $directive_params . '))';
                                }
                                break;
                        }
                    } else {
                        $tpl_funcs = array();
                        $eval = debug_eval('return ' . $first_param . ';', $tpl_funcs, array(), $cl);
                        if (!is_string($eval)) {
                            $eval = '';
                        }
                        $directive_name = $eval;
                        if (isset($GLOBALS['DIRECTIVES_NEEDING_VARS'][$directive_name])) {
                            $current_level_data[] = 'ecv($cl,array(' . implode(',', $escaped) . '),' . strval(TC_DIRECTIVE) . ',' . $first_param . ',array(' . $_opener_params . ',\'vars\'=>$parameters))';
                        } else {
                            $current_level_data[] = 'ecv($cl,array(' . implode(',', $escaped) . '),' . strval(TC_DIRECTIVE) . ',' . $first_param . ',array(' . $_opener_params . '))';
                        }
                    }
                }
                break;

            case ',': // NB: Escaping via "\," was handled in our regexp split
                switch ($current_level_mode) {
                    case PARSE_NO_MANS_LAND:
                    case PARSE_DIRECTIVE_INNER:
                        $current_level_data[] = '","';
                        break;
                    default:
                        $current_level_params[] = $current_level_data;
                        $current_level_data = array();
                        break;
                }
                break;

            default:
                $literal = php_addslashes(str_replace(array('\,', '\}', '\{'), array(',', '}', '{'), $next_token));
                if ($GLOBALS['XSS_DETECT']) {
                    ocp_mark_as_escaped($literal);
                }

                $current_level_data[] = '"' . $literal . '"';
                break;
        }
    }
    if ((!array_key_exists('LAX_COMCODE', $GLOBALS)) || ($GLOBALS['LAX_COMCODE'] === false)) {
        if ($stack !== array()) {
            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")))));
            }
        }
    }

    if ($current_level_data === array('')) {
        $current_level_data = array('""');
    }

    // Some optimisations
    $merged = array();
    $just_done_string = false;
    foreach ($current_level_data as $c) {
        // Try and replace some unnecessary string appending which may have happened when experiencing possible (but not) control characters
        $c = preg_replace('#([^\\\\])' . preg_quote('"."', '#') . '#', '$1', $c);

        // Try and merge some strings that don't need to be in separate seq_parts
        $c_stripped_down = str_replace(array('\\\\', '\\"'), array('', ''), $c); // Remove literal slashes and literal quotes so we can do an accurate scan to ensure it is all one string
        if ($c_stripped_down[0] === '"' && strpos($c_stripped_down, '"', 1) === strlen($c_stripped_down) - 1) {
            if ($just_done_string) {
                $pi = count($merged) - 1;
                $merged[$pi] = substr($merged[$pi], 0, strlen($merged[$pi]) - 1) . substr($c, 1, strlen($c) - 1);
            } else {
                $merged[] = $c;
            }
            $just_done_string = true;
        } else {
            $just_done_string = false;
            $merged[] = $c;
        }
    }
    $current_level_data = $merged;

    $STUCK_ABORT_SIGNAL = $sas_bak;

    return array($current_level_data, $preprocessable_bits);
}

/**
 * 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  PATH $path The path to the template file
 * @param  ID_TEXT $codename The codename of the template (e.g. foo)
 * @param  ID_TEXT $_codename The actual codename to use for the template (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)
 * @param  ?ID_TEXT $theme_orig The theme to cache in (null: main theme)
 * @return Tempcode The compiled Tempcode
 *
 * @ignore
 */
function _do_template($theme, $path, $codename, $_codename, $lang, $suffix, $theme_orig = null)
{
    if (is_null($theme_orig)) {
        $theme_orig = $theme;
    }

    $base_dir = get_custom_file_base() . '/themes/';
    if (!is_file($base_dir . $theme . $path . $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();
    }

    //$final_css_path = null;

    if (isset($FILE_ARRAY)) {
        $template_contents = unixify_line_format(file_array_get('themes/' . $theme . $path . $codename . $suffix));
    } else {
        $_path = $base_dir . filter_naughty($theme . $path . $codename) . $suffix;
        $template_contents = unixify_line_format(cms_file_get_contents_safe($_path));
    }

    // LESS support
    if ((addon_installed('less')) && ($suffix == '.less')) {
        // Up our resources
        if (php_function_allowed('set_time_limit')) {
            @set_time_limit(300);
        }
        disable_php_memory_limit();

        // Stop parallel compilation of the same file by a little hack; without this it could knock out a server
        /*
        $final_css_path = get_custom_file_base() . '/themes/' . $theme . '/templates_cached/' . $lang . '/' . $codename . '.css'; Actually this is architectually messy, just let it happen - it's not as slow as it was
        if ((is_file($final_css_path)) && (file_get_contents($final_css_path) == 'GENERATING')) {
            header('Content-type: text/plain; charset=' . get_charset());
            exit('We are doing a code update. Please refresh in around 2 minutes.');
        }
        require_code('files');
        cms_file_put_contents_safe($final_css_path, 'GENERATING', FILE_WRITE_FIX_PERMISSIONS);
        */

        if (!empty($SITE_INFO['nodejs_binary_path'])) {
            $less_path = get_custom_file_base() . '/node_modules/less/bin/lessc';

            if (!file_exists($less_path)) {
                fatal_exit('Unable to find the less NPM module. Please `cd` to your Composr directory and run `npm install less` to install it.');
            }

            $cmd = sprintf('%s %s --no-color %s', $SITE_INFO['nodejs_binary_path'], escapeshellarg($less_path), escapeshellarg($_path));
            $descriptorspec = array(
                0 => array('pipe', 'r'), // stdin
                1 => array('pipe', 'w'), // stdout
                2 => array('pipe', 'w'), // stderr
            );
            $pipes = array();
            $process = proc_open($cmd, $descriptorspec, $pipes);

            if ($process === false) {
                fatal_exit('Unable to execute the Node.js binary, please make sure it exists and proper permissions are set.');
            }

            $stdout = stream_get_contents($pipes[1]);
            fclose($pipes[1]);

            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[2]);

            $return_code = proc_close($process);

            if ($return_code !== 0) {
                fatal_exit('.less problem: ' . $stderr);
            }

            $template_contents = $stdout;
        } else {
            // Heavy-weight, newer (iLess)
            require_code('ILess/Autoloader');
            ILess_Autoloader::register();
            $less = new ILess_Parser(array(
                'import_dirs' => array(dirname($_path)),
            ), new ILess_Cache_FileSystem(array(
                'cache_dir' => get_custom_file_base() . '/themes/' . $theme . '/templates_cached/' . $lang,
            )));
            try {
                $less->parseString($template_contents);
                $template_contents = $less->getCSS();
            } catch (Exception $ex) {
                fatal_exit('.less problem: ' . $ex->getMessage());
            }
        }
    }

    if (($GLOBALS['SEMI_DEV_MODE']) && ($suffix === '.js') && (strpos($template_contents, '.innerHTML') !== false) && (!running_script('install')) && (strpos($template_contents, 'Parser hint: .innerHTML okay') === false)) {
        attach_message($codename . ': Do not use the .innerHTML property in your JavaScript because it will not work in true XHTML (when the browsers real XML parser is in action). Use Composr\'s global set_inner_html/get_inner_html functions.', 'warn');
    }

    // 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);
    }

    if ($IS_TEMPLATE_PREVIEW_OP_CACHE) {
        $test = post_param_string($codename, null);
        if (!is_null($test)) {
            $template_contents = post_param_string($test . '_new');
        }
    }

    cms_profile_start_for('_do_template');
    $result = template_to_tempcode($template_contents, 0, false, $codename, $theme_orig, $lang);
    cms_profile_end_for('_do_template', $codename . $suffix);
    if (($CACHE_TEMPLATES) && (!$IS_TEMPLATE_PREVIEW_OP_CACHE)) {
        $path2 = get_custom_file_base() . '/themes/' . $theme_orig . '/templates_cached/' . filter_naughty($lang);
        $_path2 = $path2 . '/' . filter_naughty($_codename) . $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 (e.g. foo)
 * @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
 * @return mixed The converted/compiled template as Tempcode, OR if a directive, encoded directive information
 */
function template_to_tempcode($text, $symbol_pos = 0, $inside_directive = false, $codename = '', $theme = null, $lang = null, $tolerate_errors = false)
{
    if (is_null($theme)) {
        $theme = isset($GLOBALS['FORUM_DRIVER']) ? $GLOBALS['FORUM_DRIVER']->get_theme() : 'default';
    }
    if (is_null($lang)) {
        $lang = user_lang();
    }

    list($parts, $preprocessable_bits) = compile_template(substr($text, $symbol_pos), $codename, $theme, $lang, $tolerate_errors);

    if (count($parts) === 0) {
        return new Tempcode();
    }

    $output_streaming = (function_exists('get_option')) && (get_option('output_streaming') === '1');

    $parts_groups = array();
    $parts_group = array();
    foreach ($parts as $part) {
        if (($output_streaming) && (strpos($part, '$bound_') !== false)) { // Start a new seq_part, so output streaming can break at this parameter reference
            if ($parts_group !== array()) {
                $parts_groups[] = $parts_group;
            }
            $parts_group = array();
        }
        $parts_group[] = $part;
    }
    if ($parts_group !== array()) {
        $parts_groups[] = $parts_group;
    }

    $funcdefs = array();
    $seq_parts = array();
    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[] = array(array($myfunc, array(/* Is currently unbound */), TC_KNOWN, '', ''));
    }

    $ret = new Tempcode(array($funcdefs, $seq_parts)); // Parameters will be bound in later.
    $ret->preprocessable_bits = array_merge($ret->preprocessable_bits, $preprocessable_bits);
    $ret->codename = $codename;
    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($myfunc, $parts)
{
    if ($parts === array()) {
        $parts = array('""');
    }
    $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']) {
        $unset_code = '';
        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;
}
