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: |
32f44210356989749b36eaabb3a7d2d4 |
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
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><link rel=token_endpoint href='https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]'></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); } ?> |