#!/usr/bin/php -qC
<?php
# encoding: utf-8
# api: cli
# title: webhook
# description: to be run from fossil backoffice/after-receive hooks
# version: 0.1
# doc: https://fossil-scm.org/home/doc/trunk/www/hooks.md
#
# Meant to transform commits into common webhook JSON formats,
# and then send them off.
#
# Hook syntax:
#   fossil-webhook '%R' 'http://service.rest/api?token=123'
# Parameters:
#   %R        becomes repository filename (REQUIRED)
#   --fsl     fossil-style payload (default)
#   --git     crafts a github-style payload
#   --ping    basic ping
#   --if:wiki test artifact list for type (check-in, wiki, file)
#   httpt://  webhook service endpoint (REQUIRED)
#   +tok=123  inject additional parameters
#   

#-- init
$argv = $_SERVER["argv"];
$cfg = [
    "start" => microtime(TRUE),
    "dbg" => preg_grep("~^-+(de?bu?g|du?mp)$~i", $argv),
    "git" => preg_grep("~^-+gi?t?$~i", $argv),
    "ping" => preg_grep("~^-+pi?n?g?$~i", $argv),
    "help" => preg_grep("~^-+he?l?p?$~i", $argv),
    "repo" => current(preg_grep("~^/\w+.+\.(fsl|fossil|sqlite)$~", $argv)),
    "add" => preg_grep("~^\+[\w.-]+[:=].+$~", $argv),
    "if" => preg_grep("~^-+if:[\w-]+$~i", $argv),
    "url" => current(preg_grep("~^https?://.+$~i", $argv)),
    "artifacts" => fread(STDIN, 1<<20),
        #e9ce272e806c47a10e3d4cf570549a1d33133133 file src/code.c
        #1db32c3ff6c5969191d575c65916f6322b833313 wiki HomePage
        #313908390381390813913813931931830938aaaa check-in to trunk by user on 2222-11-30 00:00
    "json" => TRUE,
    "user" => null,
];
if (empty($cfg["repo"]) || empty($cfg["url"])) {
   die("%R or url missing");
}
if ($cfg["if"] and preg_match("/if:(.+)/", $cfg["if"][0], $uu) and !preg_match("/\\b$uu[1]\\b/", $cfg["artificts"])) {
   die("--if: no match");
}
$cfg += [
    "basename" => preg_replace("~\.\w+$~", "", basename($cfg["repo"])),
    "baseurl" => get_baseurl(),
    "title" => get_config("project-title"),
];

#-- operation flag
if ($cfg["help"]) {
   die("Usage as hook:\n  fossil-webhook %R --git http://service.rest/TOKEN/ETC\n");
}
elseif ($cfg["git"]) {
    $request = git_request();
}
elseif ($cfg["ping"]) {
    $request = basic_ping();
}
else {
    $request = fossil_request();
}

#-- and send
add_params($request);
if ($cfg["dbg"]) { die(json_encode($request, JSON_PRETTY_PRINT)); }
send($request);




/**
 * Process +add=paramval arguments,
 * simply adding them to $request[] top-level.
 *
 */
function add_params(&$request) {
    global $cfg;
    #-- +add=params?
    foreach ($cfg["add"] as $add) {
        preg_match("~^\+([\w-.]+)[:=](.+)$~", $add, $kv);
        $request[$kv[1]] = $kv[2];
    }
}


/**
 * Sending
 *
 */
function send($request) {
    global $cfg;
    #-- send query
    $c = curl_init($cfg["url"]);
    curl_setopt_array($c, [
        CURLOPT_POST => 1,
        CURLOPT_POSTFIELDS => $cfg["json"] ? json_encode($request, JSON_PRETTY_PRINT) : $request,
        CURLOPT_VERBOSE => 1,
    ]);
    $c->exec();
}



/**
 * Define a fossil-style webhook payload.
 *
 */
function fossil_request() {
    global $cfg;
    $url = get_baseurl();
    return [
        '$type' => "webhook",
        '$class' => "vcs fossil",
        '$ver' => "0.1",
        "action" => main_action($cfg["artifacts"]),
	"fossil" => get_config("server-code"),
	"url" => $url,
        "project" => [
            "name" => get_basename(),
            "title" => get_config("project-name"),
            "description" => get_config("project-description"),
            "size" => filesize($cfg["repo"]),
            "id" => get_config("project-code"),
            "url_stat" => "$url/json/stat",
            "url_timeline" => "$url/json/timeline/checkin",
            "url_dir" => "$url/json/dir",
            "url_trees" => "$url/ext/trees",
        ],
        "artifacts" => array_map(
            function($row) use($url) { return expand_artifact($row, $url); },
            get_artifacts($cfg["artifacts"])
        ),
        "user" => $cfg["user"],
	"timestamp" => time(),
        "procTimeMs" =>  1000 * (microtime(TRUE) - $cfg["start"]),
    ];
}


/**
 * More basic payload
 *
 */
function basic_ping() {
    global $cfg;
    $url = get_baseurl();
    return [
        '$type' => "webhook",
        '$class' => "ping basic",
        "action" => "edited",
	"url" => $url,
	"name" => get_basename(),
        "stdin" => $cfg["artifacts"],
	"timestamp" => time(),
    ];
}

 
/**
 * Simulate GitHub-style webhook.
 * (Unlikely that we can fill in all the minutae.)
 *
 */
function git_request() {
    global $cfg;
    return [
      'action' => 'edited',
      'rule' => [
        'id' => crc32($cfg["artifacts"]),
        'repository_id' => $rep_id = hexdec(substr(get_config("project-code", "12FFFF"), 0, 6)),
        'name' => $basename = get_basename(),
        'created_at' => iso8601(mtime_event("MIN")),
        'updated_at' => iso8601(mtime_event("MAX")),
        'pull_request_reviews_enforcement_level' => 'off',
        'required_approving_review_count' => 0,
        'dismiss_stale_reviews_on_push' => false,
        'require_code_owner_review' => false,
        'authorized_dismissal_actors_only' => false,
        'ignore_approvals_from_contributors' => false,
        'required_status_checks' => [
          0 => 'basic-CI',
        ],
        'required_status_checks_enforcement_level' => 'non_admins',
        'strict_required_status_checks_policy' => false,
        'signature_requirement_enforcement_level' => 'off',
        'linear_history_requirement_enforcement_level' => 'off',
        'admin_enforced' => false,
        'allow_force_pushes_enforcement_level' => 'off',
        'allow_deletions_enforcement_level' => 'off',
        'merge_queue_enforcement_level' => 'off',
        'required_deployments_enforcement_level' => 'off',
        'required_conversation_resolution_level' => 'off',
        'authorized_actors_only' => true,
        'authorized_actor_names' => [
          0 => $user = get_main_user(),
        ],
      ],
      'changes' => [
        'authorized_actors_only' => [
          'from' => false,
        ],
        'authorized_actor_names' => [
          'from' => [
          ],
        ],
      ],
      'repository' => [
        'id' => $rep_id,
        'node_id' => 'MDEwOlJlcG9zaXRvcnkxNzI3MzA1MQ==',
        'name' => 'octo-repo',
        'full_name' => 'octo-org/octo-repo',
        'private' => true,
        'owner' => [
          'login' => 'octo-org',
          'id' => 6811672,
          'node_id' => 'MDEyOk9yZ2FuaXphdGlvbjY4MTE2NzI=',
          'avatar_url' => 'https://avatars.githubusercontent.com/u/6811672?v=4',
          'gravatar_id' => '',
          'url' => 'https://api.github.com/users/octo-org',
          'html_url' => 'https://github.com/octo-org',
          'followers_url' => 'https://api.github.com/users/octo-org/followers',
          'following_url' => 'https://api.github.com/users/octo-org/following{/other_user}',
          'gists_url' => 'https://api.github.com/users/octo-org/gists{/gist_id}',
          'starred_url' => 'https://api.github.com/users/octo-org/starred{/owner}{/repo}',
          'subscriptions_url' => 'https://api.github.com/users/octo-org/subscriptions',
          'organizations_url' => 'https://api.github.com/users/octo-org/orgs',
          'repos_url' => 'https://api.github.com/users/octo-org/repos',
          'events_url' => 'https://api.github.com/users/octo-org/events{/privacy}',
          'received_events_url' => 'https://api.github.com/users/octo-org/received_events',
          'type' => 'Organization',
          'site_admin' => false,
        ],
        'html_url' => 'https://github.com/octo-org/octo-repo',
        'description' => 'My first repo on GitHub!',
        'fork' => false,
        'url' => 'https://api.github.com/repos/octo-org/octo-repo',
        'forks_url' => 'https://api.github.com/repos/octo-org/octo-repo/forks',
        'keys_url' => 'https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}',
        'collaborators_url' => 'https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}',
        'teams_url' => 'https://api.github.com/repos/octo-org/octo-repo/teams',
        'hooks_url' => 'https://api.github.com/repos/octo-org/octo-repo/hooks',
        'issue_events_url' => 'https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}',
        'events_url' => 'https://api.github.com/repos/octo-org/octo-repo/events',
        'assignees_url' => 'https://api.github.com/repos/octo-org/octo-repo/assignees{/user}',
        'branches_url' => 'https://api.github.com/repos/octo-org/octo-repo/branches{/branch}',
        'tags_url' => 'https://api.github.com/repos/octo-org/octo-repo/tags',
        'blobs_url' => 'https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}',
        'git_tags_url' => 'https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}',
        'git_refs_url' => 'https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}',
        'trees_url' => 'https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}',
        'statuses_url' => 'https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}',
        'languages_url' => 'https://api.github.com/repos/octo-org/octo-repo/languages',
        'stargazers_url' => 'https://api.github.com/repos/octo-org/octo-repo/stargazers',
        'contributors_url' => 'https://api.github.com/repos/octo-org/octo-repo/contributors',
        'subscribers_url' => 'https://api.github.com/repos/octo-org/octo-repo/subscribers',
        'subscription_url' => 'https://api.github.com/repos/octo-org/octo-repo/subscription',
        'commits_url' => 'https://api.github.com/repos/octo-org/octo-repo/commits{/sha}',
        'git_commits_url' => 'https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}',
        'comments_url' => 'https://api.github.com/repos/octo-org/octo-repo/comments{/number}',
        'issue_comment_url' => 'https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}',
        'contents_url' => 'https://api.github.com/repos/octo-org/octo-repo/contents/{+path}',
        'compare_url' => 'https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}',
        'merges_url' => 'https://api.github.com/repos/octo-org/octo-repo/merges',
        'archive_url' => 'https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}',
        'downloads_url' => 'https://api.github.com/repos/octo-org/octo-repo/downloads',
        'issues_url' => 'https://api.github.com/repos/octo-org/octo-repo/issues{/number}',
        'pulls_url' => 'https://api.github.com/repos/octo-org/octo-repo/pulls{/number}',
        'milestones_url' => 'https://api.github.com/repos/octo-org/octo-repo/milestones{/number}',
        'notifications_url' => 'https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}',
        'labels_url' => 'https://api.github.com/repos/octo-org/octo-repo/labels{/name}',
        'releases_url' => 'https://api.github.com/repos/octo-org/octo-repo/releases{/id}',
        'deployments_url' => 'https://api.github.com/repos/octo-org/octo-repo/deployments',
        'created_at' => '2014-02-28T02:42:51Z',
        'updated_at' => '2021-03-11T14:54:13Z',
        'pushed_at' => '2021-03-11T14:54:10Z',
        'git_url' => 'git://github.com/octo-org/octo-repo.git',
        'ssh_url' => 'org-6811672@github.com:octo-org/octo-repo.git',
        'clone_url' => 'https://github.com/octo-org/octo-repo.git',
        'svn_url' => 'https://github.com/octo-org/octo-repo',
        'homepage' => '',
        'size' => 300,
        'stargazers_count' => 0,
        'watchers_count' => 0,
        'language' => 'C',
        'has_issues' => true,
        'has_projects' => false,
        'has_downloads' => true,
        'has_wiki' => false,
        'has_pages' => true,
        'forks_count' => 0,
        'mirror_url' => NULL,
        'archived' => false,
        'disabled' => false,
        'open_issues_count' => 39,
        'license' => NULL,
        'forks' => 0,
        'open_issues' => 39,
        'watchers' => 0,
        'default_branch' => 'trunk',
      ],
      'organization' => [
        'login' => 'octo-org',
        'id' => 6811672,
        'node_id' => 'MDEyOk9yZ2FuaXphdGlvbjY4MTE2NzI=',
        'url' => 'https://api.github.com/orgs/octo-org',
        'repos_url' => 'https://api.github.com/orgs/octo-org/repos',
        'events_url' => 'https://api.github.com/orgs/octo-org/events',
        'hooks_url' => 'https://api.github.com/orgs/octo-org/hooks',
        'issues_url' => 'https://api.github.com/orgs/octo-org/issues',
        'members_url' => 'https://api.github.com/orgs/octo-org/members{/member}',
        'public_members_url' => 'https://api.github.com/orgs/octo-org/public_members{/member}',
        'avatar_url' => 'https://avatars.githubusercontent.com/u/6811672?v=4',
        'description' => 'Working better together!',
      ],
      'sender' => [
        'login' => $user,
        'id' => crc32($user),
        'node_id' => base64_encode($user),
        'avatar_url' => 'https://avatars1.githubusercontent.com/u/21031067?v=4',
        'gravatar_id' => '',
        'url' => 'https://api.github.com/users/Codertocat',
        'html_url' => 'https://github.com/Codertocat',
        'followers_url' => 'https://api.github.com/users/Codertocat/followers',
        'following_url' => 'https://api.github.com/users/Codertocat/following{/other_user}',
        'gists_url' => 'https://api.github.com/users/Codertocat/gists{/gist_id}',
        'starred_url' => 'https://api.github.com/users/Codertocat/starred{/owner}{/repo}',
        'subscriptions_url' => 'https://api.github.com/users/Codertocat/subscriptions',
        'organizations_url' => 'https://api.github.com/users/Codertocat/orgs',
        'repos_url' => 'https://api.github.com/users/Codertocat/repos',
        'events_url' => 'https://api.github.com/users/Codertocat/events{/privacy}',
        'received_events_url' => 'https://api.github.com/users/Codertocat/received_events',
        'type' => 'User',
        'site_admin' => false,
      ],
    ];
    # do...
}


/**
 * 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;
    global $cfg;
    if (empty($db)) {
        if (!preg_match("~^/\w[/\w.-]+\w\.(fs?l?|fossil|sqlite)$~", $cfg["repo"])) {
            die("db(): FOSSIL_REPOSITORY doesn't look right. Abort.");
        } 
        #$db = new PDO("sqlite::memory:");
        $db = new PDO("sqlite:$cfg[repo]");
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
    }
    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;
}



#-- public access url
function get_baseurl() {
    $urls = array_column(db("SELECT SUBSTR(name,9,200) AS url FROM config WHERE name LIKE 'baseurl:%'"), "url");
    if ($best = preg_grep("~localhost|:\d+/~i", $urls, PREG_GREP_INVERT)) {
        return array_values($best)[0];
    }
    elseif ($best = preg_grep("~:\d+/~i", $urls, PREG_GREP_INVERT)) {
        return array_values($best)[0];
    }
    else {
        return $urls[0];
    }
}

#-- artifact owner
function get_user($uuid) {
    $r = db("
        SELECT login
          FROM blob
          LEFT JOIN rcvfrom ON blob.rcvid=rcvfrom.rcvid
          LEFT JOIN user ON user.uid=rcvfrom.uid
         WHERE uuid = ?",
        [$uuid]
    );
    return $r ? $r[0]["login"] : null;
}

#-- primary user
function get_main_user() {
    return db("
        SELECT user, COUNT(type) AS cnt
          FROM event
         WHERE type='ci'
         GROUP BY user
         ORDER BY cnt DESC"
    )[0]["user"];
}

#-- basename.fossil
function get_basename() {
    global $cfg;
    return preg_replace("/\.\w+$/", "", basename($cfg["repo"]));
}

#-- split STDIN into rows
function get_artifacts($art) {
    preg_match_all("/^(\w+)\s+([\w-]+)(?:\s+(.+))?$/m", $art, $rows, PREG_SET_ORDER);
    $rows = array_map(
        function($row) {
            return [
                "uuid" => $row[1],
                "type" => $row[2],
                "comment" => $row[3],
            ];
        },
        $rows
    );
    return $rows;
}

#-- turn uuid into appropriate url
function expand_artifact($row, $url, $q="urlencode") {
    global $cfg;
    switch ($row["type"]) {
        case "attachment":
        case "file":
            $row["name"] = $row["comment"];
            $row["url_raw"] = "$url/raw/$row[uuid]?at={$q($row['name'])}";
            $row["url_json"] = "$url/json/artifact/{$q($row['name'])}";
            break;
        case "wiki":
            $row["name"] = $row["comment"];
            $row["url_web"] = "$url/wiki/{$q($row['name'])}";
            $row["url_json"] = "$url/json/wiki/get/{$q($row['name'])}";
            break;
        case "check-in":
            break;
        case "attachment-control":
        case "tag":
        case "referenced":
        default:
            break;
    }
    $cfg["user"] = $row["user"] = get_user($row["uuid"]);
    return $row;
}

#-- check-in or file, or other artifact types
function main_action($art) {
    foreach (["check-in", "file", "attachment", "wiki", "referenced", "tag"] as $t) {
        if (preg_match("/^\w+\s$t\\b/m", $art)) {
            return $t;
        }
    }
    return "after-receive";
}

function iso8601($t) {
    return strftime("%Y-%m-%dT%H:%M:%SZ", $t);
}

function mtime_event($FN="MIN") {
    return db("SELECT strftime('%s', $FN(mtime)) AS dt FROM event LIMIT 1")[0]["dt"];
}
?>