<?php
# encoding: utf-8
# api: php
# type: functions
# category: database
# title: Fossil utility code
# description: database and EXTCGI helper code
# version: 0.1
# state: alpha
# config: -
#
# Combines database and IO utility code for extroot/ scripts.
#
#  · db("SELECT * FROM config WHERE name=?", ["col"])
#  · get_config("project-description", "…")
#  · h("html context output")
#  · is_admin()
#  · config(meta($fn)) - PMD
#



#-- init
ini_set("display_errors", !empty($_REQUEST["dbg"]));
$_SERVER["FOSSIL_SELF"] = "https://$_SERVER[SERVER_NAME]$_SERVER[FOSSIL_URI]/";


/**
 * Database query shorthand. (Using active fossil repository.)
 *
 * @param  string  $sql      Query with placeholders
 * @param  array   $params   Bound parameters
 * @param  bool    $fetch    Immediate ->fetchAll()
 * @return array|PDOStatement|PDO
 */
function db($sql="", $params=[], $fetch=TRUE) {
    static $db;
    if (empty($db)) {
        if (!preg_match("~^/\w[/\w.-]+\w\.(fs?l?|fossil|sqlite)$~", $_SERVER["FOSSIL_REPOSITORY"])) {
            die("db(): FOSSIL_REPOSITORY doesn't look right. Abort.");
        } 
        $db = new PDO("sqlite::memory:");
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
        $db->query("ATTACH DATABASE '$_SERVER[FOSSIL_REPOSITORY]' AS 'repo'");
    }
    if ($params) {
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        return $fetch ? $stmt->fetchAll(PDO::FETCH_ASSOC) : $stmt;
    }
    elseif ($sql) {
        return $db->query($sql)->fetchAll(PDO::FETCH_ASSOC);
    }
    else {
        return $db;
    }
}

/**
 * Query fossil `config` table.
 *
 * @param  string  $name     Option
 * @param  array   $default  Fallback
 * @return string
 */
function get_config($name, $default) {
    $r = db("SELECT value FROM config WHERE name=?", [$name]);
    return $r ? $r[0]["value"] : $default;
}

/**
 * HTML escape shorthand (for interpolation {$h(expr)} in strings).
 *
 * @param  string  $s        Raw string
 * @return string
 */
function h($s) {
    return htmlspecialchars($s, ENT_QUOTES, "utf-8");
}
$h = "h";


/**
 * Test if active user had admin "s" capabilities.
 *
 * @return bool
 */
function is_admin() {
    return strpos($_SERVER["FOSSIL_CAPABILITIES"], "s") !== false;
}




/**
 * Extract PMD (plugin meta data) from file.
 * Multilang, pluginconf-derived, but still fairly crude.
 *
 * @param  string  $fn       Source file
 * @return array
 */
function meta($fn) {
    $src = file_get_contents($fn, false, NULL, 0, 4096);
    $src = preg_replace("~^#!.+|\R~", "\n", $src);   # clean CRLF / shebang
    preg_match_all("~(^\h{0,4}(#|//|/?\*).*\n)+~m", $src, $uu);  # get comments (⚠ not consecutive)
    $src = preg_replace("~^\h{0,4}(#|//|/?\*)\h{0,3}\r*~m", "", implode("", $uu[0]));  # strip any #// prefix
    preg_match_all("~^([\w-]+):(.*$\\n(?:(?![\w-]+:).+$\\n)*)~m", $src, $uu);  # extract fields (multiline)
    $r = array_combine($uu[1], $uu[2]);
    array_change_key_case($r); # ⚠ still unmapped hyphens
    if (count($doc = preg_split("~\R\h*\R~", trim($src), 2)) == 2) { ;  # comment block
        $r["__doc__"] = $doc[1];
    }
    return $r;
}

/**
 * PMD config: list extraction.
 *
 * @param  array $meta       From meta()
 * @return array
 */
function config($meta, $r=[]) {
    if (empty($meta["config"])) {
        return [];
    }
    preg_match_all("~\{ (.+?) \} | \< (.+?) \>~x", $meta["config"], $def);  # iterate over each {…} block
    foreach (array_merge($def[1], $def[2]) as $row) if ($row) {
        preg_match_all("~ [\"':$]?(\w+)[\"']?   \s*[:=]+\s*  (?: \"([^\"]*)\" | '([^']*)' | ([^,]*) )~x", $row, $kv, PREG_SET_ORDER);
        $opt = [];  # visit each key:value pair
        foreach ($kv as $f) {
            $f = array_values(array_filter($f, "strlen"));
            $opt[$f[1]] = isset($f[2]) ? $f[2] : "";
        }
        $r[] = $opt;
    }
    return $r;
}

