#! /usr/bin/env php

<?php

require_once("html/inc/boinc_db.inc");
require_once("html/inc/util_basic.inc");

$apps = BoincApp::enum("");
$platforms = BoincPlatform::enum("");

$config = file_get_contents("config.xml");
$download_url = parse_element($config, "<download_url>");
$download_dir = parse_element($config, "<download_dir>");
$fanout = parse_element($config, "<uldl_dir_fanout>");

function lookup_app($name) {
    global $apps;
    foreach ($apps as $app) {
        if ($app->name == $name) return $app;
    }
    return null;
}

function lookup_platform($p) {
    global $platforms;
    foreach ($platforms as $platform) {
        if ($platform->name == $p) return $platform;
    }
    return null;
}

function readdir_aux($d) {
    while ($f = readdir($d)) {
        if ($f == ".") continue;
        if ($f == "..") continue;
        return $f;
    }
    return false;
}

// Data structures:
// Files are described by objects with fields
//  physical_name
//  logical_name
//  main_program
//  url
// etc.
// This are parsed from version.xml, or created by us
//
// Variables named $fd refer to such objects
//

// return a <file_info> element for the file
//
function file_info_xml($fd) {
    $xml =
        "<file_info>\n".
        "    <name>".$fd->physical_name."</name>\n"
    ;
    if (is_array($fd->url)) {
        foreach ($fd->url as $url) {
            $xml .= "    <url>$url</url>\n";
        }
    } else {
        $xml .= "    <url>$fd->url</url>\n";
    }
    if ($fd->executable || $fd->main_program) {
        $xml .= "    <executable/>\n";
    }
    $xml .= "    <file_signature>\n";
    $xml .= $fd->signature;
    $xml .=
        "    </file_signature>\n".
        "    <nbytes>".$fd->nbytes."</nbytes>\n".
        "</file_info>\n"
    ;
    return $xml;
}

// return a <file_ref> element for the file
//
function file_ref_xml($fd) {
    $xml =
        "<file_ref>\n".
        "    <file_name>".$fd->physical_name."</file_name>\n"
    ;
    if (isset($fd->logical_name) && strlen($fd->logical_name)) {
        $xml .= "    <open_name>$fd->logical_name</open_name>\n";
    }
    if ($fd->copy_file) {
        $xml .= "    <copy_file/>\n";
    }
    if ($fd->main_program) {
        $xml .= "    <main_program/>\n";
    }
    $xml .= "</file_ref>\n";
    return $xml;

}

function lookup_file($fds, $name) {
    foreach ($fds as $fd) {
        if ($fd->physical_name == $name) return $fd;
    }
    return null;
}

// update file in list, or add to list
//
function update_file($fds, $fd) {
    for ($i=0; $i<sizeof($fds); $i++) {
        if ($fds[$i]->physical_name == $fd->physical_name) {
            $fds[$i] = $fd;
            return $fds;
        }
    }
    $fds[] = $fd;
    return $fds;
}

// move file to download dir, check immutability, fill in $fd->url
//
function stage_file($a, $v, $p, $fd) {
    global $download_url, $download_dir;

    $name = $fd->physical_name;
    $path = "apps/$a/$v/$p/$name";
    $dl_path = "$download_dir/$name";
    if (is_file($dl_path)) {
        if (md5_file($path) != md5_file($dl_path)) {
            die ("Error: files $path and $dl_path differ.\nBOINC files are immutable.\nIf you change a file, you must give it a new name.\n");
        }
    } else {
        echo("cp $path $dl_path\n");
        system("cp $path $dl_path");
    }
    
    $fd->url = "$download_url/$name";
    return $fd;
}

function get_api_version($a, $v, $p, $fds) {
    foreach ($fds as $fd) {
        if ($fd->main_program) {
            $path = "apps/$a/$v/$p/$fd->physical_name";
            $handle = popen("strings $path | grep API_VERSION", "r");
            $x = fread($handle, 8192);
            pclose($handle);
            $x = strstr($x, "API_VERSION_");
            return trim(substr($x, strlen("API_VERSION_")));
        }
    }
    return "";
}

$sig_gen_confirmed = false;

function confirm_sig_gen($name) {
    global $sig_gen_confirmed;

    if ($sig_gen_confirmed) return true;

    echo "
    NOTICE: You have not provided a signature file for $name,
    and your project's code-signing private key is on your server.

    IF YOUR PROJECT IS PUBLICLY ACCESSABLE, THIS IS A SECURITY VULNERABILITY.
    PLEASE STOP YOUR PROJECT IMMEDIATELY AND READ:
    http://boinc.berkeley.edu/trac/wiki/CodeSigning

    Continue (y/n)? ";

    $x = trim(fgets(STDIN));
    if ($x != "y") {
        exit;
    }
    $sig_gen_confirmed = true;
}

// process a file
//
function process_file($a, $v, $p, $name, $fds) {
    $fd = lookup_file($fds, $name);
    if (!$fd) {
        $fd = null;
        $fd->physical_name = $name;
        $fd->url = array();
    }
    $path = "apps/$a/$v/$p/$name";

    $stat = stat($path);
    $fd->nbytes = $stat['size'];

    $sigpath = "apps/$a/$v/$p/$name.sig";
    if (is_file($sigpath)) {
        $fd->signature = file_get_contents($sigpath);
    } else {
        $keypath = "keys/code_sign_private";
        if (is_file($keypath)) {
            confirm_sig_gen($name);
            $handle = popen("bin/sign_executable $path $keypath", "r");
            $fd->signature = fread($handle, 8192);
            pclose($handle);
        } else {
            die("   Error: no .sig file for $name, and no code signing private key\n");
        }
    }

    if (!sizeof($fd->url)) {
        $fd = stage_file($a, $v, $p, $fd);
    }

    if (!isset($fd->executable)) {
        $perms = fileperms($path);
        $fd->executable = ($perms & 0x0040)?true:false;
    }

    if (!isset($fd->main_program)) {
        $fd->main_program = false;
    }
    if (!isset($fd->copy_file)) {
        $fd->copy_file = false;
    }

    $fd->present = true;
    $fds = update_file($fds, $fd);
    return $fds;
}

// scan the directory, and process files
//
function process_files($a, $v, $p, $fds) {
    $d = opendir("apps/$a/$v/$p");
    while ($f = readdir_aux($d)) {
        if ($f == "version.xml") continue;
        if (strstr($f, ".sig") == ".sig") continue;
        $fds = process_file($a, $v, $p, $f, $fds);
    }
    return $fds;
}

function parse_platform_name($p, &$platform, &$plan_class) {
    $x = explode("__", $p);
    $platform = $x[0];
    if (sizeof($x) > 1) {
        $plan_class = $x[1];
    } else {
        $plan_class = "";
    }
}

function parse_version($v) {
    $x = explode(".", $v);
    if (!is_numeric($x[0])) return -1;
    if (sizeof($x) > 1) {
        if (!is_numeric($x[1])) return -1;
        return $x[1] + 100*$x[0];
    }
    return (int)$x[0];
}

function already_exists($a, $v, $platform, $plan_class) {
    $app = lookup_app($a);
    $plat = lookup_platform($platform);
    $vnum = parse_version($v);
    $av = BoincAppVersion::lookup("appid=$app->id and version_num=$vnum and platformid=$plat->id and plan_class='$plan_class'");
    if ($av) return true;
    return false;
}

function missing_files($fds) {
    $missing = false;
    foreach ($fds as $fd) {
        if (!$fd->present) {
            echo "   File $fd->physical_name is listed in version.xml but not present\n";
            $missing = true;
        }
    }
    return $missing;
}

// Check whether there's a main program
//
function check_main_program($fds) {
    $n = 0;
    foreach ($fds as $fd) {
        if ($fd->main_program) $n++;
    }
    if ($n == 0) {
        echo "    No file was marked as the main program.\n";
        return false;
    }
    if ($n > 1) {
        echo "    More than one file was marked as the main program.\n";
        return false;
    }
    return true;
}

function confirm($fds) {
    echo "    Files:\n";
    foreach ($fds as $fd) {
        echo "        $fd->physical_name";
        if ($fd->main_program) {
            echo " (main program)";
        }
        echo "\n";
    }
    echo "    Do you want to add this application version (y/n)? ";
    $x = trim(fgets(STDIN));
    return ($x == "y");
}

// convert SimpleXMLElement object to a standard object
//
function convert_simplexml($x) {
    $fds = array();
    $fxs = $x->xpath('file');
    foreach ($fxs as $fx) {
        //echo "fx: "; print_r($fx);
        $fd = null;

        $fd->present = false;
        $fd->physical_name = trim((string) $fx->physical_name);
        $fd->logical_name = trim((string) $fx->logical_name);
        $fd->url = array();
        foreach($fx->xpath('url') as $url) {
            $fd->url[] = trim((string) $url);
        }
        $fd->main_program = false;
        foreach($fx->xpath('main_program') as $x) {
            $s = trim((string) $x);
            if ($s == "" || int($s)>0) $fd->main_program = true;
        }
        foreach($fx->xpath('copy_file') as $x) {
            $s = trim((string) $x);
            if ($s == "" || int($s)>0) $fd->copy_file = true;
        }

        //echo "fd: "; print_r($fd);
        $fds[] = $fd;
    }
    return $fds;
}

function process_version($a, $v, $p) {
    echo "Found application version: $a $v $p\n";
    $app = lookup_app($a);
    parse_platform_name($p, $platform, $plan_class);
    if (already_exists($a, $v, $platform, $plan_class)) {
        echo "  This app version already exists\n";
        return;
    }
    $vfile = "apps/$a/$v/$p/version.xml";
    if (is_file($vfile)) {
        $x = simplexml_load_file($vfile);
        if (!$x) {
            die("Can't load XML file apps/$a/$v/$p.  Check that it exists and is valid.");
        }
        $fds = convert_simplexml($x);
    } else {
        $fds = array();
    }

    $fds = process_files($a, $v, $p, $fds);

    if (missing_files($fds)) return;
    if (sizeof($fds) == 1) $fds[0]->main_program = true;
    if (!check_main_program($fds)) return;
    $api_version = get_api_version($a, $v, $p, $fds);

    if (!confirm($fds)) {
        return;
    }

    $xml = "";
    foreach ($fds as $fd) {
        $xml .= file_info_xml($fd);
    }
    $xml .=
        "<app_version>\n".
        "    <app_name>".$app->name."</app_name>\n".
        "    <version_num>".parse_version($v)."</version_num>\n".
        "    <api_version>$api_version</api_version>\n"
    ;
    foreach ($fds as $fd) {
        $xml .= file_ref_xml($fd);
    }
    $xml .= "</app_version>\n";

    $now = time();
    $vnum = parse_version($v);
    $plat = lookup_platform($platform);
    $query = "set create_time=$now, appid=$app->id, version_num=$vnum, platformid=$plat->id , xml_doc='$xml', plan_class='$plan_class'";

    $id = BoincAppVersion::insert($query);
    if ($id) {
        echo "    Application version added successfully; ID=$id\n";
    } else {
        echo "    Error; application version not added\n";
    }
}

function scan_version_dir($a, $v) {
    $d = opendir("apps/$a/$v");
    while ($p = readdir_aux($d)) {
        process_version($a, $v, $p);
    }
}

function scan_app_dir($a) {
    $d = opendir("apps/$a");
    while ($v = readdir_aux($d)) {
        if (parse_version($v) < 0) {
            echo "$v is not a version number; skipping\n";
            continue;
        }
        scan_version_dir($a, $v);
    }
    closedir($d);
}

function scan_apps() {
    $d = opendir("apps");
    while ($a = readdir_aux($d)) {
        if (!lookup_app($a)) {
            echo "$a is not an app\n";
            continue;
        }
        scan_app_dir($a);
    }
}

scan_apps();

?>
