bitslip6/bitfire

View on GitHub
firewall/src/renderer.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * BitFire PHP based Firewall.
 * Author: BitFire (BitSlip6 company)
 * Distributed under the AGPL license: https://www.gnu.org/licenses/agpl-3.0.en.html
 * Please report issues to: https://github.com/bitslip6/bitfire/issues
 *
 * HTML/JS Recursive Renderer and minifier.
 * TODO: Refactor to use FileData and Effect abstractions.
 */
namespace ThreadFin\view;

use function ThreadFin\_t;

use const ThreadFin\DAY;

/**
 * take a unix timestamp and return localized string for number of days ago
 * @param string $epoch 
 * @return string 
 */
function days_format(string $epoch) : string {
    $t = intval($epoch);
    if ($t > 1) {
        $diff = time() - $t;
        $r = floor($diff / DAY);
        if ($r == 0) { return _t("today"); }
        if ($r > 0 && $r < 1024) { return  $r . _t(" days ago"); }
    }
    return _t("never");
}

function checked($value) : string {
    if ($value) { return " checked='checked' "; }
    return "X";
}


/**
 * @param string $text 
 * @return string returns the unmodified $text
 */
function identity(string $text) : string {
    return $text;
}

/**
 * escape user input for display on rendered page
 * @param string $text 
 * @param null|callable $next_fn 
 * @return string 
 */
function escape(string $text) : string {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
}

/**
 * count the number of elements in an array, or 0 for any other input type
 * @param mixed $input 
 * @return string 
 */
function counter($input) : string {
    if (is_array($input)) {
        return count($input);
    }
    return "0";
}



namespace ThreadFin;

use DOMDocument;
use DOMNode;
use DOMXPath;
use ThreadFin\Effect;
use ThreadFin\FileData;
use ThreadFin\FileMod;

use function ThreadFin\partial_right as BINDR;
use function ThreadFin\debug;
use function ThreadFin\icontains;
use BitFire\Config as CFG;

const NOT_MINIFY_EXTENSIONS = ["svg", "min"];
const MODIFIER_MAPPING = [
    "-" => "\ThreadFin\\view\\escape",
    "+" => "intval",
    "%d" => "\ThreadFin\\view\\days_format",
    "%u" => "strtoupper",
    "%U" => "ucfirst",
    "%W" => "ucwords",
    "%h" => "\ThreadFin\\view\\checked",
    "%c" => "ThreadFin\\view\\counter"
];
// CHAR FORMAT matches single character modifiers
const CHAR_FORMAT = "[+-]";
const MAX_VAR_REPLACEMENT = 1000;
const DO_MINIFY = true;


/**
 * extract innerHTML from a node
 * @param DOMNode $element 
 * @return string 
 */
function inner_html(DOMNode $element) : string {
    $innerHTML = "";
    $children  = $element->childNodes;

    foreach ($children as $child) {
        $innerHTML .= $element->ownerDocument->saveHTML($child);
    }

    return $innerHTML;
}


/**
 * pure function to minify HTML.
 * This will remove all newlines excess whitespace and comments.
 * @param string $in 
 * @return string 
 */
function minify_str(string $in) : string {
    //$t1 = preg_replace("/[\s\n]+/m", " ", $in);
    //$t2 = preg_replace("/>\s+</", "><", $t1);
    //$t3 = preg_replace("/<!--.*?-->/", "", $t2);
    $t3 = $in;
    if (empty($t3)) { return ""; }

    if (!class_exists("DOMDocument")) {
        return $in;
    }

    libxml_use_internal_errors(false);

    // load XML
    $doc = new DOMDocument();
    if (CFG::enabled("debug_file")) {
        $doc->preserveWhiteSpace = false;
        $doc->formatOutput = true;
    }
    $doc->loadHTML($t3, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOCDATA | LIBXML_NONET);
    foreach (libxml_get_errors() as $error) {
        debug("libxml: %s:%d [%s]", $error->file, $error->line, $error->message);
    }
    
    // find all content to translate
    libxml_clear_errors();
    $xpath = new DOMXpath($doc);
    $elms = $xpath->query("//*[contains(@class,'tdc')]");
    debug("translating %d tags\n", $elms->count());
    for ($i = 0; $i < $elms->count(); $i++) {
        $in = trim(inner_html($elms->item($i)));
        $translated = _t($in);
        $t3 = str_replace($in, $translated, $t3);
    }

    return $t3;
}

/**
 * add a new string modifier to the list of available view modifiers
 * if $modifier_fn is NULL returns the modifier already mapped to $modifier_name
 * @param null|string $modifier_name 
 * @param null|callable $modifier_fn 
 * @return array 
 */
function view_modifier(string $modifier_name, ?callable $modifier_fn = null) : ?Callable {
    static $mapping = MODIFIER_MAPPING;

    if ($modifier_fn === NULL) {
        $modifiers = explode("|", $modifier_name);

        $idx = 0;
        $fn = $mapping[$modifiers[0]]??NULL;
        while(isset($modifiers[++$idx])) {
            // echo "CHAIN $fn = {$modifiers[$idx]}\n";
            $fn = chain($fn, $mapping[$modifiers[$idx]]??NULL);
        }
        return $fn;
    }

    $mapping[$modifier_name] = $modifier_fn;
    return NULL;
}


/**
 * pure function to minify HTML files.
 * 
 * @param string $filename 
 * @return Effect 
 */
function minify(string $filename) : Effect {
    $effect  = Effect::new();

    $extension = pathinfo($filename, PATHINFO_EXTENSION);
    // just serve minified files directly
    if ($extension === "min") {
        $effect->out(
            FileData::new($filename)->raw()
        );
    }
    // if the file is minify-able
    else if (DO_MINIFY && ! icontains($extension, NOT_MINIFY_EXTENSIONS)) {
        $min_filename = "{$filename}.min";
        $min_file = FileData::new($min_filename);
        // if we have a minified version on disk, serve that

        if ($min_file->exists && filemtime($min_filename) > filemtime($filename)) {
            $effect->out($min_file->raw());
        }
        // read the raw file, minify it and return an effect to write the
        // minified file to disk
        else {
            //echo "<h1>$filename</h1>\n";
            $source_content = FileData::new($filename)->raw();
            $min_content = minify_str($source_content);
            $effect->out($min_content);
            $effect->file(new FileMod($min_filename, $min_content, \BitFire\FILE_RW));
        }
    }

    return $effect;
}


/**
 * pure function to render the file source with any replacements, src_root unused
 */
function render_file(string $src2, array $replacements = array()) : string {
    $min_effect = minify($src2);
    $content = $min_effect->read_out(true);
    $min_effect->run();
    $rendered = process_line($content, $replacements);
    return $rendered;
}



/**
 * the view variable replacement code.  
 * supports string, array and object access with the dot operator
 * @param array $x 
 * @param array $replacements 
 * @return string 
 */
function content_replacement(array $x, array $replacements) {
    $mod_fn = NULL;
    if (!empty($x[2])) {
        $mod_fn = view_modifier($x[2]);
    }
    $primary = $x[3];
    $secondary = $x[4]??"";

    // replace content.  apply any view modifiers
    if (isset($replacements[$primary])) {
        $return = $replacements[$primary];
        if (!empty($secondary)) {
            if (is_object($return)) {
                if (isset($return->$secondary)) {
                    $value = $return->$secondary;
                }
            }
            else if (is_array($return)) {
                if (isset($return[$secondary])) {
                    $value = $return[$secondary];
                }
            }
            else {
                $value = (string)$return;    
            }
        }
        else {
            $value = (string)$return;    
        }
        if (!isset($value)) {
            debug("VIEW VAR MISSING %s.%s", $primary, $secondary);
            $value = "";
        }
        return ($mod_fn == NULL) ? $value : $mod_fn($value);
    }

    debug("unset view variable [%s.%s]", $primary, $secondary);
    return "undefined [{$primary}.{$secondary}]";
}

function template_replacement_min(string $template_markup, string $template_var, string $var_name, array $replacements) : string {
    $content = "";
    foreach($replacements[$var_name] as $item) {
        $replacements[$template_var] = $item;
        $content .= process_line($template_markup, $replacements);
    }
    return $content;
}

/**
 * 
 */
function template_replacement(array $x, array $replacements, array $templates) {
    $template_name = $x[1];
    $var_name = $x[2];
    $content = "";
    $template_arr = $templates[$template_name]??[];
    $template_var = $template_arr[0]??$var_name;
    $template_markup = $template_arr[1]??"";
    if (isset($replacements[$var_name]) && is_array($replacements[$var_name]) && count($replacements[$var_name]) > 0) {
        foreach($replacements[$var_name] as $item) {
            $replacements[$template_var] = $item;
            $content .= process_line($template_markup, $replacements);
        }
    }
    return $content;
}


/**
 * replace all variables in a line with data from replacements, sub render any included content
 * and templates
 */
function process_line(string $line, array $replacements) : string {
    // internal templates
    $templates = [];

    //extract inline templates
    $line = preg_replace_callback(
        "/{{\s*template\s*:\s*(\w+)[\s\:]+[\'\"]?(\w+)[\'\"]?\s*}}\s*(.*?){{\s*end[\s\:]+template\s*}}/mis",
        function($x) use (&$templates) {
            $templates[$x[1]] = [$x[2], $x[3]];
            return "";
    }, $line);

    // replace variable substitution
    $line = preg_replace_callback("/{{\s*((\%[a-zA-Z]|".CHAR_FORMAT."|\|)*)*\s*([_\w-]+)\.?([_\w-]*)\s*}}/", 
    BINDR("ThreadFin\\content_replacement", $replacements), $line, MAX_VAR_REPLACEMENT);

    // replace inline templates
    $line = preg_replace_callback("/{{\s*render[\s:]+(\w+)\s+[\'\"]?(\w+)[\'\"]?\s*}}/",
    BINDR("ThreadFin\\template_replacement", $replacements, $templates), $line, MAX_VAR_REPLACEMENT);

    // include sub templates, params will be injected into view variables
    // {{> path/to/view param1="value" param2="value" }}
    $line = preg_replace_callback("/{{>\s*([^\s}]+)\s*([^}]*)}}/", 
        function ($x) use ($replacements) {
            preg_match_all("/([_\w]+)\s*=\s*\"([^\"]*)/", $x[2], $matches);
            $params = array();
            for ($i=0,$m=count($matches[1]);$i<$m;$i++) {
                $params[$matches[1][$i]] = $matches[2][$i];
            }

            $replacements = array_merge($replacements, $params);
            return render_file(VIEW_ROOT . DS . $x[1], $replacements);
         }, $line, MAX_VAR_REPLACEMENT);

    return $line;
}