Internet radio browser GUI for music/video streams from various directory services.

⌈⌋ ⎇ branch:  streamtuner2


fossil-json-plugin-repo.php at [379c7b875f]

File dev/fossil-json-plugin-repo.php artifact 80abd12636 part of check-in 379c7b875f


<?php
/**
 * api: php
 * type: handler
 * title: Common.json repo list
 * description: Create json dict of selected fossil repository contents
 * version: 0.1
 * depends: fossil:json
 * doc: http://fossil.include-once.org/streamtuner2/wiki?name=plugin+meta+data
 *      https://pypi.python.org/pypi/pluginconf/
 *
 * Generates a „common-repo“-json list for files from a fossil repository.
 *
 *  · Used for streamtuner2 plugin manager - to refresh module list directly
 *    from repositories.
 *
 *  · Extracts key:value comments as seen above.
 *
 *  · Accepts PATH_INFO specifiers using glob patterns `/reponame/src*dir/*.ext`.
 *
 *  · You'll probably want to hook it beside the actual fossil server, using
 *    using a RewriteRule or ScriptAliasMatch.
 *    e.g.
 *        RewriteRule   ^(/?)repo.json/(.+)$ $1plugins.php/$2
 *
 *  · Each entry carries a faux $type, $dist and $file references, and all
 *    extracted meta fields, no docs. (= The crudest implementation so far.)
 *
 */


// run
$p = opts() and gen($p);



/**
 * Request params
 * [WHITELIST]
 *
 *  · assert alphanumeric repository.fossil name
 *  · limit allowed glob specifiers and file paths
 *
 */
function opts() {
    preg_match(
        "~^
           /(?<repo>[\w-]+)            # fossil basename
           /(?<glob>
              (?:
               [/\w.-]+                # basedir prefix
               [*]?                    
              ){0,3}                   # up to 3 *-glob segments
            )
        $~x",
        $_SERVER["PATH_INFO"], $groups
    );
    return $groups;
}


/**
 * Handler
 * [MAIN]
 *
 *  · invoked with $repo="projectname" and $glob="lib/*.src"
 *  · scans files, converts into repo.json, and outputs that
 *
 */
function gen($kwargs) {
    extract($kwargs);
    header("Content-Type: json/common-repo; x-ver=0");
    exit(
        json_encode(
            common_repo(
                query_files("/fossil.d/$repo.fossil", $glob),
                $repo
            ),
            JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT
        )
    );
}



/**
 * Read glob-specified files from repository
 * [EXEC]
 *
 *  · scans fossil repos for given filespec, e.g. "lib/plugins/*.py"
 *  · returns just first 2K from content
 *
 */
function query_files($R, $glob) {
   $r = [];
   if (!file_exists($R)) {
       return $r;
   }

   // loop through files
   $glob = escapeshellarg(preg_replace("~[^/\w.*-]~", "", $glob));
   $sql = escapeshellarg("
      SELECT  name AS name,  uuid,   SUBSTR(HEX(CONTENT(uuid)),1,2048) AS content
         FROM (SELECT  filename.name, bf.uuid, filename.fnid 
               FROM filename  JOIN mlink ON mlink.fnid=filename.fnid JOIN blob bf ON bf.rid=mlink.fid
                    JOIN event ON event.objid=mlink.mid
               WHERE (mlink.fnid NOT IN (SELECT fnid FROM mlink WHERE fid=0))
                     AND filename.name GLOB $glob
               GROUP BY filename.name
               ORDER BY event.mtime DESC
              )
   ");

   // Just retrieve as CSV list from fossil directly,
   // instead of using PDO handle and `fossil artifact` on each UUID
   $pipe = popen("fossil sqlite -R $R \".mode csv\" $sql", "rb");
   while ($row = fgetcsv($pipe, 0, ",", '"', '"')) {

      // skip emtpy rows
      if (count($row) != 3) {
          continue;
      }
      // add file
      $r[$row[0]] = hex2bin($row[2]);
   }

   return $r;
}


/**
 * Convert attributes into list
 * [TRANSFORM]
 *
 *  · from $fn=>$meta dict to [$pkg, $pkg] list
 *
 */
function common_repo($files, $R) {

    // extend each PMD key:value list
    $repo = meta_extract($files);
    foreach ($repo as $fn => $meta) {
    
        // basename, extension
        $id = strtok(basename($fn), ".");
        $ext = pathinfo($fn, PATHINFO_EXTENSION);
        $dir = basename(dirname($fn));
        
        // add some stub fields
        $meta += [
            "type" => "unknown",
            "api" => "$R",
            "title" => null,
            "description" => null,
            "version" => null,
        ];
        
        // common repo fields carry a `$` sigil
        $repo[$fn] = array_merge(
            [
                "\$name" => $id,                         # package basename
                "\$type" => "x-$ext",                    # e.g. "deb", "rpm" or "x-src"
                "\$dist" => "app/$R/$dir",               # e.g. "trusty/main" or "app:pkg:part"
                "\$file" => "http://fossil.include-once.org/$R/cat/$fn",   # resource locator
            ],
            $meta
        );
    }
    return array_values($repo);
}


/**
 * Extract plugin meta data
 * [REGEX]
 *
 *  · really just looks for scalar key:value lines
 *  · comment/docs are not extracted
 *
 */
function meta_extract($files) {
    foreach ($files as $fn=>$cnt) {
        preg_match_all("~\s*(?:#|//|[*])\s*(\w+):\h*(.+)~m", $cnt, $fields);
        $meta = array_combine($fields[1], $fields[2]);
        $meta = array_change_key_case($meta);
        $files[$fn] = $meta;
    }
    return $files;
}