Collection of themes/skins for the Fossil SCM

โŒˆโŒ‹ โŽ‡ branch:  Fossil Skins Extra


Check-in [32f4421035]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Rough outline of how to map requests onto fossil commands.
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 32f44210356989749b36eaabb3a7d2d43246839d
User & Date: mario 2021-04-06 18:22:28
Context
2021-04-06
18:23
Use markdown error message for non-admin users. check-in: 5b4bfc7542 user: mario tags: trunk
18:22
Rough outline of how to map requests onto fossil commands. check-in: 32f4421035 user: mario tags: trunk
11:52
Update comments on user.* table. check-in: ab76ada131 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added extroot/micropub.















































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
#!/usr/bin/php-cgi -dcgi.force_redirect=0
<?php
# encoding: utf-8
# api: cgi
# type: json
# title: MicroPub API
# description: Accepts blog/ticket/chat entries from micropub clients
# version: 0.0
# state: untested
# depends: php:sqlite
# doc: https://indieweb.org/obtaining-an-access-token
# config: -
#
# Supposed to feed micropub requests back into `fossil`.
# Verifies auth/token, unpacks request parameters, and invokes fossil bin
# on the current -R repository.
#   ยท cat "content..." | fossil wiki create|commit -M text/x-markdown
#   ยท cat "content..." | fossil wiki --technote -M text/x-markdown
#   ยท ticket add CONTENT ... STATUS ... ETC ...
#
#

if ($_REQUEST["dbg"]) {
    error_reporting(E_ALL); ini_set("display_errors", 1);
}

#-- database (== fossil repo)
function db($sql="", $params=[]) {
    static $db;
    if (empty($db)) {
        $db = new PDO("sqlite:$_SERVER[FOSSIL_REPOSITORY]");
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
    }
    if ($params) {
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
    else {
        return $db->query($sql);
    }
}

#-- JSON/form-encoded output
function json_response($r, $status) {
    if ($status) {
        header("Status: $status");
        if (is_string($r)) {
            $r = ["error"=>"invalid_request", "error_description"=>$r];
        }
    }
    if (stristr($_SERVER["HTTP_ACCEPT"], "/json")) {
        header("Content-Type: text/json");
        die(json_encode($r, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
    }
    elseif (stristr($_SERVER["HTTP_ACCEPT"], "/x-www-form")) {
        header("Content-Type: application/x-www-form-urlencoded");
        array_walk($r, function(&$v, $k) { $v = "$k=" . urlencode($v); });
        die(join("&", $r));
    }
    else {
        header("Content-Type: text/php-source");
        die(var_export($r, True));
    }
}


#-- load authorization properties by auth code
function get_token_by_code($code) {
    return db("SELECT * FROM fx_auth WHERE code=? AND `type`=? AND expires>?", [$code, 'token', time()]) ?: [[]];
}
#-- get code from Authorization: header
function bearer() {
    if (empty($_SERVER["HTTP_AUTHORIZATION"])) {
         return "";
    }
    if (preg_match("/Bearer\s+(\S+)/i", $_SERVER["HTTP_AUTHORIZATION"], $uu)) {
         return $uu[1];
    }
}
#-- fetch token for Authorization: Bearer id
function get_token($access_token=NULL) {
    # find Auth: token
    $code = $access_token ?: bearer();
    return get_token_by_code($code)[0];
}

#-- alias for generic types to fossil items
function scope_map($h_type) {
    $h_type = preg_replace("/^h-", "", $h_type);
    $map = [
         "entry" => "technote",
         "issue" => "ticket",
         "event" => "chat",
         "cite" => "wiki",
    ];
    return $map[$h_type] ?: $h_type;
}
# check if request action+type entry all match token scopes
function verify_scope($token=[], $scopes=[]) {
    preg_match_all("/(\w+)/", $token["scope"], $scope_ls);
    $ok = NULL;
    # comapare against token scope list
    foreach ($scopes as $t) {
        $ok = ($a !== False) && in_array(scope_map($t), $scope_ls[1]);
    }
    return $ok;
}

#-- transform $_POST to JSON body
function request() {
    if (preg_match("~^\s(application|text|json)/json\s(;|$)~", $_SERVER["HTTP_CONTENT_TYPE"])) {
        $json = json_decode(file_get_contents("php://input"), True);
    }
    elseif (count($_POST)) {
        $json = [
            "q" => "$_POST[q]",
            "type" => "h-$_POST[h]",
            "properties" => array_map(function ($v) { return [$v]; }, $_POST),
        ];
    }
    return $_POST = $json;
}


#-- transform post to fossil command
class create {

    public $token = [];
    public $url = "";

    # dispatch `type` onto functions
    function __construct($post, $token) {
        $this->token = $token;
        $action = $post["action"];
        $type = scope_map($post["type"]);
        if (method_exists($this, $type)) {
            $r = call_user_func([$this, $type], $post["properties"]);
            if (!$this->url) {
                $this->new_url("", $r);
            }
            if ($this->url) {
                header("Location: $this->url");
                json_response(["result"=>"$r"], "201 Created");
            }
        }
        json_response("Unknown h-type: `$type`", "400 No");
    }

    # grep new Location: url from fossil command output
    function new_url($path, $r="") {
        if (preg_match("/Created new wiki page (.+)\./i", $r, $uu)) {
            $path = "/wiki/$uu[1]";
        }
        elseif (preg_match("/Created new tech note ([\d\-:.\s]+)$/im", $r, $uu)) {
            if (preg_match("/(\w+) $uu[1]/", $this->fossil(["wiki","list","-t","-s"]), $uu)) {
                $path = "/technote/$uu[1]";
            }
        }
        elseif (preg_match("/ticket add succeeded for (\w+)/i", $r, $uu)) {
            $path = "/ticket/$uu[1]";
        }
        $this->url = preg_replace("/ext/\w+", "", "https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]$path");
    }
    
    function technote($p, $act="create") {
        # &content=
        # &category[]=
        $args = ["wiki", "$act", "--technote", ($this->id ?: "now"), "--mimetype", "text/x-markdown", "-"];
        if ($p["category"]) { $args += ["--technote-tags", implode(",", $p["category"])]; }
        $md = join("\n", $p["content"]);
        return $this->fossil($args, $md);
    }

    function wiki($p, $act="create") {
        # &title=
        # &content=
        # &category[]=
        $args = ["wiki", "$act", "--mimetype", "text/x-markdown", ($this->id ?: $p["title"][0])];
        $md = join("\n", $p["content"]);
        return $this->fossil($args, $md);
    }

    function ticket($p, $args=["ticket", "add"]) {
        # comment type	status	subsystem	priority	severity	foundin	private_contact	resolution	title	comment
        $map = ["type"=>"type", "summary"=>"title", "content"=>"comment", "priority"=>"priority", "severity"=>"severity", "category"=>"subsystem", "version"=>"foundin", "contact"=>"private_contact"];
        foreach ($map as $from=>$to) {
            if ($p[$from]) {
                $args[] = $to;
                $args[] = implode(", ", $p[$from]);
            }
        }
        return $this->fossil($args);
    }

    function chat($p, $act="send") {
        $args = ["chat", "send", "-m", $p["content"]];
        if (count($_FILES)) {
            $args += ["-f", current($_FILES)["tmp_name"]];
        }
        $_FILES = [];
        $this->new_url("/chat/#" . md5(rand()));
        return $this->fossil($args);
    }

    # a bit more escaping
    static function esc($s) {
        if (is_array($s)) { $s = implode(" ", $s); }  // join any list
        $s = preg_replace("/[^\\r\\n\\t\\x20-\\xFF]/", "", $s); // strip control characters
        return escapeshellarg($s);  // quote for shell
    }

    # invoke fossil binary with escaped arguments, and possibly piping content as stdin
    function fossil($args, $input) {
        $bin = "fossil";
        $args = ["-R", $_SERVER["FOSSIL_REPOSITORY"]] + $args;
        $args = [$bin] + array_map("create::esc", $args);
        if (empty($input)) {
            exec(implode(" ", $args), $stdout);
            return implode("\n", $stdout);
        }
        else {
            $pipespec = [["pipe","r"],["pipe","w"],["pipe","a"]];
            $pipes = [NULL, NULL, NULL];
            $env = [  # override ENV so as not to trigger nested CGI mode
                "PATH" => $_SERVER["PATH"],
                "TEMP" => $_SERVER["TEMP"],
                "USER" => $this->token["user"],
                "LANG" => "C",
                "EDITOR" => "/bin/false",
            ];
            $proc = proc_open(implode(" ", $args), $pipespec, $pipes, "/tmp", $env);
            fwrite($pipes[0], $input);
            $stdout = fread($pipes[1]);
            array_map("fclose", $pipes);
            proc_close($proc);
            return $stdout;
        }
    }
}

# same commands, but usually `commit` instead of `create`
class update extends create {
    function __construct($post, $token) {
        if (!$this->url = $post["url"]) {
            json_response("No original url: given", "400 Params");
        }
        $this->id = preg_replace("~^.+/(?=\w+)|/$~", "", $this->url);
        $post["properties"] = $post["replace"] ?: $post["add"];
        parent::__construct($post, $token);
    }
    function technote($p, $act="commit") {
        return parent::technote($p, $act);
    }
    function wiki($p, $act="commit") {
        return parent::wiki($p, $act);
    }
    function ticket($p, $args=["ticket", "set", "ID"]) {
        return parent::ticket($p, ["ticket", "set", $this->id]);
    }
    function chat($p, $act="send") {}
}

# fetch source code
class source extends update {
    function __construct($get, $token) {
        if (!$this->url = $get["url"]) {
            json_response("No original url: given", "400 Params");
        }
        $this->id = preg_replace("~^.+/(?=\w+)|/$~", "", $this->url);
        $this->token = $token;
        $type = scope_map($get["type"]);
        if (method_exists($this, $type)) {
            $r = call_user_func([$this, $type], []);
            json_response([
                "type" => [$get["type"]],
                "properties" => (is_array($r) ? $r : [
                    "content"=> [$r],
                ])
            ]);
        }
        json_response("Unknown h-type: `$type`", "400 No");
    }
    function technote($p, $act="export") {
        die($this->fossil(["wiki", "export", "-t", $this->id]));
    }
    function wiki($p, $act="export") {
        die($this->fossil(["wiki", "export", $this->id]));
    }
    function ticket($p, $act="history") {
        die($this->fossil(["ticket", "history", $this->id]));
    }
    function chat($p, $act="send") {}
}


#-- run
if (!request()) {
    print<<<HTML
    <div class='fossil-doc' data-title='MicroPub API'>
       <svg style="float:left; margin-right:30pt" width="156" height="126" version="1.1" viewBox="0 0 156.51 126.77" xmlns="http://www.w3.org/2000/svg">
        <g transform="translate(-29.749 -72.377)" fill="#5c3566">
         <path d="m103.29 73.103c3.9056-0.01807 7.8113-0.03615 11.717-0.05422 2.526 1.5001 1.5486 4.8454 2.0569 7.3146 3.1429 39.032 6.2859 78.064 9.4288 117.1-0.49825 2.5589-3.4725 1.3775-5.3001 1.6403-9.3872-0.0112-18.774-0.0224-28.162-0.0336-2.3887-0.63707-0.91351-3.4807-1.0714-5.2336 3.2757-39.864 6.5514-79.729 9.8271-119.59 0.42052-0.44951 0.84244-1.0337 1.5034-1.1356z"/>
         <path d="m120.38 73.127c5.1936 1.1052 12.911-2.4119 16.035 2.898 16.539 40.252 33.24 80.453 49.841 120.69-2.5876 3.7337-8.9293 0.82482-13.081 1.6871-11.989-0.0484-23.982 0.0943-35.967-0.30639-2.8022-4.0246-1.711-10.44-3.0477-15.379-4.8334-36.042-9.9045-72.071-14.589-108.12 0.0326-0.55322 0.17007-1.2857 0.80831-1.4647z"/>
         <path d="m90.484 72.482c2.3372 0.13672 4.732-0.08479 7.0317 0.28512 1.709 1.4705 0.18503 3.9654 0.26679 5.9122-5.1709 39.581-10.342 79.163-15.513 118.74-0.52456 2.2879-3.6851 0.74965-5.4418 1.1551-15.525-0.2027-31.05-0.40541-46.575-0.60812-1.5214-1.1172 0.86267-3.4736 1.1244-5.0745 16.758-39.681 33.516-79.363 50.275-119.04 1.2482-2.5695 6.3795-0.89786 8.8317-1.3698z"/>
        </g>
       </svg>
       <h3>MicroPub endpoint</h3>
       Interface for micropub clients to post blog/technotes (entry), tickets (issue), or possibly wiki or forum posts.
       <p>
       Should be registered in the repo template or a user homepage with:<br>
       <code>&lt;link rel=token_endpoint href='https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]'&gt;</code>
    </div>
HTML;
    die();
}
elseif ($_POST["q"] == "config") {
    json_response([
        "media-endpoint" => "https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]",  # same
        "syndicate-to" => [],
        "supported-types" => [
            "entry" => [
               "internal-name" => "technote",
               "properties" => ["content"],
            ],
            "issue" => [
                "internal-name" => "ticket",
                "properties" => ["content", "summary"],
            ],
            "event" => [
                "internal-name" => "chat",
                "accepts-media" => TRUE,
                "properties" => ["content", "file"],
            ],
            "cite"  => [
                "internal-name" => "wiki",
                "properties" => ["content"],
            ],
        ],
    ]);
}
elseif (!$token = get_token($_POST["access_token"])) {
    json_response("token expired", "401 Token expired");
}
elseif (!verify_scope($token, [$_POST["action"], $_POST["type"]])) {
    json_response("scope insufficient", "403 Scope insufficient");
}
elseif ($_GET["q"] == "source") {
    new source($_GET, $token);
}
elseif ($_POST["action"] == "update") {
    new update($_POST, $token);
}
else {
    new create($_POST, $token);
}


?>