Skip to content
Snippets Groups Projects
Evaluate.php 9.16 KiB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/12/16
 * Time: 4:36 PM
 */

namespace qfq;

use qfq;

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/database/Database.php');
Carsten  Rose's avatar
Carsten Rose committed
/**
 * Class Evaluate
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
class Evaluate {
Carsten  Rose's avatar
Carsten Rose committed
    private $store = null;
Carsten  Rose's avatar
Carsten Rose committed
    private $db = null;
Carsten  Rose's avatar
Carsten Rose committed
    private $startDelimiter = '';
    private $startDelimiterLength = 0;
    private $endDelimiter = '';
    private $endDelimiterLength = 0;
    private $sqlKeywords = array('SELECT ', 'INSERT ', 'DELETE ', 'UPDATE ', 'SHOW ', 'REPLACE ');
    private $escapeTypeDefault = '';

//    private $debugStack = array();
    /**
     * @param \qfq\Store $store
Carsten  Rose's avatar
Carsten Rose committed
     * @param Database $db
     * @param string $startDelimiter
     * @param string $endDelimiter
Carsten  Rose's avatar
Carsten Rose committed
    public function __construct(Store $store, Database $db, $startDelimiter = '{{', $endDelimiter = '}}') {
        $this->store = $store;
        $this->db = $db;
        $this->startDelimiter = $startDelimiter;
        $this->startDelimiterLength = strlen($startDelimiter);
        $this->endDelimiter = $endDelimiter;
        $this->endDelimiterLength = strlen($endDelimiter);
        $this->escapeTypeDefault = $this->store->getVar(F_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        if (empty($this->escapeTypeDefault) || $this->escapeTypeDefault == TOKEN_ESCAPE_CONFIG) {
            $this->escapeTypeDefault = $this->store->getVar(SYSTEM_ESCAPE_TYPE_DEFAULT, STORE_SYSTEM);
        }
     * Evaluate a whole array or an array of arrays.
Carsten  Rose's avatar
Carsten Rose committed
     * @param       $tokenArray
     * @param array $skip Optional Array with keynames, which will not be evaluated.
     * @param array $debugStack
    public function parseArray($tokenArray, array $skip = array(), &$debugStack = array()) {
        $arr = array();

        foreach ($tokenArray as $key => $value) {
                $arr[$key] = $value;
                $value = Support::handleEscapeSpaceComment($value);
                $arr[$key] = $this->parse($value, 0, $debugStack);
Carsten  Rose's avatar
Carsten Rose committed
     * Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them. All queries will be
     * fired. In case of an 'INSERT' statement, return the last_insert_id().
     *
     * Token to replace have to be enclosed by '{{' and '}}'
Carsten  Rose's avatar
Carsten Rose committed
     * @param     $line
Carsten  Rose's avatar
Carsten Rose committed
     */
    public function parse($line, $recursion = 0, &$debugStack = array(), &$foundInStore = '') {
        $flagTokenReplaced = false;
Carsten  Rose's avatar
Carsten Rose committed

        if ($recursion > 4) {
            throw new qfq\UserFormException("Recursion too deep ($recursion). Line: $line", ERROR_RECURSION_TOO_DEEP);
        $result = $line;
        $debugIndent = str_repeat(' ', $recursion);
        $debugLocal[] = $debugIndent . "PARSE: $result";
        $posFirstClose = strpos($result, $this->endDelimiter);
//        $this->store->setVar(SYSTEM_SQL_RAW, $line, STORE_SYSTEM);
        while ($posFirstClose !== false) {

            $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter);
Carsten  Rose's avatar
Carsten Rose committed
            if ($posMatchOpen === false) {
                throw new \qfq\UserFormException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
            $pre = substr($result, 0, $posMatchOpen);
            $post = substr($result, $posFirstClose + $this->endDelimiterLength);
            $match = substr($result, $posMatchOpen + $this->startDelimiterLength, $posFirstClose - $posMatchOpen - $this->startDelimiterLength);
            $evaluated = $this->substitute($match, $foundInStore);
            // newline
            $debugLocal[] = '';

            $debugLocal[] = $debugIndent . "REPLACE: $match";

                // Encode the non replaceable part as preparation not to process again. Recode them at the end.
                $evaluated = Support::encryptDoubleCurlyBraces($this->startDelimiter . $match . $this->endDelimiter);
                $debugLocal[] = $debugIndent . "BY: <nothing found - not replaced>";
                // If an array is returned, break everything and return this assoc array.
                if (is_array($evaluated)) {
                    $result = $evaluated;
                    $debugLocal[] = $debugIndent . "BY: array(" . count($result) . ")";
                $debugLocal[] = $debugIndent . "BY: $evaluated";
                // More to substitute in the new evaluated result? Start recursion just with the new result..
                if (strpos($evaluated, $this->endDelimiter) !== false) {
                    $evaluated = $this->parse($evaluated, $recursion + 1, $debugLocal, $foundInStore);
                }
            }
            $result = $pre . $evaluated . $post;
            $posFirstClose = strpos($result, $this->endDelimiter);
        }
        $result = Support::decryptDoubleCurlyBraces($result);

        if ($flagTokenReplaced === true) {
            $debugLocal[] = $debugIndent . "FINAL: " . is_array($result) ? "array(" . count($result) . ")" : "$result";

            $debugStack = $debugLocal;
Carsten  Rose's avatar
Carsten Rose committed
    }

    /**
     * Tries to substitute $token.
     *   a) a SQL statement to fire
     *   b) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:alnumx', 'form:F:all:s','form:F:all:s:default'
     * The token have to be _without_ Delimiter '{{' , '}}'
     * If neither a) or b) match, return the token itself.
Carsten  Rose's avatar
Carsten Rose committed
     *
Carsten  Rose's avatar
Carsten Rose committed
     * @param string $foundInStore Returns the name of the store where $key has been found. If $key is not found,
     *                             return ''.
     *
     * @throws CodeException
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
     */
    public function substitute($token, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
        $sqlMode = ROW_IMPLODE_ALL;

        $token = trim($token);

        if ($token[0] === '!') {
Carsten  Rose's avatar
Carsten Rose committed
            $sqlMode = ROW_REGULAR;
        }

        // just to extract the first token: check if this is a SQL Statement
        $arr = explode(' ', $token, 2);

Carsten  Rose's avatar
Carsten Rose committed
        // SQL Statement?
        if (in_array(strtoupper($arr[0] . ' '), $this->sqlKeywords)) {
Carsten  Rose's avatar
Carsten Rose committed
            return $this->db->sql($token, $sqlMode);
        }

        // explode for: <key>:<store priority>:<sanitize class>:<escape>:<default>
        $arr = explode(':', $token, 5);
        $arr = array_merge($arr, [null, null, null, null, null]); // fake isset()

        $escapeTypes = (empty($arr[3])) ? $this->escapeTypeDefault : $arr[3];
Carsten  Rose's avatar
Carsten Rose committed

        // search for value in stores
        $value = $this->store->getVar($arr[0], $arr[1], $arr[2], $foundInStore);
            // Process all escape requests in the given order.
            for ($ii = 0; $ii < strlen($escapeTypes); $ii++) {
                $escape = $escapeTypes[$ii];
                if ($escape == TOKEN_ESCAPE_CONFIG) {
                    $escape = $this->escapeTypeDefault;
                }
                switch ($escape) {
                    case TOKEN_ESCAPE_SINGLE_TICK:
                        $value = str_replace("'", "\\'", $value);
                        break;
                    case TOKEN_ESCAPE_DOUBLE_TICK:
                        $value = str_replace('"', '\\"', $value);
                        break;
                    case TOKEN_ESCAPE_LDAP_FILTER:
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_FILTER);
                        break;
                    case TOKEN_ESCAPE_LDAP_DN:
                        $value = Support::ldap_escape($value, null, LDAP_ESCAPE_DN);
                        break;
                    case TOKEN_ESCAPE_MYSQL:
                        $value = $this->db->realEscapeString($value);
                        break;
                    case TOKEN_ESCAPE_NONE: // do nothing
                        break;
                        throw new UserFormException("Unknown Escape qualifier: $escape", UNKNOWN_TYPE);
        // Not found and a default is given: take the default.
        if ($foundInStore == '' && !empty($arr[4])) {
            $foundInStore = TOKEN_FOUND_AS_DEFAULT;
            $value = str_replace('\\:', ':', $arr[4]);
        }
Carsten  Rose's avatar
Carsten Rose committed
    }
    /**
     * @return string
     */
//    public function getDebug() {
//        return '<pre>' . implode("\n", $this->debugStack) . '</pre>';
//    }