ExecutePHP

Da lwiki.

English


La cosa è nata da questa [domanda su Stack Overflow].

L'utilità di un "esecutore custom per PHP" sta nel fatto che rende possibile eseguire codice con maggior flessibilità rispetto a una libreria. Anziché effettuare override di classi, posso modificarne il codice. Posso individuare (o provarci) operazioni che non desidero siano effettuate, o modificarne gli effetti.

Nel caso di pacchetti "precotti" che inviino dati in uscita, posso trasformarli in funzioni; e modificarne il codice in modo tale che l'HTML in uscita sia coerente con un layout mio.

Molto di questo posso già farlo usando un *esecutore interno*:

   - creo un "sub-virtualhost" nascosto che accetta connessioni solo dall'IP del server Web
   - utilizzo un qualunque pacchetto cURL WebClient per simulare un browser e fare le richieste
   - ottengo direttamente l'HTML in uscita.

Questo approccio "black box" però è appunto "black box", non posso mettere le mani dentro alla scatola.

Un altro interessante punto di un esecutore "attivo" è che il codice *non deve arrivare necessariamente dal file system*, posso per esempio eseguire codice PHP prelevato da un database, da file compressi, da archivi criptati, e così via.

I problemi da risolvere per una soluzione completa sono molteplici:

  • il codice può essere incluso in vari modi logici, e moltissimi modi semantici (per es. require 'file.php', ma anche require (PATH_CONSTANT . 'file.php') ). Una preg_replace può bastare per riconoscerli, o anche no.
  • il codice incluso può includere altro codice
  • una parola: "scoping".
  • una parola: "namespace".
  • output buffering.
  • code obfuscation.

Per quanto riguarda l'inclusione, se non altro require e compagnia sono direttive e non funzioni, quindi non si può fare

    $fn = 'require';
    $fn($myFile);

TODO: the code below does not work ;-)

    /**
     * Parse a single token.
     *
     * @param integer $token        Token
     * @param string $text          Text associated with token
     * @param string $status        Parser status
     */
 
    private static function executeToken($token, $text, $parent, $namespace, &$once, &$status) {
        if (T_INLINE_HTML == $token) {
            return "print base64_decode('" . base64_encode($text) . "');\n";
        }
        $xlat   = [
            T_FILE      => null,    // __FILE__, sistemato più sotto.
            T_COMMENT   => "\n",
            T_OPEN_TAG  => '',
            T_CLOSE_TAG => '',
        ];
        if (array_key_exists($token, $xlat)) {
            return T_FILE === $token ? "'{$parent}'" : $xlat[$token];
        }
        $boaClose   = ", '{$parent}','{$namespace}'" . ( $once ? ', true)' : ')' ) . ";\n";
        $boaRunner  = '\\Basic\\BoaFile::executePHP';
        $stage = [
            -1              => [ 'O' => [ '(' => [ 'status' => 'C'            ] ],
                                 'C' => [ ';' => [ 'text'   => $boaClose, 'status' => '' ],
                                          ')' => [ 'text'   => $boaClose, 'status' => 'W' ] ],
                                 'W' => [ ';' => [ 'text'   => '', 'status' => '', ],
                                          '*' => [ 'status' => '' ],
                                 ],
                             ],
            T_WHITESPACE    => false,
            T_INCLUDE_ONCE  => [ '*' => [ '*' => [ 'text'   => $boaRunner, 'status' => 'O', 'once'   => true, ] ] ],
            T_REQUIRE_ONCE  => [ '*' => [ '*' => [ 'text'   => $boaRunner, 'status' => 'O', 'once'   => true, ] ] ],
            T_INCLUDE       => [ '*' => [ '*' => [ 'text'   => $boaRunner, 'status' => 'O'   ] ] ],
            T_REQUIRE       => [ '*' => [ '*' => [ 'text'   => $boaRunner, 'status' => 'O'   ] ] ],
            '*'             => [ 'O' => [ '*' => [ 'text'   => '(' . $text,'status' => 'C'   ] ] ],
        ];
        foreach ([ $token, $status, $text ] as $key) {
            $stage  = array_key_exists($key, $stage)
                    ? $stage[$key]
                    : ( array_key_exists('*', $stage)
                        ? $stage['*']
                        : false );
            if (false === $stage) {
                return $text;
            }
        }
        if (array_key_exists('status', $stage)) {
            $status = $stage['status'];
        }
        if (array_key_exists('once', $stage)) {
            $once = $stage['once'];
        }
        if (array_key_exists('text', $stage)) {
            return $stage['text'];
        }
        return $text;
    }
 
    /**
     * Converts a path into a reliable path.
     * @param string $path
     * @param string $parent
     * @param boolean $once
     * @return boolean|string
     */
    private static function tokenPath($path, $parent, $once) {
        if ('/' !== substr($path, 0, 1)) {
            if ('.' == dirname($path)) {
                $path = dirname($parent) . DIRECTORY_SEPARATOR . $path;
            } else {
                $asked  = $path;
                try {
                    $path   = Config::instance()->localpath($asked, false);
                } catch (ResourceError $e) {
                    $e->append([
                        'requested'     => $path,
                        'included-by'   => $parent,
                    ]);
                }
            }
        }
        if (in_array($path, static::$executedByBoa)) {
            if ($once) {
                return false;
            }
        } else {
            static::$executedByBoa[] = $path;
        }
        return $path;
    }
 
    /**
     * Rewrite a PHP file for inclusion.
     */
    private static function phpRewrite($path, $ns) {
        if (1 == $ns) {
            Config::dd("Blast");
        }
        $tokens = token_get_all(file_get_contents($path));
        $once   = false;
        $source = "\nnamespace {$ns};\nuse Basic\\BoaFile; /* {$path} */\n\n";
        $status = '';
        foreach ($tokens as $raw) {
            if (is_string($raw)) {
                $raw = [ -1, $raw ];
            }
            // $once and $status MAY BE MODIFIED by executeToken
            $source .= self::executeToken($raw[0], $raw[1], $path, $ns, $once, $status);
        }
        return $source;
    }
 
    /**
     * Execute a PHP file replacing some functions with appropriate wrappers.
     *
     * @param string $path      filename
     * @param boolean $once     whether to run this once, or not.
     * @return string
     */
 
    public static function executePHP($boaPath, $boaParent, $boaNameSpace, $once = false, $env = [ ]) {
        if (0 === static::$evaluating) {
            ob_start();
            // print "<pre>[BEGIN]\n";
            static::$globalVariables    = $env;
            static::$globalVariables['_SERVER'] = Config::instance('response')->server();
        }
        // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . "boa-execute {$boaPath}\n";
        if (false === ($boaPath   = self::tokenPath($boaPath, $boaParent, $once))) {
            // print "Already included, exiting\n";
            return '';
        }
        // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . "loading {$boaPath}\n";
        $boaSource  = self::phpRewrite($boaPath, $boaNameSpace);
        static::$evaluating++;
        try {
            global  $_SERVER;
            extract(static::$globalVariables);
            $_SERVER['SCRIPT_FILENAME'] = $boaPath;
            // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . "Evaluating {$boaPath}\n";
            // preg_match_all('#BoaFile::.*$#', $boaSource, $gregs);
            // foreach ($gregs[0] as $row) {
                // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . " >>> {$row}";
            // }
            $fp = fopen("cache/boa.src", "w");
            fwrite($fp, "\n\n===\n{$boaPath}\n===\n\n");
            fwrite($fp, $boaSource);
            fwrite($fp, "\n\n");
            foreach (get_declared_classes() as $c) {
                fwrite($fp, "'{$c}'\n");
            }
            fclose($fp);
 
            // unset($fp, $boaPath, $boaNameSpace);
            eval($boaSource);
            // FROM HERE ON, YOU CANNOT TRUST YOUR OWN VARIABLES.
 
            // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . "Evaluated {$boaPath}\n";
            static::$globalVariables = array_diff_key(get_defined_vars(), [ 'boaPath' => true, 'boaSource' => true ]);
        } catch (\Exception $e) {
            $count  = 0;
            Config::dd([
                $e->getMessage(),
                implode(
                    "\n",
                    array_map(
                        function($row) use (&$count) {
                            return sprintf("%5d ", ++$count) . $row;
                        },
                        explode("\n", $boaSource)
                    )
                )
            ]);
        }
        // print str_repeat("&nbsp;&nbsp;&nbsp;", static::$evaluating) . "$boaPath executed, returning\n";
        $ret    = '';
        static::$evaluating--;
        if (0 === static::$evaluating) {
            unset($_SERVER, $_GET, $_POST, $_REQUEST);
            // print "FINAL RUN<br />";
            $ret = '';
            while (ob_get_level()) {
                $ret    .= ob_get_clean();
            }
        }
        return $ret;
    }