~mro/pin4sha.cgi

63edb6f88f06a532de00023d6a61fc6220ddc3bc — Marcus Rohrmoser 4 years ago 8c0db45
λ 🐫
33 files changed, 658 insertions(+), 1272 deletions(-)

M .gitignore
M .travis.yml
A Makefile
M README.md
A bin/cgi.ml
A bin/dune
A bin/pinboard.ml
A bin/shell.ml
D bindata.go
D build.sh
M doap.rdf
A lib/cgi.ml
A lib/datetime.ml
A lib/dune
A lib/name.ml
A lib/pinboard.ml
A lib/url.ml
A lib/version.ml
D model.go
D model_test.go
D pinboard.go
R openapi.yaml => pinboard.in/v1/openapi.yaml
D pinboard_test.go
R testdata/bookmark/login.html => test/data/bookmark/login.html
R testdata/sebsauvage/login.html => test/data/sebsauvage/login.html
R testdata/shaarligo/linkform.html => test/data/shaarligo/linkform.html
A test/data/soup_test/configure.1.html
R testdata/v0.10.2/login.html => test/data/v0.10.2/login.html
A test/dune
A test/simple_test.ml
A test/soup_test.ml
A test/url_test.ml
D version.go
M .gitignore => .gitignore +4 -6
@@ 1,6 1,4 @@
scripts/curl.*
scripts/response.rnc
scripts/categories.rnc
source.tar.gz
tmp/
pinboard4shaarli-linux-amd64-*.gz
_build/
.merlin
dune-project
bin/version.ml

M .travis.yml => .travis.yml +1 -67
@@ 1,67 1,1 @@
---
# http://docs.travis-ci.com/user/migrating-from-legacy/
sudo: false
branches:
  only: [master, develop]
# http://docs.travis-ci.com/user/languages/php/#Choosing-PHP-versions-to-test-against
language: php
php:
- 5.6
- 7.0
- 7.3
# http://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle
# http://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix
env:
  global:
  - GITHUB_SRC_SUBDIR=Shaarli-*
  - BASE_URL=http://127.0.0.1:8000
  - USERNAME=tast
  - PASSWORD=tust
  matrix:
  - GITHUB=sebsauvage/Shaarli/archive/master
  - GITHUB=shaarli/Shaarli/archive/v0.0.40beta
  - GITHUB=shaarli/Shaarli/archive/v0.0.41beta
  - GITHUB=shaarli/Shaarli/archive/v0.0.42beta
#  - GITHUB=shaarli/Shaarli/archive/v0.0.43beta
#  - GITHUB=shaarli/Shaarli/archive/v0.0.44beta
  - GITHUB=shaarli/Shaarli/archive/v0.0.45beta
  - GITHUB=shaarli/Shaarli/archive/v0.5.4
  - GITHUB=shaarli/Shaarli/archive/v0.6.4
  - GITHUB=shaarli/Shaarli/archive/v0.6.5
  - GITHUB=shaarli/Shaarli/archive/v0.7.0
  - GITHUB=shaarli/Shaarli/archive/v0.8.5
  - GITHUB=shaarli/Shaarli/archive/v0.9.5
  - GITHUB=shaarli/Shaarli/archive/v0.10.3
  - GITHUB=shaarli/Shaarli/archive/v0.11.1
  - GITHUB=shaarli/Shaarli/archive/master
  - GITHUB=shaarli/Shaarli/archive/stable
  - GITHUB=shaarli/Shaarli/archive/webdesign
matrix:
  allow_failures:
  # first community release is known to fail, too:
  - env: GITHUB=shaarli/Shaarli/archive/v0.0.40beta
  # why does this one fail?
  - env: GITHUB=shaarli/Shaarli/archive/v0.0.42beta
  - env: GITHUB=shaarli/Shaarli/archive/v0.5.4
  - env: GITHUB=shaarli/Shaarli/archive/v0.6.4
  - env: GITHUB=shaarli/Shaarli/archive/webdesign
before_install:
- sh scripts/download.sh
- go get github.com/yhat/scrape
- go get golang.org/x/net/html
- go get golang.org/x/net/html/atom
  # for testing only:
- go get github.com/stretchr/testify
- go vet -x
- go test -v
install:
- go build -ldflags "-s -w -X main.GitSHA1=$(git rev-parse --short HEAD)" -o pinboard.cgi
addons:
  apt:
    packages:
    - xsltproc
    - libxml2-utils
#before_script:
#  # http://docs.travis-ci.com/user/customizing-the-build/#Implementing-Complex-Build-Steps
script:
- sh scripts/run-tests.sh


A Makefile => Makefile +31 -0
@@ 0,0 1,31 @@
#
# https://github.com/ocaml/dune/tree/master/example/sample-projects/hello_world
# via https://stackoverflow.com/a/54712669
#
.PHONY: all build clean test install uninstall doc examples

build:
	@echo "let git_sha = \""`git rev-parse --short HEAD`"\"" > bin/version.ml
	@echo "let date = \""`date`"\""                         >> bin/version.ml
	dune build bin/pinboard.exe

all: build

test:
	dune runtest

examples:
	dune build @examples

install:
	dune install

uninstall:
	dune uninstall

doc:
	dune build @doc

clean:
	rm -rf _build *.install


M README.md => README.md +20 -17
@@ 1,38 1,41 @@
# pinboard4shaarli.cgi
# pinboard.in.cgi

The natural API for shaarli.

[![Build Status](https://travis-ci.org/mro/Shaarli-API-test.svg?branch=master)](https://travis-ci.org/mro/Shaarli-API-test)

## Why?

The wish to have an API goes back to the [early days](https://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas).
The wish to have an API goes back to the [early
days](https://sebsauvage.net/wiki/doku.php?id=php:shaarli:ideas).

And because shaarli started as a personal, minimal delicious clone, using a minimal subset of just
the delicious API, seems natural to me. [pinboard](https://pinboard.in/api/) prooves the API
is not only seasoned and mature, but also still up the job today.
And because shaarli started as a personal, minimal, delicious clone, using a
minimal subset of just that very API seems natural to me.
[pinboard.in](https://pinboard.in/api/) prooves the API is not only seasoned and
mature, but also still up the job today.

Also I need a drop-in API compatibility layer for a wide range of shaarli versions out there being
fed to by [ShaarliOS](https://code.mro.name/mro/ShaarliOS/).
Also another project of mine, [ShaarliOS](/mro/ShaarliOS/) needs a drop-in API
compatibility layer for a wide range of shaarlis out in the wild.

## How?

You find a single, statically linked, zero-dependencies ([Go](https://golang.org/)) binary which is both a
You find a single, statically linked, zero-dependencies ([🐫
Ocaml](https://ocaml.org/)) binary which is both a

1. cgi to drop into your shaarli php webapplication next to index.php – as the API endpoint,
2. commandline client to any shaarli out there, mostly for debugging purposes.
1. cgi to drop into your shaarli php webapplication next to index.php – as the API
   endpoint,
2. commandline client to any shaarli out there, mostly for debugging and
   compatibility-testing purposes.

![post flow](post.png)

## Compatibility

All shaarlis from the old ages until at least spring 2019 (v0.10.3).

All systems [Go](https://golang.org/) can produce binaries for.
All shaarlis from the old ages until spring 2020
([v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)).

Just the delicious API calls in [openapi.yaml](openapi.yaml)
All systems [🐫 Ocaml](https://ocaml.org/) can produce binaries for.

The API server code (delicious implementation) is the same as in [ShaarliGo](https://code.mro.name/mro/ShaarliGo).
Just the delicious API calls in
[pinboard.in/v1/openapi.yaml](pinboard.in/v1/openapi.yaml)

## Design Goals


A bin/cgi.ml => bin/cgi.ml +48 -0
@@ 0,0 1,48 @@

open Lib

let w s = 
  print_string s;
  print_string "\n"

let va n =
  print_string "<li>";
  print_string n;
  print_string ": ";
  let v = Cgi.getenv_safe ~default:"-" n in
    (* TODO: escape *)
    print_string v;
  print_string "</li>";
  print_string "\n"

let run () =
  w "Content-type: text/html; charset=utf-8";
  w "";
  w "<html>
<head><title>Hello, WorΞ»d</title></head>
<body>
<h1>OCaml, where art thou 🐫!</h1>
<p>";
  let cwd = Sys.getcwd () in
    w cwd;
  w "</p>
<ul>";
  va "HOME";
  va "HTTPS";
  va "HTTP_HOST";
  va "HTTP_COOKIE";
  va "HTTP_ACCEPT";
  va "REMOTE_ADDR";
  va "REMOTE_USER";
  va Cgi.http_request_method;
  va "REQUEST_URI";
  va "PATH_INFO";
  va "QUERY_STRING";
  va "SERVER_NAME";
  va "SERVER_PORT";
  va "SERVER_SOFTWARE";
  w "</ul>
</body>
</html>";
  0


A bin/dune => bin/dune +34 -0
@@ 0,0 1,34 @@
; https://stackoverflow.com/a/53325230/349514
(executable
  (name pinboard)
  (libraries Lib)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Prepare for static linking
;
; http://rgrinberg.com/posts/static-binaries-tutorial/
; https://discuss.ocaml.org/t/statically-link/1464/9
; Issue https://discuss.ocaml.org/t/statically-link/1464/13
; https://www.systutorials.com/how-to-statically-link-ocaml-programs/
;
; $ date
; Tue Mar 24 11:36:40 CET 2020
; $ uname -o -m
; x86_64 GNU/Linux
; $ cat /etc/issue
; Devuan GNU/Linux 1 \n \l
;
; # on Ubuntu Bionic note
; # https://github.com/ocaml/ocaml/issues/9131#issuecomment-599765888
; $ sudo add-apt-repository ppa:avsm/musl
;
; $ sudo apt-get install musl-tools
; $ eval (opam env)
; $ opam switch create 4.10.0+musl+static+flambda
; $ opam switch 4.10.0+musl+static+flambda
; $ eval (opam env)
; $ opam install dune
; $ make clean build
; $ file _build/default/bin/*.exe 
; _build/default/bin/meta.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
  (link_flags (-ccopt "-static"))
)

A bin/pinboard.ml => bin/pinboard.ml +34 -0
@@ 0,0 1,34 @@
(* 
 * https://caml.inria.fr/pub/docs/u3-ocaml/ocaml-steps.html 
 *
 * extract some stuff about the request:
 * - method
 * - request uri
 * - header
 * - cookie(s)
 * - POST form data
 *
 * Response:
 * - http status + reason
 * - header
 *   - content-type
 *   - server
 * - body
 *   - xml+atom (syndic) with xslt and comment prefix,
 *   - xhtml or
 *   - text/plain
 *
 * http://cumulus.github.io/Syndic/syndic/Syndic__/Syndic_atom/#input-and-output
 *
 * other cgi lib:
 * https://gitlab.com/gerdstolpmann/lib-ocamlnet3/blob/master/code/examples/cgi/netcgi2/add.ml
 * http://projects.camlcity.org/projects/dl/ocamlnet-4.1.6/doc/html-main/Netcgi.html#TYPEexn_handler
 *)

let () =
 let status =
    match Sys.getenv_opt Lib.Cgi.http_request_method with
    | Some _ -> Cgi.run()
    | None   -> Shell.run() in
  exit status;;


A bin/shell.ml => bin/shell.ml +46 -0
@@ 0,0 1,46 @@

(* https://caml.inria.fr/pub/docs/manual-ocaml/libref/Sys.html *)

let print_version () =
  let exe = Filename.basename Sys.executable_name in
  Printf.printf "%s: https://mro.name/%s/v%s, %s\n" exe "pin4sha.cgi" Version.git_sha Version.date;
  0

let print_help () =
  let exe = Filename.basename Sys.executable_name in
  Printf.printf "
Access a shaarli as if it had the pinboard.in/api.

SYNOPSIS

$ %s -v

$ %s -h

$ %s 'https://uid:pwd@my.shaarli.host/posts/get?dt=2011-09-14'

" exe exe exe;
  0

let err i msgs =
  let exe = Filename.basename Sys.executable_name in
  msgs
    |> List.cons exe
    |> String.concat ": "
    |> prerr_endline;
  i

let run () =
  let status = match Sys.argv |> Array.to_list |> List.tl with
  | []  -> err 2 ["get help with -h"]
  | arg ->
    begin match List.hd arg with
    | "-h"
    | "--help"    -> print_help ()
    | "-v"
    | "--version" -> print_version ()
    | n           -> err 2 ["unknown noun"; n]
    end
  in
  exit status;;


D bindata.go => bindata.go +0 -258
@@ 1,258 0,0 @@
// Code generated by go-bindata.
// sources:
// doap.rdf
// openapi.yaml
// DO NOT EDIT!

package main

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

func bindataRead(data []byte, name string) ([]byte, error) {
	gz, err := gzip.NewReader(bytes.NewBuffer(data))
	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}

	var buf bytes.Buffer
	_, err = io.Copy(&buf, gz)
	clErr := gz.Close()

	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}
	if clErr != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

type asset struct {
	bytes []byte
	info  os.FileInfo
}

type bindataFileInfo struct {
	name    string
	size    int64
	mode    os.FileMode
	modTime time.Time
}

func (fi bindataFileInfo) Name() string {
	return fi.name
}
func (fi bindataFileInfo) Size() int64 {
	return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
	return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
	return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
	return false
}
func (fi bindataFileInfo) Sys() interface{} {
	return nil
}

var _doapRdf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\x94\x5f\x4e\xdc\x40\x0c\xc6\xdf\xf7\x14\x56\x78\xe0\x29\x19\xa0\x7d\x28\xab\xec\x22\xd4\xd2\x0a\xa9\xad\x10\x3d\x81\x37\x71\xb2\x2e\x33\xe3\xc8\x9e\xec\x2e\x47\xe2\x1c\x5c\xac\x0a\x59\x52\x15\x50\x85\xe0\x6d\x64\xcf\xf7\xf3\x67\xcf\x9f\xf2\x6c\x17\x3c\x6c\x48\x8d\x25\x2e\xb2\xe3\xe2\x28\x03\x8a\x95\xd4\x1c\xdb\x45\xd6\xa7\x26\xff\x94\x9d\x2d\x67\xa5\xd6\xcd\xfc\xfa\xcb\x57\xd8\x05\x1f\x6d\xae\x75\xb3\xc8\xd6\x29\x75\x73\xe7\xb6\xdb\x6d\xb1\xfd\x50\x88\xb6\xee\xf8\xf4\xf4\xd4\x1d\x9d\xb8\x93\x93\x5c\xeb\x26\xb7\xdb\x98\x70\x97\x47\x3b\xc8\x66\x00\xa3\x74\x92\xf5\x46\x4d\xef\x39\x56\x45\x25\xc1\x45\x73\xb5\x60\x77\x90\x2d\x67\x00\xe5\x95\xca\x6f\xaa\xd2\xb0\x06\x28\x57\x7d\x9b\xd7\x98\x70\x85\x46\x30\xf8\x50\x32\xe9\xb5\xa2\x91\x65\x73\xe7\x2a\xa9\xa9\x08\x2a\x45\xc4\x40\x2e\xa8\xb8\x8e\xe3\x4a\x50\xeb\x8f\xb6\x46\x54\xcf\x8e\xcd\x7a\xb2\xcc\xed\xa1\x6b\x09\xd4\x61\xfb\x0e\xe0\x84\xe2\xd0\x79\x0a\x14\x93\x3d\x81\x6d\x8e\x9d\x74\x14\xb1\xe3\xe2\x16\x83\x9f\x04\x9e\x2b\x8a\xef\xe9\xc5\xb4\x72\x01\x2d\x91\xba\xef\x97\x9f\x2f\x7e\xfe\xba\x98\xd8\x01\x39\x26\xe4\x48\xfa\x5a\xfc\x24\x1d\x02\xcb\xa7\xb5\x8a\xaa\xe5\xd2\x3d\xa4\xc6\x5d\x9d\x4a\xab\x18\x02\xc7\x36\xf7\x18\xdb\x1e\x5b\x5a\xb6\x32\x2c\x4b\xf7\x62\x72\xd4\x29\x75\x62\x9c\x44\x6f\xc7\x00\x40\xf9\x8d\xd3\xf5\xb3\xe8\x70\xe0\x2a\xdb\x77\x8c\xe7\xb1\xa1\x71\xd6\x52\x61\x62\x89\x6f\xa6\x15\x2d\xa7\xbf\xc4\xd2\xbd\x60\xba\x74\x4f\x9b\x2b\x6d\x2d\x9a\xf2\x9a\xac\x52\xee\x1e\xea\xef\x82\x9f\x0f\x23\x59\x64\x14\xb3\xe5\x0f\xbc\x21\x48\x6b\x02\x32\xa3\x98\x18\x3d\x58\xbf\x32\x4a\xd0\x88\x42\x27\x96\x38\xb6\x90\x04\x10\xf6\x3e\x00\x37\xc8\x1e\x57\x9e\x60\xc3\x38\xc4\xc7\xfd\xd2\x3c\x70\x1e\x6d\xbb\x9a\x3c\x57\x2c\xbd\xc1\xf9\xd5\x65\x51\xba\x67\x4e\x5e\xe1\xb0\xd1\x6c\x79\x4d\xb1\x56\x02\x4f\x60\xd2\x5b\x3e\x5c\xd8\x30\x14\xdf\x1b\x26\x0f\x9d\xf4\x0a\xfe\x10\x9b\x86\xab\xf5\xf0\x94\x6a\x8c\x06\x7d\x9c\x1c\xd7\x6c\x9d\x44\x7e\xb4\x3c\x64\xfe\x41\xd5\x04\xfe\xf0\xfc\xea\xf2\x25\xf3\x01\x53\xaf\x04\x94\xe0\xfe\xae\x53\xe9\x37\xf7\x77\xf4\x9f\x6e\xb6\x7c\xc3\x6f\x7f\x4f\x83\x7a\x3c\xe3\xd2\x4d\x7f\x4f\xe9\xf6\x7f\xde\x72\xf6\x27\x00\x00\xff\xff\xd6\xe9\xa4\xd0\x25\x05\x00\x00")

func doapRdfBytes() ([]byte, error) {
	return bindataRead(
		_doapRdf,
		"doap.rdf",
	)
}

func doapRdf() (*asset, error) {
	bytes, err := doapRdfBytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "doap.rdf", size: 1317, mode: os.FileMode(420), modTime: time.Unix(1554233711, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}

var _openapiYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x3a\xe9\x72\x1b\x39\x73\xff\xf9\x14\x5d\x4a\xaa\xb8\x5b\x45\x0e\x0f\x4b\xb2\xc5\x54\x2a\x51\xac\xdd\x2f\xf2\x67\x47\x2a\x59\xde\x5c\x95\x6c\x35\x07\x3d\x1c\x58\x33\xc0\x2c\x80\x11\x35\x55\x79\x9f\x3c\x48\x5e\x2c\xd5\xc0\x5c\xa4\x48\x9b\x96\xa5\xdd\x9d\x3f\x12\x71\xf4\x85\x46\x5f\xe8\xf1\x78\x3c\xd0\x05\x29\x2c\xe4\x02\x5e\x45\xd3\x68\x36\x90\x2a\xd1\x8b\x01\x80\x93\x2e\xa3\x05\x7c\x2c\x97\x96\x1c\xe8\x04\x5c\x4a\x70\x2d\xd5\x52\xa3\x11\x70\x7e\x7d\x39\x00\xb8\x27\x63\xa5\x56\x0b\x18\xce\x86\x03\x00\x41\x36\x36\xb2\x70\x7e\xe8\x7f\xc6\x03\x00\x80\xdb\xad\x5d\x20\x2d\x20\xac\xb1\x02\xa7\x41\x2a\x47\x06\x63\x07\x85\xd1\x2b\x83\x39\x3a\x19\x63\x96\x55\xb0\x96\x2e\x85\x4a\x97\x06\x96\x5a\xdf\xe5\x68\xee\xec\xc8\x83\x53\xda\x91\x05\x54\x02\xb4\x4b\xc9\x74\xa0\x05\x3a\x8c\x06\x7e\xcd\xbf\xa6\x64\xe8\x9e\x0c\x14\xda\x5a\xb9\xcc\xe8\x11\xe9\x50\x5a\xb2\x7e\xd4\x62\x4e\x60\x2b\xe5\xf0\xc1\x43\xcd\xc9\xa5\x5a\x80\xc2\x9c\xd1\x58\x0f\x8f\xd7\x5d\x50\x26\x63\xa9\x4b\x0b\xbf\xcc\x18\x42\x04\x1f\x89\x40\xc8\x24\x21\x43\x2a\x26\x0b\x89\xd1\x79\x6f\x59\xa2\x0d\x20\x24\x65\x96\x41\x26\x2d\x0b\xd0\xc3\x42\x43\x68\x61\xcd\x14\x7a\xb8\xe7\xd7\x97\x16\x84\xbc\x27\xb3\xa2\x9a\xfc\x9f\x4b\x57\x1a\x82\x84\x90\xff\x5a\xc8\xb1\x82\x25\xf9\xa9\x31\x60\xe9\xf4\x38\x91\x59\x06\x5a\xc1\xa4\xd0\xd6\xd9\xc9\x8a\x9c\x47\x57\xaa\x3b\xa5\xd7\x0a\x4a\x93\xd9\x7a\xb9\xd2\x0a\x3e\xdd\xbe\x05\x27\x73\x6a\xc6\x48\xc5\x99\xb6\x1e\xf4\x0f\x32\xc7\x15\xd9\x11\xb8\xb4\xcc\x97\x0a\x65\x66\x47\x80\xa5\x90\x7a\x04\xf7\x52\x90\xfe\x71\x00\x10\x6b\xe5\x30\x76\x0b\xbf\x9d\x72\x94\xd9\x02\x8a\x5a\x98\x63\x2c\xe4\x3f\xe6\x46\x47\x2c\xb0\x70\x40\x98\xd3\x02\x36\x86\x4a\x93\x2d\x20\x75\xae\xb0\x8b\xc9\x24\xd6\x82\xa2\x66\x7a\x92\x1b\x3d\x69\x60\x1d\xdb\x14\xd1\x64\x72\x40\x0f\x8e\x8c\xc2\xec\x42\xc7\x96\xb1\xfa\xfd\x47\x51\x34\xc1\xa5\x2e\xdd\xd1\xb6\xa2\x5d\x5c\x9d\x5f\xc3\xcd\xc5\xcf\x03\x4b\x86\x15\x72\x31\x18\x6f\xa2\x14\x94\xeb\x0e\x65\x8d\x65\x7c\x3f\x8d\x8e\x67\xcb\x47\xd8\xa3\x78\x25\x27\xf7\xb3\x83\x61\xcc\xa6\xd1\xfc\xbb\x80\xac\x74\xf7\xdf\x9e\x7d\x58\xc8\xa8\x41\x11\x49\xc5\x2b\x0a\x74\xa9\x17\xce\xd1\xa4\xbe\xbf\x51\x85\x79\x76\x14\x4e\x69\x45\xf5\x71\x01\x38\x5c\xd9\xe6\xff\x31\x60\x21\xeb\xff\x6d\x99\xe7\x68\xaa\x85\xbf\x0e\x3d\x79\xd6\xd3\xba\x20\x83\xfc\xfb\x52\x2c\xc0\xcb\xfd\x2a\xa0\xa9\xe7\x0d\xd9\x42\x2b\x4b\x2d\x6c\x80\xe1\x7c\x3a\x1d\x76\x3f\xb7\x4e\x69\x37\x1a\xfe\x58\xbf\x48\xb9\xfe\x4e\x00\x47\x0f\x6e\xf2\x30\x66\x9e\x36\x27\x00\x6c\x9c\x52\x8e\xdb\xa3\x00\xae\x2a\x68\x01\xd6\x19\xa9\x56\x5e\x30\xa5\x25\xc3\xb2\xfb\xd5\xe9\x3b\x52\x5f\x13\x0d\xaf\xde\x96\xcd\x0d\xb9\xd2\xa8\x60\x29\x78\x7e\x68\x3d\x1f\x1e\x1e\xfc\xc0\x77\x2e\xc7\x3b\xa9\x56\x7e\x94\x2d\x97\xf5\x96\x4b\x97\x0e\x10\x0a\xb4\x76\xad\x8d\xf8\x31\xda\x29\xd3\x42\xde\x32\x98\x7a\xae\x40\x83\x39\x39\xaf\xbe\x0d\x45\x47\x7f\x6b\x28\x39\x5a\xc0\xd1\xdf\x4c\x62\x9d\x17\x5a\x91\x72\x76\xd2\xad\x9c\x24\xda\xe4\xe8\xae\x79\xe0\xe8\xc9\xe7\xc2\x26\x25\x1d\xbb\x1e\x2d\x7b\x0f\x05\x8b\x22\x93\xb1\x67\x61\xf2\x70\xf8\xc1\xec\x58\x0a\xad\xa5\x30\x64\xcb\xcc\x7d\xed\x30\x37\x3f\x7a\xc0\xbc\x60\xe7\xf4\x6f\x57\x7f\x79\x73\xfa\xd3\xeb\x77\x97\xff\xfe\xe1\x72\x2f\xa5\x9f\xad\x56\x87\x92\x5a\x18\x3e\x24\x27\xfb\xf2\xeb\xbe\x40\xeb\xae\x99\xaf\xd1\x34\x3c\x99\xce\xf6\x9f\x41\xed\x2a\x9b\xef\x2d\x2a\xa5\x1d\x38\xca\x32\xc0\xa0\x6c\x11\x5c\xe6\xb8\x92\x8a\x00\xc1\x3a\x74\x94\x91\xb5\x90\xe0\xff\xfd\x2f\x0a\x82\x54\x0a\x56\x42\x84\xda\x90\xc0\x12\xe3\x3b\x52\x02\x5c\x8a\x0e\x52\xb4\xa0\x34\x48\x41\xc8\x6e\xdc\x96\x71\xca\x60\x53\xbf\x85\x7d\x69\xa7\xd3\xd1\x06\x1d\x57\x2a\xab\x40\x26\x5e\x41\x60\x8d\x96\xb7\xc6\x64\x6d\x52\x66\xa3\xe0\x7e\xd7\xd2\x12\xbb\x69\xbe\x55\x80\x70\x3c\x9d\x4d\x8e\xa7\xaf\xa2\x83\x2e\x77\x91\xa1\x3c\xf8\x5c\x0e\x53\x87\x93\xe9\xac\xbe\x9b\x9e\x64\x16\xa2\x2d\x8b\x42\x1b\x47\x62\xc4\x17\x18\xfe\xf9\xf6\xf6\x1a\xfe\x09\xad\x8c\x23\x6f\x22\x5a\x07\xfa\x35\xeb\xe0\x17\xee\x33\x0f\x5a\x11\xb0\x2d\xd0\x86\xc2\x42\x76\xce\x08\x56\xaa\x55\x46\x20\xb0\x82\x1c\x5d\xec\x05\xce\x96\x04\xcd\xaa\xcc\xf9\x2e\x47\x70\x99\xb4\xdc\x28\xcd\x31\x8c\x07\x54\x9a\x8c\xc3\xa4\x95\xbc\x27\x35\xaa\x47\x13\xc8\xb5\x75\x60\x28\x26\xe5\xda\xa8\x08\xd6\x1c\x0a\x2c\xbd\x75\x12\x8d\xdc\x37\x74\xeb\x32\xf1\x86\x89\x44\x88\xa9\x10\x2a\x72\x6d\xa4\xf0\xe9\xe6\xfd\xc8\x7b\x19\xf8\xcb\x4f\xb7\x9e\x36\x1f\xf5\xf9\x48\x68\x55\x92\xb5\x2d\x75\x2c\x8d\x10\xe2\xf0\xaa\x4f\x37\xef\x77\xd9\xb4\x15\xb9\x6b\x6d\xdd\x33\x9b\xb4\x71\x6d\x29\x1c\x76\x47\x2f\xd5\x02\x7e\x2b\xc9\x54\x83\x9d\x17\x2a\x91\x99\x23\x03\xcb\x0a\xca\x82\xe3\x4c\x97\x1a\x22\xcf\xc3\x60\xbf\xa6\xed\xa6\x2d\xac\xb3\x13\x87\xab\x6d\x82\x84\x3b\x94\x1e\xe3\x15\xa5\xb6\x1f\xb6\x3d\x3d\x12\xac\x28\x2e\x95\x96\x95\xe4\xc9\xa4\xb1\x82\x1c\x1d\x2e\xe2\xd2\x64\x5e\xbe\x57\x85\xdb\xe6\x28\x27\x87\x87\xf2\x24\x55\x9c\x95\x82\x0d\x52\x9c\xa2\x5a\x11\x08\x72\x14\xf3\x1c\x58\xb9\x52\x3e\x78\x05\xc9\xf7\x80\xa1\x02\x3a\x67\xe4\xb2\x74\xf4\x64\x36\x2b\xb2\xbf\x2a\xfd\x64\x47\x97\x68\xfd\x42\xfe\xed\xcb\x64\x7b\x83\x70\xd4\xb3\x36\x28\xc4\x93\xac\xcd\xb9\x10\x80\xad\xee\x44\x70\xde\x98\x91\x70\xb3\x6d\x8a\x82\x84\x37\xfd\x2b\xa3\x4b\x25\x38\xd7\x00\x43\xbf\x95\xd2\x74\xb6\x61\x33\x06\x11\xe2\x45\xee\xeb\xc1\x2a\xf8\xe8\x46\xed\x08\x10\xbf\xa2\x86\xb7\xde\x60\xd5\xe9\xa9\x74\x94\x47\x70\xcb\x17\x2a\x91\x94\x09\xb6\xa2\xa5\x4a\xb4\x71\xa5\x62\xa7\x59\x79\x3c\x02\x86\x3d\x10\xc3\xde\x79\x72\x44\xc7\x02\x5c\xa3\x11\x16\x98\x76\x74\x72\x29\x33\xe9\xea\x94\x94\x91\x88\x36\xc7\x0b\x49\x70\xa3\x8f\x41\xd0\x0b\x70\xa6\x7c\xba\x92\x7b\xfb\xbb\x2d\x16\x4e\x88\x94\x20\x71\xa8\x4c\x2e\xba\x1f\x9b\x92\x79\x1b\x3c\xc1\xb0\x01\x38\xfc\x12\xc7\x3d\x9a\x3d\xef\xbb\xf9\xfe\x66\x06\xe9\xe1\x91\xd9\xd9\x30\xcc\x5f\xe1\xed\x7d\x48\xa6\x6b\xc3\x3e\x9b\x4e\xff\x40\xb3\x1e\x1b\xf2\x77\xc9\xa7\xd7\x5e\x94\xde\x96\x77\x57\xf4\x82\x12\xf4\x16\xdf\x69\x88\x4b\x63\xd8\x75\xf3\xda\x08\x2e\xd0\x91\x75\x98\x17\xb6\x47\xaa\x0f\x20\x5c\x8a\x0a\x66\x53\xc8\xa5\x2a\x7d\xad\x23\x25\x14\x3e\x70\xf3\xe9\x6d\xc0\xd5\xb8\x7d\x43\x96\x5c\x1f\x7a\x58\xd4\x83\xc9\xcb\xbf\xcb\xb3\x30\x80\x6d\x11\x19\x2a\x32\x8c\xe9\x50\x39\xdd\x84\xe5\x80\xaa\x02\x7a\x90\xd6\x71\x24\xd4\x0b\x61\xfc\xc5\x92\x96\xc3\x8a\x56\x66\x7c\x75\x2b\xb2\xfd\x58\xf2\x92\x65\xe0\xb9\x55\x7a\x14\x44\xe0\x52\xa3\xd7\x1c\xc2\x92\x31\xda\x70\xb0\xda\x82\xf5\x88\x9e\xae\x18\x9b\xde\xa6\x61\xdc\xa6\x68\x0e\xbf\x87\x1f\xf0\x8e\x3a\x82\x8a\x72\x99\xc9\x78\x83\xc1\x61\x45\x76\x08\xa5\xf2\xd1\x3c\x27\x98\x3e\x58\x27\x85\xcb\xac\x87\x25\x54\xa0\x86\x16\xef\x09\x90\xcf\xbd\x29\x89\x71\xf8\x5e\x18\x79\x8f\x8e\x86\x61\xbb\x25\xc7\xc2\x1d\xb1\xe3\x5d\xa7\x32\x4e\x21\x46\xcb\x26\xcb\xa3\xec\x41\x64\xe4\x4a\x0f\x9f\x59\x3c\x4e\x1b\xc2\x6f\x10\x0f\xf3\xc0\xbc\xb5\x32\x42\xb6\xd7\x0c\x63\x53\x4c\xcf\x47\xe9\x21\x61\xc3\x6e\x78\xed\xce\xc9\x85\x56\xd4\x77\xe8\x82\x32\x72\xf4\x24\x9f\x7e\xe1\xb7\xf6\xdd\xfa\x2e\x3f\x1d\x10\xfc\x39\x5c\xf5\xf3\x0a\x90\x45\x74\x48\xfa\xd5\xb3\xf1\x8f\xb2\xaf\xcd\x2a\x6b\xbf\x56\xe3\x93\x16\xcc\xb4\x5a\x75\xde\x5b\x95\xf9\x92\x8c\x5f\xd8\xd6\x44\xeb\x2b\x56\xc1\x9a\xcc\x66\x2a\xb5\x9d\xde\xdc\x76\x74\xfc\x71\x15\x1b\xb6\x7b\x7d\x9f\xf7\x3b\x56\x6b\xb6\xf0\xd6\x92\xf8\x62\xd1\xc4\xe1\x6a\x77\xc5\x24\xe4\xf4\x68\x0c\x56\x3b\xe7\x39\x64\xd9\x09\xb1\xdb\xac\x97\x9f\x29\x7e\x5c\x3c\x3a\x84\xac\xf0\xc5\xba\xdc\x16\xdb\xe6\xb7\x47\x1a\xdd\xd7\xe6\x35\x5b\x81\xdf\x3e\x92\xa5\x72\xb4\xda\xf0\xd0\xdb\x5f\x57\xce\xd8\xbb\x68\xaf\x4c\x9f\x9d\xe8\x96\x98\x90\x3e\xd5\x37\xf6\x30\x8b\xb7\xeb\xd2\x36\x06\x4f\x75\x81\x80\xc3\xd5\x17\xac\xde\x6d\x9b\xf5\xff\xbe\xf5\x84\xe7\x8a\xe7\xbb\xf8\xf2\x05\x2c\xa7\x21\x66\xe5\x69\xc6\x93\x77\xf2\x39\x38\x5c\x8d\x40\x1b\x48\x34\xe7\x4b\x8e\x23\x07\xa7\x0f\x3a\xa0\x80\xfd\xe5\x0e\x48\x67\x07\x87\x12\x43\xa5\x59\x99\x7d\x99\x8d\x23\x06\xa5\x5d\x88\x7c\x2c\x29\x2b\x9d\xbc\xa7\xe1\x0b\x9e\x6c\x43\xb0\xa2\xf5\xa1\x04\xcb\x04\x28\x2f\x5c\x35\x62\x52\x7d\x65\xd0\xc7\xb3\x29\x16\x45\xaf\x0e\xff\x67\x56\xc2\x6e\x8a\xf7\x6e\x9f\x7d\xef\x58\x1b\xd0\x41\x46\x61\x62\xb0\x5b\x48\xdb\x8c\x7d\x99\xad\x00\x2a\x70\xd6\x84\x29\x9b\xc8\x4a\x93\xed\xc1\xb4\x53\xb2\xdf\x86\xfe\xd3\xcd\xfb\x4d\xdc\x57\x85\x3b\x14\xfd\x93\x30\x6d\x9d\x1d\x1f\x43\x03\x61\xeb\x49\x47\xb1\x64\xb2\x76\x43\xbd\xe8\x51\x94\xf0\x85\x08\x61\x57\x74\xb0\xc3\xad\xec\x7d\xc3\xd9\xef\x7c\x63\x2d\xe8\xb1\x77\x6a\xfc\xcc\xa3\x89\x31\x08\xad\x1e\x7b\xa8\x31\x58\x9d\x53\x7d\x73\x38\xf1\x5d\x1b\xbd\xe3\x69\x60\x8f\x27\x7c\xec\xff\x6a\x51\x87\xc5\x3d\xf7\xba\xe3\xe1\x61\x43\xd6\xa1\x0c\x31\x3f\x39\x81\x38\x45\x83\x31\xdf\x80\x08\x3e\x60\x15\x4c\x90\x56\x0e\xa5\x82\x58\xe7\x39\x5a\xb6\xb2\xeb\x54\x3a\xb2\x05\xc6\x14\xc1\x75\x46\x68\x3b\xd6\x96\x04\xb8\xc6\x50\x01\x70\x21\x76\x5d\xd2\x4a\x2a\x25\x9b\xf8\x15\xa1\x20\x23\x75\xa8\xec\x39\x43\xe8\x48\xf4\x72\x40\x5f\xb6\x77\x46\xae\xfa\xf1\x85\x2d\x28\x96\x98\xb5\x6b\x1c\xae\xc0\x52\x8e\xca\xc9\xb8\xce\xae\x3f\xdd\xbc\x3f\x94\x5d\xb4\x9c\x4b\x4a\x45\x02\x96\x15\xdc\xfc\xfc\x16\x5e\x9d\xbd\x39\x8d\x38\x26\xd5\x6b\x12\x41\x8c\x64\x3d\x81\xa9\x73\x45\x78\x5d\xb0\x23\xf8\x8c\xf7\x18\xe0\x8c\x5a\xd2\x72\x94\x99\xd3\x23\x48\x5c\xe1\x49\x4f\x64\x46\x91\x6f\x18\xf9\x88\x09\x1a\x39\xf6\xb4\x27\x32\x86\x84\x5a\xd8\xbe\x7f\xa4\xc6\xb6\x2c\x9d\x37\x9b\x7d\x11\xf6\xc4\x82\x60\x2b\xa5\x55\x95\xfb\xd2\x0c\x13\x12\xf8\x0d\x9d\x2d\xdf\x71\xc0\x90\x35\xba\xe6\xe8\xc1\x7d\x1b\xa4\xd3\x93\x93\x57\xa7\xdb\xb0\x22\x38\x57\x15\x9f\x83\x6d\x2b\x3b\xbe\xd1\x23\x93\xea\x4e\x26\x92\x04\xac\xd3\x9e\x5b\x10\xd2\x16\x19\x56\x4d\x9a\xd2\x54\x69\x0e\x25\x64\xd8\x76\x84\x38\xcc\x0b\xef\xf0\x7d\xc5\xd4\x9b\xd3\x05\xcc\xa7\xb3\xe9\x78\x36\x1f\xcf\x66\xb7\xb3\xb3\xc5\xf1\x9b\xc5\x74\xfe\x1f\x11\xfc\x82\x99\xf4\x5d\x36\x9d\xc2\x1a\x5f\xf6\x97\x16\xde\xa1\x82\xd9\x08\x66\x70\x7e\xc1\x2c\xbe\x43\x55\xa2\xa9\x78\x68\x3e\x9b\x4e\xe1\x07\x3e\x27\x4b\xe4\x9b\x77\x60\x49\x99\x5e\x87\x96\x02\x48\x42\xab\x4b\x4b\x8b\xfd\x31\x6a\xfc\x74\x1b\x78\x0e\x77\xd1\x33\x6c\x19\xff\x26\xa6\xfd\x53\xda\x7e\x7e\x23\xf8\xc8\x41\x51\xe0\x8b\x55\xbd\x16\x2c\x53\xdb\x45\x10\xbb\x28\x0b\x73\xa1\xd4\x70\x28\x45\x9c\x8d\x66\xd2\x91\xc1\xac\x5e\x55\xd7\x83\xb4\xe9\x57\x3c\x3a\x74\xcd\x58\x2e\x4e\x0e\xc5\xf1\x6a\xde\xa9\x1a\xa4\xf4\x80\x82\x62\xc9\xae\xe1\xc3\xc5\x09\xa4\x68\x53\xbf\xa9\xce\x49\x36\x81\x6e\x26\x2a\x5b\x4f\x3f\x7e\x2a\x48\xb2\x91\xd7\x34\x8a\xe6\xff\xfd\x6a\xde\xf3\xfe\xdf\x23\x88\xcf\x56\xab\x20\x89\x87\x3c\x7b\x24\x0a\x9e\x6d\x21\xf9\x32\xd1\x82\x4d\xbd\x1f\xf2\x75\x96\x4d\xd4\xfd\x1c\x73\x97\x63\x12\x1b\x29\xe0\x9e\xb7\xe6\x47\xae\x64\x7f\x12\x75\x88\xee\xc2\xe3\x34\xee\x19\x11\xaf\x69\x29\xe8\xbe\x9d\x29\x6d\x77\xba\xcf\x8c\xa9\xd7\x36\xb3\x95\xb0\xef\x4c\xd1\xb7\x50\x84\xf8\xa1\xe8\x2a\x5b\xfb\x22\x87\xd4\x50\xb2\xd5\x44\xb0\xaf\x25\x60\x87\xcf\xff\x52\xbe\xdb\x72\xc2\x0e\x62\x31\x99\xac\xd7\xeb\x28\xd5\x6b\xa7\x7d\x75\x9f\xa2\x58\x47\xe5\xdd\xc4\x95\x4e\x1b\x89\x59\x78\xc0\x20\x23\x2d\x45\x45\x5a\xfc\x83\xd0\xf9\xdf\xcf\x7a\xf0\xfa\x8a\xfd\xb2\xe4\xbe\xc3\x7b\xfc\xe8\x71\xc1\xc5\xd5\x07\x30\x54\x77\x22\x6e\x1c\x52\x78\xe6\x79\x61\x4a\x84\xce\x77\xa2\x67\xfb\xf2\xc2\xa8\xe3\xe9\xfc\xd5\x1b\x11\x4f\xe3\xe3\xe3\x64\xfa\x5a\x20\x09\x71\x86\xb3\x44\x9c\x2d\x97\x82\x68\xd9\xaf\xa1\xe7\xe4\xf0\x85\xa9\x39\x9b\x9f\x9d\x9c\xe1\xd9\x69\x22\x4e\xcf\x66\xc7\xa7\xf1\x49\x42\xaf\xe3\xa5\xa0\x53\x3a\x79\x3d\x9f\x26\xf3\xde\x2e\xdf\x3e\x63\x5f\x98\x9e\x93\x7e\x05\xe9\x51\xcd\xe8\x45\xf4\xa0\x8b\xf3\xb6\x6d\x50\x78\x8a\x7a\x61\x12\x38\x97\x3d\x19\xcf\x66\xe3\xf9\x9b\xdb\xe9\xc9\x62\x7e\xba\x98\x9e\x45\xd3\xe9\xd4\x1b\x5d\x4b\x71\x69\xa4\xab\x3e\x86\x20\x35\x20\xf0\xcd\x40\xe7\xa5\x4b\x37\x5d\x06\x1b\x83\x7e\xae\x46\x0b\x58\xf2\x4a\x3f\xc6\xcb\x7d\xf3\xde\x96\x9b\x29\xe4\x5f\xa9\xda\x93\xf1\x05\x4b\x87\xa5\x4b\x7f\xed\xf7\xda\xed\xeb\x03\x3b\x0f\x3d\x4c\xc4\x61\x7a\xfd\xc6\xe8\x1b\x9b\x7c\xf7\xb4\x4d\xb5\x71\xa0\x0b\xfc\xad\x24\x90\x82\x17\x25\xb2\xf3\xc7\xec\x7d\x5b\x40\x43\x36\xd0\x1e\xf9\xed\xd5\x5f\x7f\xfa\x97\x61\xdd\x64\xcc\xdf\x27\xcb\xf1\x67\x8c\x0a\x12\xe9\x9b\xc5\x48\x9a\x5e\x7f\xa3\x6f\x4d\xe1\xa1\xfa\x51\xc9\x42\x81\xab\x10\xa0\x57\x7e\x17\xe7\xd0\xd4\xb3\xdc\x08\x8a\xd6\x4d\x03\x96\xf3\xaf\x7d\x7c\xe4\x7f\x17\xc2\x2d\x1f\xd8\x4a\x75\xcf\x71\xa4\x4f\x44\x3c\xec\xc2\xd0\x7d\xf3\xa6\xdc\xf4\xa0\xf5\x84\x50\xf1\x5e\x23\xc6\x05\x1a\x57\x81\xe5\xdc\xa9\xdf\x72\x59\x53\xe0\xfb\xad\x96\x94\x62\x96\x80\x4e\xba\x4e\x6f\x66\xbd\x2b\x57\xfb\xee\x25\x54\xa0\x4b\x67\xa5\xa0\xe6\x3d\xf5\xc3\xa7\x8f\xb7\xbe\x37\xcc\x53\xb9\x25\xf4\xba\x1d\x5c\x2a\xeb\xc2\x2b\x6c\x97\x57\xb1\x3f\xa8\x1b\xba\xea\xe7\x86\xa6\xe1\x33\x82\x5f\xa4\xce\xd0\x69\xd3\x85\xf3\xcb\x4c\xc7\x77\x24\x02\x11\xa5\xad\x77\x76\x9c\x5e\x5f\x46\x83\x46\x3f\x17\x83\x71\x4f\xc5\xe0\x3f\xff\x6b\x30\xee\xa9\x29\xff\x0e\xf5\xbd\xa6\xec\x14\xba\x72\xb7\xbb\xa6\xb7\xfa\xae\xfb\xfd\xc3\x4d\x1b\xef\x66\x23\x45\x4a\xa0\x8d\x5c\x49\x85\xd9\x66\x9f\xff\xff\x07\x00\x00\xff\xff\x21\xda\xaa\x31\x25\x30\x00\x00")

func openapiYamlBytes() ([]byte, error) {
	return bindataRead(
		_openapiYaml,
		"openapi.yaml",
	)
}

func openapiYaml() (*asset, error) {
	bytes, err := openapiYamlBytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "openapi.yaml", size: 12325, mode: os.FileMode(420), modTime: time.Unix(1554236102, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}

// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	if f, ok := _bindata[cannonicalName]; ok {
		a, err := f()
		if err != nil {
			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
		}
		return a.bytes, nil
	}
	return nil, fmt.Errorf("Asset %s not found", name)
}

// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
	a, err := Asset(name)
	if err != nil {
		panic("asset: Asset(" + name + "): " + err.Error())
	}

	return a
}

// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	if f, ok := _bindata[cannonicalName]; ok {
		a, err := f()
		if err != nil {
			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
		}
		return a.info, nil
	}
	return nil, fmt.Errorf("AssetInfo %s not found", name)
}

// AssetNames returns the names of the assets.
func AssetNames() []string {
	names := make([]string, 0, len(_bindata))
	for name := range _bindata {
		names = append(names, name)
	}
	return names
}

// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
	"doap.rdf":     doapRdf,
	"openapi.yaml": openapiYaml,
}

// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
//     data/
//       foo.txt
//       img/
//         a.png
//         b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
	node := _bintree
	if len(name) != 0 {
		cannonicalName := strings.Replace(name, "\\", "/", -1)
		pathList := strings.Split(cannonicalName, "/")
		for _, p := range pathList {
			node = node.Children[p]
			if node == nil {
				return nil, fmt.Errorf("Asset %s not found", name)
			}
		}
	}
	if node.Func != nil {
		return nil, fmt.Errorf("Asset %s not found", name)
	}
	rv := make([]string, 0, len(node.Children))
	for childName := range node.Children {
		rv = append(rv, childName)
	}
	return rv, nil
}

type bintree struct {
	Func     func() (*asset, error)
	Children map[string]*bintree
}

var _bintree = &bintree{nil, map[string]*bintree{
	"doap.rdf":     &bintree{doapRdf, map[string]*bintree{}},
	"openapi.yaml": &bintree{openapiYaml, map[string]*bintree{}},
}}

// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
	data, err := Asset(name)
	if err != nil {
		return err
	}
	info, err := AssetInfo(name)
	if err != nil {
		return err
	}
	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
	if err != nil {
		return err
	}
	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
	if err != nil {
		return err
	}
	return nil
}

// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
	children, err := AssetDir(name)
	// File
	if err != nil {
		return RestoreAsset(dir, name)
	}
	// Dir
	for _, child := range children {
		err = RestoreAssets(dir, filepath.Join(name, child))
		if err != nil {
			return err
		}
	}
	return nil
}

func _filePath(dir, name string) string {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

D build.sh => build.sh +0 -68
@@ 1,68 0,0 @@
#!/bin/sh
# https://golang.org/doc/install/source#environment
#

cd "$(dirname "${0}")" || exit 1
# $ uname -s -m
# Darwin x86_64
# Linux x86_64
# Linux armv6l

say="say"
parm="" # "-u"
{
  "${say}" "go get"
    go get ${parm} \
    github.com/jteeuwen/go-bindata/... \
    golang.org/x/net/html \
    \
    github.com/stretchr/testify \
    github.com/yhat/scrape
}

"$(go env GOPATH)/bin/go-bindata" doap.rdf openapi.yaml

PROG_NAME="pinboard4shaarli"
VERSION="$(grep -F 'version = ' version.go | cut -d \" -f 2)"

rm "${PROG_NAME}"-*-"${VERSION}" 2>/dev/null

"${say}" "test"
umask 0022
go fmt && go vet && go test --short || { exit $?; }
"${say}" "ok"

"${say}" "build localhost"
go build -ldflags "-s -w -X main.GitSHA1=$(git rev-parse --short HEAD)" -o ~/"public_html/b/${PROG_NAME}.cgi" || { echo "Aua" 1>&2 && exit 1; }
"${say}" "ok"
ls -l ~/"public_html/b/${PROG_NAME}.cgi"
# open "http://localhost/~$(whoami)/b/pinboard.cgi"

"${say}" bench
go test -bench=.
"${say}" ok

"${say}" "linux build"
# http://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5
env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.GitSHA1=$(git rev-parse --short HEAD)" -o "${PROG_NAME}-linux-amd64-${VERSION}" || { echo "Aua" 1>&2 && exit 1; }
# env GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w -X main.GitSHA1=$(git rev-parse --short HEAD)" -o "${PROG_NAME}-linux-arm-${VERSION}" || { echo "Aua" 1>&2 && exit 1; }
# env GOOS=linux GOARCH=386 GO386=387 go build -o "${PROG_NAME}-linux-386-${VERSION}" # https://github.com/golang/go/issues/11631
# env GOOS=darwin GOARCH=amd64 go build -o "${PROG_NAME}-darwin-amd64-${VERSION}"

"${say}" "simply"
# scp "ServerInfo.cgi" simply:/var/www/lighttpd/h4u.r-2.eu/public_html/"info.cgi"
gzip --force --best "${PROG_NAME}"-*-"${VERSION}" \
&& chmod a-x "${PROG_NAME}"-*-"${VERSION}.gz" \
&& rsync -vp --bwlimit=1234 "${PROG_NAME}"-*-"${VERSION}.gz" "simply:/tmp/" \
&& ssh simply "sh -c 'cd /var/www/lighttpd/demo.mro.name/ && gunzip < /tmp/${PROG_NAME}-linux-amd64-${VERSION}.gz > ${PROG_NAME}.cgi && chmod a+x ${PROG_NAME}.cgi && ls -l ${PROG_NAME}?cgi*'"

# ssh simply "sh -c 'cd /var/www/lighttpd/b.mro.name/public_html/u/ && cp /var/www/lighttpd/l.mro.name/public_html/pinboard?cgi* . && ls -l pinboard?cgi*'"
"${say}" "ok"

exit 0

"${say}" "vario"
# scp "ServerInfo.cgi" vario:~/mro.name/webroot/b/"info.cgi"
ssh vario "sh -c 'cd ~/mro.name/webroot/b/ && curl -L http://purl.mro.name/${PROG_NAME}_cgi.gz | tee ${PROG_NAME}_cgi.gz | gunzip > ${PROG_NAME}.cgi && chmod a+x ${PROG_NAME}.cgi && ls -l ${PROG_NAME}?cgi*'"
"${say}" "ok"


M doap.rdf => doap.rdf +5 -5
@@ 2,8 2,8 @@
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns="http://usefulinc.com/ns/doap#">
  <Project>
    <bug-database rdf:resource="https://code.mro.name/mro/pin4sha/issues"/>
    <homepage rdf:resource="https://code.mro.name/mro/pin4sha/"/>
    <bug-database rdf:resource="https://code.mro.name/mro/pin4sha.cgi/issues"/>
    <homepage rdf:resource="https://code.mro.name/mro/pin4sha.cgi/"/>
		<implements rdf:resource="pinboard.in/v1/openapi.yaml"/>
    <license rdf:resource="LICENSE"/>
    <maintainer rdf:resource="https://code.mro.name/mro"/>


@@ 12,12 12,12 @@
    <programming-language>ocaml</programming-language>
    <repository>
      <GitRepository>
        <browse rdf:resource="https://code.mro.name/mro/pin4sha"/>
        <location rdf:resource="https://code.mro.name/mro/pin4sha.git"/>
        <browse rdf:resource="https://code.mro.name/mro/pin4sha.cgi"/>
        <location rdf:resource="https://code.mro.name/mro/pin4sha.cgi.git"/>
      </GitRepository>
    </repository>
    <short-description xml:lang="en">Make the essential subset for posting to a shaarli available via a subset of the pinboard/delicious API.</short-description>
    <short-description xml:lang="fr">Rendre le sous-ensemble essentiel pour l'affichage dans un shaarli disponible via un sous-ensemble de l'API pinboard/delicious mature et Γ©prouvΓ©e.</short-description>
    <wiki rdf:resource="https://code.mro.name/mro/pin4sha/wiki"/>
    <wiki rdf:resource="https://code.mro.name/mro/pin4sha.cgi/wiki"/>
  </Project>
</rdf:RDF>

A lib/cgi.ml => lib/cgi.ml +12 -0
@@ 0,0 1,12 @@

let http_request_method = "REQUEST_METHOD"

(* https://github.com/rixed/ocaml-cgi/blob/master/cgi.ml#L169 *)
let getenv_safe ?default s =
  try
    Sys.getenv s
  with Not_found ->
    match default with
    | Some d -> d
    | None   -> failwith ("Cgi: the environment variable " ^ s ^ " is not set")


A lib/datetime.ml => lib/datetime.ml +23 -0
@@ 0,0 1,23 @@

type year  = Year  of int
type month = Month of int
type day   = Day   of int
type date  = year * month * day

let create_date y m d =
  (Year y, Month m, Day d)


type hour   = Hour   of int
type minute = Minute of int
type second = Second of int
type time   = hour * minute * second

let create_time h m s =
  (Hour h, Minute m, Second s)


type t = date * time

let create y m d hh mm ss =
  (create_date y m d, create_time hh mm ss)

A lib/dune => lib/dune +16 -0
@@ 0,0 1,16 @@
(library
  (name Lib)
  (libraries
    tyre lambdasoup
  )
)

;(rule
;   (target version.ml)
;   (action
;    (with-stdout-to %{target}
;    (echo "let git_sha = \"foo")
;     (run git rev-parse --short HEAD)
;    (echo "bar\"")
;   ))
;  )

A lib/name.ml => lib/name.ml +135 -0
@@ 0,0 1,135 @@

(*
 * What is in a name?
 *
 * /some/dir/2019-12-31-173519-MyFooBar_--_sometag_anothertag.a.b.gz
 * |--dirs--||---datetime----| |title-|    |-tag-| |--tag---||exts-|
 *
 * dirs     ^([^/]*/)*
 * datetime ((\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})-)?
 * title    (.*?)
 * tags     (_--(_[^_.]+)* )?
 * ext      (\.[^.]* )*$
 *)

type dir   = Dir   of string
type title = Title of string
type tag   = Tag   of string
type ext   = Ext   of string

(* A tuple would suffice because everything is semantically strict typed. *)
type parsed_name = {
  dirs     : dir list ;
  datetime : Datetime.t option ;
  title    : title ;
  tags     : tag list ;
  exts     : ext list ;
}

(* https://caml.inria.fr/pub/docs/manual-ocaml/libref/Sys.html *)

module P = struct
  open Tyre

  let dir' =
    let to_ s = Dir s
    and of_ (Dir o) = o in
    conv to_ of_ (pcre "[^/]*/")

  let dirs' =
    list dir'

  let datetime =
    let to_ (((((dy,dm),dd),th),tm),ts) = Datetime.create
      (int_of_string dy)
      (int_of_string dm)
      (int_of_string dd)
      (int_of_string th)
      (int_of_string tm)
      (int_of_string ts)
    and of_ ((dy,dm,dd),(th,tm,ts)) =
      let dy' = match dy with Datetime.Year   x -> string_of_int x
      and dm' = match dm with Datetime.Month  x -> string_of_int x
      and dd' = match dd with Datetime.Day    x -> string_of_int x
      and th' = match th with Datetime.Hour   x -> string_of_int x
      and tm' = match tm with Datetime.Minute x -> string_of_int x
      and ts' = match ts with Datetime.Second x -> string_of_int x in
      (((((dy',dm'),dd'),th'),tm'),ts') in
    conv to_ of_ (
      pcre "[0-9]{4}" <* char '-' <&>
      pcre "01|02|03|04|05|06|07|08|09|10|11|12" <* char '-' <&>
      pcre "[0-3][0-9]" <* char '-' <&>
      pcre "[0-2][0-9]" <&>
      pcre "[0-5][0-9]" <&>
      pcre "[0-5][0-9]"
    )

  (* brings the trailing - *)
  let string_of_datetime ((dy,dm,dd),(th,tm,ts)) =
    let dy' = match dy with Datetime.Year   x -> x
    and dm' = match dm with Datetime.Month  x -> x
    and dd' = match dd with Datetime.Day    x -> x
    and th' = match th with Datetime.Hour   x -> x
    and tm' = match tm with Datetime.Minute x -> x
    and ts' = match ts with Datetime.Second x -> x
    in Format.sprintf "%04d-%02d-%02d-%02d%02d%02d-" dy' dm' dd' th' tm' ts'

  let tit' =
    let to_ s = Title s
    and of_ (Title o) = o in
    conv to_ of_ (pcre "[^/]*?")

  let tag' =
    let to_ s = Tag s
    and of_ (Tag o) = o in
    conv to_ of_ (pcre "[^_.]+")

  let sep' = "_"
  let sep = "_--"

  let tags' =
    str sep *> list ( str sep' *> tag' )

  let ext' =
    let to_ s = Ext s
    and of_ (Ext o) = o in
    conv to_ of_ (pcre "[.][^.]*")

  let exts' =
    list ext'

  (* https://gabriel.radanne.net/papers/tyre/tyre_paper.pdf#page=9 *)
  let full =
    let to_ (dirs, (datetime, ((title, ta), exts))) =
      let tags = match ta with None -> [] | Some t -> t in
      {dirs; datetime; title; tags; exts}
    and of_ {dirs; datetime; title; tags; exts} =
      let ta = match tags with [] -> None | t -> Some t in
      (dirs, (datetime, ((title, ta), exts)))
    in
    conv to_ of_ (dirs' <&> (opt (datetime <* char '-') <&> (tit' <&> opt tags' <&> exts') <* stop))

  let full' = compile full
end

let parse str : parsed_name =
  match Tyre.exec P.full' str with
  | Error _ -> failwith "gibt's nicht."
  | Ok n -> n

let unparse p : string =
  (* Tyre.eval P.full p *)
  let dt = match p.datetime with
  | None -> ""
  | Some dt -> P.string_of_datetime dt
  and tagpart = match p.tags with
  | [] -> ""
  | t -> t |> List.map (function Tag o -> o) |> List.cons P.sep |> String.concat P.sep'
  in p.exts
  |> List.map (function Ext o -> o)
  |> List.cons tagpart
  |> List.cons (match p.title with Title t -> t)
  |> List.cons dt
  |> List.append (p.dirs |> List.map (function Dir o -> o))
  |> String.concat ""


A lib/pinboard.ml => lib/pinboard.ml +1 -0
@@ 0,0 1,1 @@


A lib/url.ml => lib/url.ml +104 -0
@@ 0,0 1,104 @@

(*
 * https://tools.ietf.org/html/rfc1738
 *)

type scheme = Scheme of string
type uid    = Uid    of string
type pwd    = Pwd    of string
type host   = Host   of string
type port   = Port   of string
type path   = Path   of string
type name   = Name   of string
type value  = Value  of string

type par    = {
  name     : name ;
  value    : value ;
} 

type t = {
  scheme   : scheme ;
  uid      : uid ;
  pwd      : pwd ;
  host     : host ;
  port     : port ;
  path     : path ;
  query    : par list ;
  (* no fragment *)
}

module P = struct
  open Tyre

  let scheme' =
    let to_ s = Scheme s
    and of_ (Scheme o) = o in
    conv to_ of_ (pcre "https?")

  let uid' =
    let to_ s = Uid s
    and of_ (Uid o) = o in
    conv to_ of_ (pcre "[^:]*")

  let pwd' =
    let to_ s = Pwd s
    and of_ (Pwd o) = o in
    conv to_ of_ (pcre "[^@]*")

  let host' =
    let to_ s = Host s
    and of_ (Host o) = o in
    conv to_ of_ (pcre "[^:/?]*")

  let port' =
    let to_ s = Port s
    and of_ (Port o) = o in
    conv to_ of_ (pcre "[0-9]+")

  let path' =
    let to_ s = Path s
    and of_ (Path o) = o in
    conv to_ of_ (pcre "[^?&]*")

  let name' =
    let to_ s = Name s
    and of_ (Name o) = o in
    conv to_ of_ (pcre "[^=&]+")

  let value' =
    let to_ s = Value s
    and of_ (Value o) = o in
    conv to_ of_ (pcre "[^&]*")

  let par' =
    let to_ (name, value) = {name; value}
    and of_ {name; value} = (name, value)
    in
    conv to_ of_ (str "&" *> name' <&> str "=" *> value')

  let query' =
    list par'

  (* https://gabriel.radanne.net/papers/tyre/tyre_paper.pdf#page=9 *)
  let full =
    let to_ ((scheme, ((uid, pwd), (host, port))), (path, query)) =
      {scheme; uid; pwd; host; port; path; query}
    and of_ {scheme; uid; pwd; host; port; path; query} =
      ((scheme, ((uid, pwd), (host, port))), (path, query))
    in
    conv to_ of_ (
      (scheme' <* char ':' <* str "//" <&>
       ((uid' <* char ':' <&> pwd' <* char '@') <&>
       (host' <* char ':' <&> port'))) <&>
       (path' <&> query') <*
       stop)

  let full' = compile full
end

let parse str : t =
  match Tyre.exec P.full' str with
  | Error _ -> failwith "gibt's nicht."
  | Ok n -> n


A lib/version.ml => lib/version.ml +1 -0
@@ 0,0 1,1 @@
let git_sha = "8c0db45"

D model.go => model.go +0 -48
@@ 1,48 0,0 @@
//
// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/pinboard4shaarli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

package main

import (
	"encoding/xml"
)

type Result struct {
	XMLName xml.Name `xml:"result" json:"-"`
	Code    string   `xml:"code,attr,omitempty" json:"result_code"`
}

type Post struct {
	Href        string `xml:"href,attr" json:"href"`
	Description string `xml:"description,attr" json:"description"`
	Extended    string `xml:"extended,attr" json:"extended"`
	Hash        string `xml:"hash,attr" json:"hash"`
	Meta        string `xml:"meta,attr,omitempty" json:"meta"`
	Others      int    `xml:"others,attr" json:"others"`
	Tag         string `xml:"tag,attr" json:"tag"`
	Time        string `xml:"time,attr" json:"time"`
	Shared      string `xml:"shared,attr,omitempty" json:"shared,omitempty"`
	Toread      string `xml:"toread,attr,omitempty" json:"toread,omitempty"`
}

type Posts struct {
	XMLName xml.Name `xml:"posts" json:"-"`
	User    string   `xml:"user,attr" json:"user"`
	Dt      string   `xml:"dt,attr" json:"date"`
	Tag     string   `xml:"tag,attr,omitempty" json:"tag,omitempty"`
	Posts   []Post   `xml:"post" json:"posts"`
}

D model_test.go => model_test.go +0 -74
@@ 1,74 0,0 @@
//
// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/pinboard4shaarli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"encoding/xml"

	"github.com/stretchr/testify/assert"
	"testing"
)

// api_token {"result":"692D3D4BD4A2825D5A4A"}
// add {"result_code":"item already exists"}
// {"result_code":"done"}

// get {"date":"2018-04-09T19:26:54Z","user":"mro","posts":[]}

func xm(o interface{}) string {
	var b bytes.Buffer
	w := bufio.NewWriter(&b)
	enc := xml.NewEncoder(w)
	enc.Encode(o)
	enc.Flush()
	return string(b.Bytes())
}

func js(o interface{}) string {
	var b bytes.Buffer
	w := bufio.NewWriter(&b)
	enc := json.NewEncoder(w)
	enc.Encode(o)
	w.Flush()
	return string(b.Bytes())
}

func TestXmlEncodeResult(t *testing.T) {
	t.Parallel()
	r := Result{Code: "done"}
	assert.Equal(t, "<result code=\"done\"></result>", xm(r), "ach")
	assert.Equal(t, "{\"result_code\":\"done\"}\n", js(r), "ach")
}

func TestXmlEncodePosts(t *testing.T) {
	t.Parallel()

	assert.Equal(t, "<posts user=\"\" dt=\"\"><post href=\"uhu\" description=\"\" extended=\"\" hash=\"\" others=\"0\" tag=\"\" time=\"\"></post><post href=\"aha\" description=\"\" extended=\"\" hash=\"\" others=\"0\" tag=\"\" time=\"\"></post></posts>",
		xm(Posts{Posts: []Post{
			Post{Href: "uhu"},
			Post{Href: "aha"},
		}}), "ach")
	assert.Equal(t, "{\"user\":\"\",\"date\":\"\",\"posts\":[{\"href\":\"uhu\",\"description\":\"\",\"extended\":\"\",\"hash\":\"\",\"meta\":\"\",\"others\":0,\"tag\":\"\",\"time\":\"\"},{\"href\":\"aha\",\"description\":\"\",\"extended\":\"\",\"hash\":\"\",\"meta\":\"\",\"others\":0,\"tag\":\"\",\"time\":\"\"}]}\n",
		js(Posts{Posts: []Post{
			Post{Href: "uhu"},
			Post{Href: "aha"},
		}}), "ach")
}

D pinboard.go => pinboard.go +0 -495
@@ 1,495 0,0 @@
//
// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/pinboard4shaarli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

package main

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/cgi"
	"net/http/cookiejar"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/yhat/scrape"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
	// "golang.org/x/net/html/charset"
	"golang.org/x/net/publicsuffix"
)

const (
	ShaarliDate = "20060102_150405"
	IsoDate     = "2006-01-02"
)

var GitSHA1 = "Please set -ldflags \"-X main.GitSHA1=$(git rev-parse --short HEAD)\"" // https://medium.com/@joshroppo/setting-go-1-5-variables-at-compile-time-for-versioning-5b30a965d33e

// even cooler: https://stackoverflow.com/a/8363629
//
// inspired by // https://coderwall.com/p/cp5fya/measuring-execution-time-in-go
func trace(name string) (string, time.Time) { return name, time.Now() }
func un(name string, start time.Time)       { log.Printf("%s took %s", name, time.Since(start)) }

func main() {
	if cli() {
		return
	}

	if true {
		// lighttpd doesn't seem to like more than one (per-vhost) server.breakagelog
		log.SetOutput(os.Stderr)
	} else { // log to custom logfile rather than stderr (may not be reachable on shared hosting)
	}

	// - http.StripPrefix (and just keep PATH_INFO as Request.URL.path)
	// - route
	// - authenticate
	// - extract parameters
	// - call api backend method
	// - build response

	h := handleMux(os.Getenv("PATH_INFO"))
	if err := cgi.Serve(http.TimeoutHandler(h, 5*time.Second, "🐌")); err != nil {
		log.Fatal(err)
	}
}

/// $ ./pinboard4shaarli.cgi --help | -h | -?
/// $ ./pinboard4shaarli.cgi https://demo.shaarli.org/pinboard4shaarli.cgi/v1/about
/// $ ./pinboard4shaarli.cgi 'https://uid:pwd@demo.shaarli.org/pinboard4shaarli.cgi/v1/posts/get?url=http://m.heise.de/12'
/// $ ./pinboard4shaarli.cgi 'https://uid:pwd@demo.shaarli.org/pinboard4shaarli.cgi/v1/posts/add?url=http://m.heise.de/12&description=foo'
/// todo
/// $ ./pinboard4shaarli.cgi https://uid:pwd@demo.shaarli.org/pinboard4shaarli.cgi/v1/user/api_token
/// $ ./pinboard4shaarli.cgi --data-urlencode auth_token=uid:XYZUUU --data-urlencode url=https://m.heise.de/foo https://demo.shaarli.org/pinboard4shaarli.cgi/v1/posts/get
///
func cli() bool {
	// test if we're running cli
	if len(os.Args) == 1 {
		return false
	}

	for i, a := range os.Args[2:] {
		fmt.Fprintf(os.Stderr, "  %d: %s\n", i, a)
	}

	// todo?: add parameters

	if req, err := http.NewRequest(http.MethodGet, os.Args[1], nil); err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
	} else {
		usr := req.URL.User
		if pwd, isset := usr.Password(); isset {
			req.SetBasicAuth(usr.Username(), pwd)
		}
		bin := filepath.Base(os.Args[0])
		str := req.URL.Path
		idx := strings.LastIndex(str, bin)
		pi := str[idx+len(bin):]
		handleMux(pi)(reqWri{r: req, f: os.Stderr, h: http.Header{}}, req)
	}

	return true
}

type reqWri struct {
	r *http.Request
	f io.Writer
	h http.Header
}

func (w reqWri) Header() http.Header {
	return w.h
}
func (w reqWri) Write(b []byte) (int, error) {
	return w.f.Write(b)
}
func (w reqWri) WriteHeader(statusCode int) {
	const LF = "\r\n"
	fmt.Fprintf(w.f, "%s %d %s"+LF, w.r.Proto, statusCode, http.StatusText(statusCode))
	for k, v := range w.Header() {
		fmt.Fprintf(w.f, "%s: %s"+LF, k, strings.Join(v, " "))
	}
	fmt.Fprintf(w.f, LF)
}

// https://pinboard.in/api
func handleMux(path_info string) http.HandlerFunc {
	agent := strings.Join([]string{"https://code.mro.name/mro/pinboard4shaarli", "#", version, "+", GitSHA1}, "")
	// https://stackoverflow.com/a/18414432
	options := cookiejar.Options{PublicSuffixList: publicsuffix.List}
	
	return func(w http.ResponseWriter, r *http.Request) {
		defer un(trace(strings.Join([]string{"v", version, "+", GitSHA1, " ", r.RemoteAddr, " ", r.Method, " ", r.URL.String()}, "")))

		w.Header().Set(http.CanonicalHeaderKey("X-Powered-By"), agent)
		w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/xml; charset=utf-8")

		if http.MethodGet != r.Method {
			w.Header().Set(http.CanonicalHeaderKey("Allow"), http.MethodGet)
			http.Error(w, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http.StatusMethodNotAllowed)
			return
		}

		jar, err := cookiejar.New(&options)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		client := http.Client{Jar: jar, Timeout: 2 * time.Second}

		asset := func(name, mime string) {
			w.Header().Set(http.CanonicalHeaderKey("Content-Type"), mime)
			if b, err := Asset(name); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			} else {
				w.Write(b)
			}
		}

		base := *r.URL
		base.Path = path.Join(base.Path[0:len(base.Path)-len(path_info)], "..", "index.php")

		switch path_info {
		case "":
			http.Redirect(w, r, "about", http.StatusFound)
			return
		case "/about":
			asset("doap.rdf", "application/rdf+xml")
			return
		case "/v1":
			http.Redirect(w, r, "v1/openapi.yaml", http.StatusFound)
			return
		case "/v1/openapi.yaml":
			asset("openapi.yaml", "text/x-yaml; charset=utf-8")
			return

			// now comes the /real/ API
		case
			"/v1/posts/get":
			// pretend to add, but don't actually do it, but return the form preset values.
			uid, pwd, ok := r.BasicAuth()
			if !ok {
				http.Error(w, "Basic Pre-Authentication required.", http.StatusForbidden)
				return
			}

			params := r.URL.Query()
			if 1 != len(params["url"]) {
				http.Error(w, "Required parameter missing: url", http.StatusBadRequest)
				return
			}
			p_url := params["url"][0]

			/*
				if 1 != len(params["description"]) {
					http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
					return
				}
				p_description := params["description"][0]

				p_extended := ""
				if 1 == len(params["extended"]) {
					p_extended = params["extended"][0]
				}

				p_tags := ""
				if 1 == len(params["tags"]) {
					p_tags = params["tags"][0]
				}
			*/

			v := url.Values{}
			v.Set("post", p_url)
			base.RawQuery = v.Encode()

			req, err := http.NewRequest(http.MethodGet, base.String(), nil)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			req.Header.Set(http.CanonicalHeaderKey("User-Agent"), agent)
			resp, err := client.Do(req)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			formLogi, err := formValuesFromReader(resp.Body, "loginform")
			resp.Body.Close()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			formLogi.Set("login", uid)
			formLogi.Set("password", pwd)

			req, err = http.NewRequest(http.MethodPost, resp.Request.URL.String(), bytes.NewReader([]byte(formLogi.Encode())))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			req.Header.Set(http.CanonicalHeaderKey("Content-Type"), "application/x-www-form-urlencoded")
			req.Header.Set(http.CanonicalHeaderKey("User-Agent"), agent)
			resp, err = client.Do(req)
			// resp, err = client.PostForm(resp.Request.URL.String(), formLogi)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}

			formLink, err := formValuesFromReader(resp.Body, "linkform")
			resp.Body.Close()
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			// if we do not have a linkform, auth must have failed.
			if 0 == len(formLink) {
				http.Error(w, "Authentication failed", http.StatusForbidden)
				return
			}

			fv := func(s string) string { return formLink.Get(s) }

			tim, err := time.ParseInLocation(ShaarliDate, fv("lf_linkdate"), time.Local) // can we do any better?
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}

			w.Write([]byte(xml.Header))
			pp := Posts{
				User: uid,
				Dt:   tim.Format(IsoDate),
				Tag:  fv("lf_tags"),
				Posts: []Post{
					Post{
						Href:        fv("lf_url"),
						Hash:        fv("lf_linkdate"),
						Description: fv("lf_title"),
						Extended:    fv("lf_description"),
						Tag:         fv("lf_tags"),
						Time:        tim.Format(time.RFC3339),
					},
				},
			}
			enc := xml.NewEncoder(w)
			enc.Encode(pp)
			enc.Flush()

			return
		case
			"/v1/posts/add":
			// extract parameters
			// agent := r.Header.Get("User-Agent")
			shared := true

			uid, pwd, ok := r.BasicAuth()
			if !ok {
				http.Error(w, "Basic Pre-Authentication required.", http.StatusForbidden)
				return
			}

			params := r.URL.Query()
			if 1 != len(params["url"]) {
				http.Error(w, "Required parameter missing: url", http.StatusBadRequest)
				return
			}
			p_url := params["url"][0]

			if 1 != len(params["description"]) {
				http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
				return
			}
			p_description := params["description"][0]

			p_extended := ""
			if 1 == len(params["extended"]) {
				p_extended = params["extended"][0]
			}

			p_tags := ""
			if 1 == len(params["tags"]) {
				p_tags = params["tags"][0]
			}

			v := url.Values{}
			v.Set("post", p_url)
			v.Set("title", p_description)
			base.RawQuery = v.Encode()

			req, err := http.NewRequest(http.MethodGet, base.String(), nil)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			req.Header.Set(http.CanonicalHeaderKey("User-Agent"), agent)
			resp, err := client.Do(req)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			formLogi, err := formValuesFromReader(resp.Body, "loginform")
			resp.Body.Close()
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			formLogi.Set("login", uid)
			formLogi.Set("password", pwd)

			req, err = http.NewRequest(http.MethodPost, resp.Request.URL.String(), bytes.NewReader([]byte(formLogi.Encode())))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			req.Header.Set(http.CanonicalHeaderKey("Content-Type"), "application/x-www-form-urlencoded")
			req.Header.Set(http.CanonicalHeaderKey("User-Agent"), agent)
			resp, err = client.Do(req)
			// resp, err = client.PostForm(resp.Request.URL.String(), formLogi)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}

			formLink, err := formValuesFromReader(resp.Body, "linkform")
			resp.Body.Close()
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			// if we do not have a linkform, auth must have failed.
			if 0 == len(formLink) {
				http.Error(w, "Authentication failed", http.StatusForbidden)
				return
			}

			// formLink.Set("lf_linkdate", ShaarliDate)
			// formLink.Set("lf_url", p_url)
			// formLink.Set("lf_title", p_description)
			formLink.Set("lf_description", p_extended)
			formLink.Set("lf_tags", p_tags)
			if shared {
				formLink.Del("lf_private")
			} else {
				formLink.Set("lf_private", "lf_private")
			}

			req, err = http.NewRequest(http.MethodPost, resp.Request.URL.String(), bytes.NewReader([]byte(formLink.Encode())))
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			req.Header.Set(http.CanonicalHeaderKey("Content-Type"), "application/x-www-form-urlencoded")
			req.Header.Set(http.CanonicalHeaderKey("User-Agent"), agent)
			resp, err = client.Do(req)
			// resp, err = client.PostForm(resp.Request.URL.String(), formLink)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			resp.Body.Close()

			w.Write([]byte(xml.Header))
			pp := Result{Code: "done"}
			enc := xml.NewEncoder(w)
			enc.Encode(pp)
			enc.Flush()

			return
		case
			"/v1/posts/delete":
			_, _, ok := r.BasicAuth()
			if !ok {
				http.Error(w, "Basic Pre-Authentication required.", http.StatusUnauthorized)
				return
			}

			params := r.URL.Query()
			if 1 != len(params["url"]) {
				http.Error(w, "Required parameter missing: url", http.StatusBadRequest)
				return
			}
			// p_url := params["url"][0]

			w.Write([]byte(xml.Header))
			pp := Result{Code: "not implemented yet"}
			enc := xml.NewEncoder(w)
			enc.Encode(pp)
			enc.Flush()
			return
		case
			"/v1/notes/ID",
			"/v1/notes/list",
			"/v1/posts/dates",
			"/v1/posts/suggest",
			"/v1/posts/update",
			"/v1/tags/delete",
			"/v1/tags/get",
			"/v1/tags/rename",
			"/v1/user/api_token",
			"/v1/user/secret",
			"/v1/posts/recent":
			http.Error(w, "Not Implemented", http.StatusNotImplemented)
			return
		}
		http.NotFound(w, r)
	}
}

func formValuesFromReader(r io.Reader, name string) (ret url.Values, err error) {
	root, err := html.Parse(r) // assumes r is UTF8
	if err != nil {
		return ret, err
	}

	for _, form := range scrape.FindAll(root, func(n *html.Node) bool {
		return atom.Form == n.DataAtom &&
			(name == scrape.Attr(n, "name") || name == scrape.Attr(n, "id"))
	}) {
		ret := url.Values{}
		for _, inp := range scrape.FindAll(form, func(n *html.Node) bool {
			return atom.Input == n.DataAtom || atom.Textarea == n.DataAtom
		}) {
			n := scrape.Attr(inp, "name")
			if n == "" {
				n = scrape.Attr(inp, "id")
			}

			ty := scrape.Attr(inp, "type")
			v := scrape.Attr(inp, "value")
			if atom.Textarea == inp.DataAtom {
				v = scrape.Text(inp)
			} else if v == "" && ty == "checkbox" {
				v = scrape.Attr(inp, "checked")
			}
			ret.Set(n, v)
		}
		return ret, err // return on first occurence
	}
	return ret, err
}

R openapi.yaml => pinboard.in/v1/openapi.yaml +0 -0
D pinboard_test.go => pinboard_test.go +0 -231
@@ 1,231 0,0 @@
//
// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/pinboard4shaarli
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	//	"net/http/cgi"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/stretchr/testify/assert"
	"net/http/httptest"
	"testing"
)

func TestString(t *testing.T) {
	t.Parallel()
	const s = "abcde"
	assert.Equal(t, "de", s[len(s)-2:], "ach")
}

func TestPath(t *testing.T) {
	t.Parallel()
	path_info := "pinboard4shaarli.cgi"
	base, _ := url.Parse("https://demo.shaarli.org/pinboard4shaarli.cgi/v1/about/")
	base.Path = path.Join(base.Path[0:len(base.Path)-len(path_info)], "..", "index.php")
	assert.Equal(t, "/index.php", base.Path, "ach")
	assert.Equal(t, "https://demo.shaarli.org/index.php", base.String(), "ach")

	cgi := filepath.Base("../uhu/pinboard4shaarli.cgi")
	str := "https://demo.shaarli.org/pinboard4shaarli.cgi/v1/about/"
	idx := strings.LastIndex(str, cgi)
	assert.Equal(t, "/v1/about/", str[idx+len(cgi):], "wowo")
}

func TestCgi(t *testing.T) {
	t.Parallel()

	// rq := httptest.NewRequest(http.MethodGet, "/", nil)
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, client, %s\n", r.URL.String())
	}))
	defer ts.Close()

	// assert.Equal(t, "", ts.Config, "wowo")
	// assert.Equal(t, "", ts.Config.Addr, "wowo")
	res, _ := http.Get(ts.URL + "/" + path.Join("pinboard4shaarli.cgi"))
	assert.Equal(t, http.StatusOK, res.StatusCode, "wowo")
	assert.Equal(t, int64(37), res.ContentLength, "wowo")
	assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-Type"), "wowo")

	body, _ := ioutil.ReadAll(res.Body)
	res.Body.Close()
	assert.Equal(t, "Hello, client, /pinboard4shaarli.cgi\n", string(body), "wowo")
}

func TestBasicAuth(t *testing.T) {
	t.Parallel()
	url := "https://dema:demu@demo.shaarli.org/pinboard4shaarli.cgi/v1/posts/add?url=http://m.heise.de/12"
	r, err := http.NewRequest(http.MethodGet, url, nil)
	assert.Equal(t, nil, err, "wowo")
	usr := r.URL.User
	pwd, _ := usr.Password()
	r.SetBasicAuth(usr.Username(), pwd)
	uid, pwd, ok := r.BasicAuth()
	assert.Equal(t, true, ok, "wowo")
	assert.Equal(t, "dema", uid, "wowo")
	assert.Equal(t, "demu", pwd, "wowo")
}

func TestURL(t *testing.T) {
	t.Parallel()

	assert.Equal(t, "a/b", path.Join("a", "b", "/"), "ach")

	u, _ := url.Parse("https://l.mro.name/pinboard.cgi/v1/info")
	assert.Equal(t, "https://l.mro.name/pinboard.cgi/v1/info", u.String(), "ach")

	base := *u
	assert.Equal(t, "https://l.mro.name/pinboard.cgi/v1/info", base.String(), "ach")

	path_info := "/v1/info"
	base.Path = base.Path[0:len(base.Path)-len(path_info)] + "/../index.php"
	assert.Equal(t, "https://l.mro.name/pinboard.cgi/../index.php", base.String(), "ach")

	v := url.Values{}
	v.Set("post", "uhu")
	base.RawQuery = v.Encode()
	assert.Equal(t, "https://l.mro.name/pinboard.cgi/../index.php?post=uhu", base.String(), "ach")
}

func TestForm(t *testing.T) {
	t.Parallel()

	f := url.Values{}
	f.Set("login", "u i d")
	f.Set("password", "p & =d")
	r := bytes.NewReader([]byte(f.Encode()))
	b, err := ioutil.ReadAll(r)
	assert.Nil(t, err, "zzz")
	assert.Equal(t, "login=u+i+d&password=p+%26+%3Dd", string(b), "zzz")
}

func TestFormValuesFromHtml(t *testing.T) {
	file, err := os.Open("testdata/v0.10.2/login.html") // curl --location --output testdata/login.html 'https://demo.shaarli.org/?post=https://demo.mro.name/shaarligo'
	assert.Nil(t, err, "soso")
	ips, _ := formValuesFromReader(file, "loginform")
	assert.Equal(t, 40, len(ips["token"][0]), "form.token")
	// assert.Equal(t, "", ips["returnurl"][0], "form.returnurl")
	assert.Equal(t, "Login", ips[""][0], "form.")
	assert.Equal(t, "", ips["login"][0], "form.login")
	assert.Equal(t, "", ips["password"][0], "form.password")
	assert.Equal(t, "", ips["longlastingsession"][0], "form.longlastingsession")
	file.Close()

	file, err = os.Open("testdata/sebsauvage/login.html") // curl --location --output testdata/login.html 'https://demo.shaarli.org/?post=https://demo.mro.name/shaarligo'
	assert.Nil(t, err, "soso")
	ips, _ = formValuesFromReader(file, "loginform")
	assert.Equal(t, 40, len(ips["token"][0]), "form.token")
	// assert.Equal(t, "", ips["returnurl"][0], "form.returnurl")
	assert.Equal(t, "Login", ips[""][0], "form.")
	assert.Equal(t, "", ips["login"][0], "form.login")
	assert.Equal(t, "", ips["password"][0], "form.password")
	assert.Equal(t, "", ips["longlastingsession"][0], "form.longlastingsession")
	file.Close()

	file, err = os.Open("testdata/bookmark/login.html") // curl --location --output testdata/login.html 'https://demo.shaarli.org/?post=https://demo.mro.name/shaarligo'
	assert.Nil(t, err, "soso")
	ips, _ = formValuesFromReader(file, "loginform")
	assert.Equal(t, 40, len(ips["token"][0]), "form.token")
	// assert.Equal(t, "", ips["returnurl"][0], "form.returnurl")
	assert.Equal(t, "Login", ips[""][0], "form.")
	assert.Equal(t, "", ips["login"][0], "form.login")
	assert.Equal(t, "", ips["password"][0], "form.password")
	assert.Equal(t, "", ips["longlastingsession"][0], "form.longlastingsession")
	file.Close()

	file, err = os.Open("testdata/shaarligo/linkform.html") // curl --location --output testdata/login.html 'https://demo.shaarli.org/?post=https://demo.mro.name/shaarligo'
	assert.Nil(t, err, "soso")
	ips, _ = formValuesFromReader(file, "linkform")
	assert.Equal(t, 40, len(ips["token"][0]), "form.token")
	assert.Equal(t, "", ips["returnurl"][0], "form.returnurl")
	assert.Equal(t, "20190106_172531", ips["lf_linkdate"][0], "form.lf_linkdate")
	// tim, _ := time.ParseInLocation("20060102_150405", "20190106_172531", time.Now().Location())
	// assert.Equal(t, int64(1546791931), tim.Unix(), "form.lf_linkdate")
	// assert.Equal(t, time.Unix(1546791931, 0), tim, "form.lf_linkdate")
	assert.Equal(t, "", ips["lf_url"][0], "form.lf_url")
	assert.Equal(t, "uhu", ips["lf_title"][0], "form.lf_title")
	assert.Equal(t, "content", ips["lf_description"][0], "form.lf_description")
	assert.Equal(t, "", ips["lf_tags"][0], "form.lf_tags")
	assert.Equal(t, "Save", ips["save_edit"][0], "form.save_edit")
	file.Close()

	/*
	   	<?xml version="1.0" encoding="UTF-8"?>
	   <?xml-stylesheet type='text/xsl' href='./assets/default/de/do-post.xslt'?>
	   <!--
	     must be compatible with https://code.mro.name/mro/Shaarli-API-test/src/master/tests/test-post.sh
	     https://code.mro.name/mro/ShaarliOS/src/1d124e012933d1209d64071a90237dc5ec6372fc/ios/ShaarliOS/API/ShaarliCmd.m#L386
	   -->
	   <html xmlns="http://www.w3.org/1999/xhtml" xml:base="https://l.mro.name/">
	   <head><title>{ πŸ”— 🐳 πŸš€ πŸ’«  }</title></head>
	   <body>
	     <ul id="taglist" style="display:none"></ul>
	     <form method="post" name="linkform">
	       <input name="lf_linkdate" type="hidden" value="20190106_172531"/>
	       <input name="lf_url" type="text" value=""/>
	       <input name="lf_title" type="text" value="uhu"/>
	       <textarea name="lf_description" rows="4" cols="25"></textarea>
	       <input name="lf_tags" type="text" data-multiple="data-multiple" value=""/>
	       <input name="lf_private" type="checkbox" value=""/>
	       <input name="save_edit" type="submit" value="Save"/>
	       <input name="cancel_edit" type="submit" value="Cancel"/>
	       <input name="token" type="hidden" value="d9aab65f6ca2462449079d72d5321ebae0ec8325"/>
	       <input name="returnurl" type="hidden" value=""/>
	       <input name="lf_image" type="hidden" value=""/>
	     </form>
	   </body>
	   </html>
	*/
}

func TestRailRoad(t *testing.T) {
	fa := func(success bool) (ret string, err error) {
		if !success {
			return "fail", fmt.Errorf("failure")
		}
		return "ok", err
	}
	fb := func(s string, err0 error) (int, error) {
		if err0 != nil {
			return 0, err0
		}
		ret := 1
		if s == "ok" {
			ret = 2
		}
		return ret, err0
	}
	{
		v, e := fb(fa(true))
		assert.Equal(t, 2, v, "success")
		assert.Equal(t, nil, e, "success")
	}
	{
		v, e := fb(fa(false))
		assert.Equal(t, 0, v, "success")
		assert.Equal(t, "failure", e.Error(), "success")
	}
}

R testdata/bookmark/login.html => test/data/bookmark/login.html +0 -0
R testdata/sebsauvage/login.html => test/data/sebsauvage/login.html +0 -0
R testdata/shaarligo/linkform.html => test/data/shaarligo/linkform.html +0 -0
A test/data/soup_test/configure.1.html => test/data/soup_test/configure.1.html +68 -0
@@ 0,0 1,68 @@
<!DOCTYPE html>
<html>
<head><title>Shaarli v0.41 πŸš€</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="alternate" type="application/rss+xml" href="https://demo.0x4c.de/shaarli-v0.41b/?do=rss" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="https://demo.0x4c.de/shaarli-v0.41b/?do=atom" title="ATOM Feed" />
<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
<link type="text/css" rel="stylesheet" href="inc/shaarli.css?version=0.0.41+beta" />

</head>
<body onload="document.configform.title.focus();">
<div id="pageheader">
	
    <div id="logo" title="Share your links !" onclick="document.location='?';"></div>
    <div style="float:right; font-style:italic; color:#bbb; text-align:right; padding:0 5 0 0;" class="nomobile">Shaare your links...<br>
        2 links</div>
    <span id="shaarli_title"><a href="?">Shaarli v0.41 πŸš€</a></span>
  

    <a href="?" class="nomobile">Home</a>
    
        <a href="?do=logout">Logout</a><a href="?do=tools">Tools</a><a href="?do=addlink"><b>Add link</b></a>
    
    <a href="https://demo.0x4c.de/shaarli-v0.41b/?do=rss" class="nomobile">RSS Feed</a>
    <a href="https://demo.0x4c.de/shaarli-v0.41b/?do=atom" style="padding-left:10px;" class="nomobile">ATOM Feed</a>
    <a href="?do=tagcloud">Tag cloud</a>
    <a href="?do=picwall">Picture wall</a>
    <a href="?do=daily">Daily</a>

    <div class="clear"></div>



<script language="JavaScript">function onChangecontinent(){document.getElementById("city").innerHTML = citiescontinent[document.getElementById("continent").value];}var citiescontinent = {"Africa":"<option value=\"Abidjan\">Abidjan<\/option><option value=\"Accra\">Accra<\/option><option value=\"Addis_Ababa\">Addis_Ababa<\/option><option value=\"Algiers\">Algiers<\/option><option value=\"Asmara\">Asmara<\/option><option value=\"Bamako\">Bamako<\/option><option value=\"Bangui\">Bangui<\/option><option value=\"Banjul\">Banjul<\/option><option value=\"Bissau\">Bissau<\/option><option value=\"Blantyre\">Blantyre<\/option><option value=\"Brazzaville\">Brazzaville<\/option><option value=\"Bujumbura\">Bujumbura<\/option><option value=\"Cairo\">Cairo<\/option><option value=\"Casablanca\">Casablanca<\/option><option value=\"Ceuta\">Ceuta<\/option><option value=\"Conakry\">Conakry<\/option><option value=\"Dakar\">Dakar<\/option><option value=\"Dar_es_Salaam\">Dar_es_Salaam<\/option><option value=\"Djibouti\">Djibouti<\/option><option value=\"Douala\">Douala<\/option><option value=\"El_Aaiun\">El_Aaiun<\/option><option value=\"Freetown\">Freetown<\/option><option value=\"Gaborone\">Gaborone<\/option><option value=\"Harare\">Harare<\/option><option value=\"Johannesburg\">Johannesburg<\/option><option value=\"Juba\">Juba<\/option><option value=\"Kampala\">Kampala<\/option><option value=\"Khartoum\">Khartoum<\/option><option value=\"Kigali\">Kigali<\/option><option value=\"Kinshasa\">Kinshasa<\/option><option value=\"Lagos\">Lagos<\/option><option value=\"Libreville\">Libreville<\/option><option value=\"Lome\">Lome<\/option><option value=\"Luanda\">Luanda<\/option><option value=\"Lubumbashi\">Lubumbashi<\/option><option value=\"Lusaka\">Lusaka<\/option><option value=\"Malabo\">Malabo<\/option><option value=\"Maputo\">Maputo<\/option><option value=\"Maseru\">Maseru<\/option><option value=\"Mbabane\">Mbabane<\/option><option value=\"Mogadishu\">Mogadishu<\/option><option value=\"Monrovia\">Monrovia<\/option><option value=\"Nairobi\">Nairobi<\/option><option value=\"Ndjamena\">Ndjamena<\/option><option value=\"Niamey\">Niamey<\/option><option value=\"Nouakchott\">Nouakchott<\/option><option value=\"Ouagadougou\">Ouagadougou<\/option><option value=\"Porto-Novo\">Porto-Novo<\/option><option value=\"Sao_Tome\">Sao_Tome<\/option><option value=\"Tripoli\">Tripoli<\/option><option value=\"Tunis\">Tunis<\/option><option value=\"Windhoek\">Windhoek<\/option>","America":"<option value=\"Adak\">Adak<\/option><option value=\"Anchorage\">Anchorage<\/option><option value=\"Anguilla\">Anguilla<\/option><option value=\"Antigua\">Antigua<\/option><option value=\"Araguaina\">Araguaina<\/option><option value=\"Argentina\/Buenos_Aires\">Argentina\/Buenos_Aires<\/option><option value=\"Argentina\/Catamarca\">Argentina\/Catamarca<\/option><option value=\"Argentina\/Cordoba\">Argentina\/Cordoba<\/option><option value=\"Argentina\/Jujuy\">Argentina\/Jujuy<\/option><option value=\"Argentina\/La_Rioja\">Argentina\/La_Rioja<\/option><option value=\"Argentina\/Mendoza\">Argentina\/Mendoza<\/option><option value=\"Argentina\/Rio_Gallegos\">Argentina\/Rio_Gallegos<\/option><option value=\"Argentina\/Salta\">Argentina\/Salta<\/option><option value=\"Argentina\/San_Juan\">Argentina\/San_Juan<\/option><option value=\"Argentina\/San_Luis\">Argentina\/San_Luis<\/option><option value=\"Argentina\/Tucuman\">Argentina\/Tucuman<\/option><option value=\"Argentina\/Ushuaia\">Argentina\/Ushuaia<\/option><option value=\"Aruba\">Aruba<\/option><option value=\"Asuncion\">Asuncion<\/option><option value=\"Atikokan\">Atikokan<\/option><option value=\"Bahia\">Bahia<\/option><option value=\"Bahia_Banderas\">Bahia_Banderas<\/option><option value=\"Barbados\">Barbados<\/option><option value=\"Belem\">Belem<\/option><option value=\"Belize\">Belize<\/option><option value=\"Blanc-Sablon\">Blanc-Sablon<\/option><option value=\"Boa_Vista\">Boa_Vista<\/option><option value=\"Bogota\">Bogota<\/option><option value=\"Boise\">Boise<\/option><option value=\"Cambridge_Bay\">Cambridge_Bay<\/option><option value=\"Campo_Grande\">Campo_Grande<\/option><option value=\"Cancun\">Cancun<\/option><option value=\"Caracas\">Caracas<\/option><option value=\"Cayenne\">Cayenne<\/option><option value=\"Cayman\">Cayman<\/option><option value=\"Chicago\">Chicago<\/option><option value=\"Chihuahua\">Chihuahua<\/option><option value=\"Costa_Rica\">Costa_Rica<\/option><option value=\"Creston\">Creston<\/option><option value=\"Cuiaba\">Cuiaba<\/option><option value=\"Curacao\">Curacao<\/option><option value=\"Danmarkshavn\">Danmarkshavn<\/option><option value=\"Dawson\">Dawson<\/option><option value=\"Dawson_Creek\">Dawson_Creek<\/option><option value=\"Denver\">Denver<\/option><option value=\"Detroit\">Detroit<\/option><option value=\"Dominica\">Dominica<\/option><option value=\"Edmonton\">Edmonton<\/option><option value=\"Eirunepe\">Eirunepe<\/option><option value=\"El_Salvador\">El_Salvador<\/option><option value=\"Fort_Nelson\">Fort_Nelson<\/option><option value=\"Fortaleza\">Fortaleza<\/option><option value=\"Glace_Bay\">Glace_Bay<\/option><option value=\"Godthab\">Godthab<\/option><option value=\"Goose_Bay\">Goose_Bay<\/option><option value=\"Grand_Turk\">Grand_Turk<\/option><option value=\"Grenada\">Grenada<\/option><option value=\"Guadeloupe\">Guadeloupe<\/option><option value=\"Guatemala\">Guatemala<\/option><option value=\"Guayaquil\">Guayaquil<\/option><option value=\"Guyana\">Guyana<\/option><option value=\"Halifax\">Halifax<\/option><option value=\"Havana\">Havana<\/option><option value=\"Hermosillo\">Hermosillo<\/option><option value=\"Indiana\/Indianapolis\">Indiana\/Indianapolis<\/option><option value=\"Indiana\/Knox\">Indiana\/Knox<\/option><option value=\"Indiana\/Marengo\">Indiana\/Marengo<\/option><option value=\"Indiana\/Petersburg\">Indiana\/Petersburg<\/option><option value=\"Indiana\/Tell_City\">Indiana\/Tell_City<\/option><option value=\"Indiana\/Vevay\">Indiana\/Vevay<\/option><option value=\"Indiana\/Vincennes\">Indiana\/Vincennes<\/option><option value=\"Indiana\/Winamac\">Indiana\/Winamac<\/option><option value=\"Inuvik\">Inuvik<\/option><option value=\"Iqaluit\">Iqaluit<\/option><option value=\"Jamaica\">Jamaica<\/option><option value=\"Juneau\">Juneau<\/option><option value=\"Kentucky\/Louisville\">Kentucky\/Louisville<\/option><option value=\"Kentucky\/Monticello\">Kentucky\/Monticello<\/option><option value=\"Kralendijk\">Kralendijk<\/option><option value=\"La_Paz\">La_Paz<\/option><option value=\"Lima\">Lima<\/option><option value=\"Los_Angeles\">Los_Angeles<\/option><option value=\"Lower_Princes\">Lower_Princes<\/option><option value=\"Maceio\">Maceio<\/option><option value=\"Managua\">Managua<\/option><option value=\"Manaus\">Manaus<\/option><option value=\"Marigot\">Marigot<\/option><option value=\"Martinique\">Martinique<\/option><option value=\"Matamoros\">Matamoros<\/option><option value=\"Mazatlan\">Mazatlan<\/option><option value=\"Menominee\">Menominee<\/option><option value=\"Merida\">Merida<\/option><option value=\"Metlakatla\">Metlakatla<\/option><option value=\"Mexico_City\">Mexico_City<\/option><option value=\"Miquelon\">Miquelon<\/option><option value=\"Moncton\">Moncton<\/option><option value=\"Monterrey\">Monterrey<\/option><option value=\"Montevideo\">Montevideo<\/option><option value=\"Montserrat\">Montserrat<\/option><option value=\"Nassau\">Nassau<\/option><option value=\"New_York\">New_York<\/option><option value=\"Nipigon\">Nipigon<\/option><option value=\"Nome\">Nome<\/option><option value=\"Noronha\">Noronha<\/option><option value=\"North_Dakota\/Beulah\">North_Dakota\/Beulah<\/option><option value=\"North_Dakota\/Center\">North_Dakota\/Center<\/option><option value=\"North_Dakota\/New_Salem\">North_Dakota\/New_Salem<\/option><option value=\"Ojinaga\">Ojinaga<\/option><option value=\"Panama\">Panama<\/option><option value=\"Pangnirtung\">Pangnirtung<\/option><option value=\"Paramaribo\">Paramaribo<\/option><option value=\"Phoenix\">Phoenix<\/option><option value=\"Port-au-Prince\">Port-au-Prince<\/option><option value=\"Port_of_Spain\">Port_of_Spain<\/option><option value=\"Porto_Velho\">Porto_Velho<\/option><option value=\"Puerto_Rico\">Puerto_Rico<\/option><option value=\"Punta_Arenas\">Punta_Arenas<\/option><option value=\"Rainy_River\">Rainy_River<\/option><option value=\"Rankin_Inlet\">Rankin_Inlet<\/option><option value=\"Recife\">Recife<\/option><option value=\"Regina\">Regina<\/option><option value=\"Resolute\">Resolute<\/option><option value=\"Rio_Branco\">Rio_Branco<\/option><option value=\"Santarem\">Santarem<\/option><option value=\"Santiago\">Santiago<\/option><option value=\"Santo_Domingo\">Santo_Domingo<\/option><option value=\"Sao_Paulo\">Sao_Paulo<\/option><option value=\"Scoresbysund\">Scoresbysund<\/option><option value=\"Sitka\">Sitka<\/option><option value=\"St_Barthelemy\">St_Barthelemy<\/option><option value=\"St_Johns\">St_Johns<\/option><option value=\"St_Kitts\">St_Kitts<\/option><option value=\"St_Lucia\">St_Lucia<\/option><option value=\"St_Thomas\">St_Thomas<\/option><option value=\"St_Vincent\">St_Vincent<\/option><option value=\"Swift_Current\">Swift_Current<\/option><option value=\"Tegucigalpa\">Tegucigalpa<\/option><option value=\"Thule\">Thule<\/option><option value=\"Thunder_Bay\">Thunder_Bay<\/option><option value=\"Tijuana\">Tijuana<\/option><option value=\"Toronto\">Toronto<\/option><option value=\"Tortola\">Tortola<\/option><option value=\"Vancouver\">Vancouver<\/option><option value=\"Whitehorse\">Whitehorse<\/option><option value=\"Winnipeg\">Winnipeg<\/option><option value=\"Yakutat\">Yakutat<\/option><option value=\"Yellowknife\">Yellowknife<\/option>","Antarctica":"<option value=\"Casey\">Casey<\/option><option value=\"Davis\">Davis<\/option><option value=\"DumontDUrville\">DumontDUrville<\/option><option value=\"Macquarie\">Macquarie<\/option><option value=\"Mawson\">Mawson<\/option><option value=\"McMurdo\">McMurdo<\/option><option value=\"Palmer\">Palmer<\/option><option value=\"Rothera\">Rothera<\/option><option value=\"Syowa\">Syowa<\/option><option value=\"Troll\">Troll<\/option><option value=\"Vostok\">Vostok<\/option>","Arctic":"<option value=\"Longyearbyen\">Longyearbyen<\/option>","Asia":"<option value=\"Aden\">Aden<\/option><option value=\"Almaty\">Almaty<\/option><option value=\"Amman\">Amman<\/option><option value=\"Anadyr\">Anadyr<\/option><option value=\"Aqtau\">Aqtau<\/option><option value=\"Aqtobe\">Aqtobe<\/option><option value=\"Ashgabat\">Ashgabat<\/option><option value=\"Atyrau\">Atyrau<\/option><option value=\"Baghdad\">Baghdad<\/option><option value=\"Bahrain\">Bahrain<\/option><option value=\"Baku\">Baku<\/option><option value=\"Bangkok\">Bangkok<\/option><option value=\"Barnaul\">Barnaul<\/option><option value=\"Beirut\">Beirut<\/option><option value=\"Bishkek\">Bishkek<\/option><option value=\"Brunei\">Brunei<\/option><option value=\"Chita\">Chita<\/option><option value=\"Choibalsan\">Choibalsan<\/option><option value=\"Colombo\">Colombo<\/option><option value=\"Damascus\">Damascus<\/option><option value=\"Dhaka\">Dhaka<\/option><option value=\"Dili\">Dili<\/option><option value=\"Dubai\">Dubai<\/option><option value=\"Dushanbe\">Dushanbe<\/option><option value=\"Famagusta\">Famagusta<\/option><option value=\"Gaza\">Gaza<\/option><option value=\"Hebron\">Hebron<\/option><option value=\"Ho_Chi_Minh\">Ho_Chi_Minh<\/option><option value=\"Hong_Kong\">Hong_Kong<\/option><option value=\"Hovd\">Hovd<\/option><option value=\"Irkutsk\">Irkutsk<\/option><option value=\"Jakarta\">Jakarta<\/option><option value=\"Jayapura\">Jayapura<\/option><option value=\"Jerusalem\">Jerusalem<\/option><option value=\"Kabul\">Kabul<\/option><option value=\"Kamchatka\">Kamchatka<\/option><option value=\"Karachi\">Karachi<\/option><option value=\"Kathmandu\">Kathmandu<\/option><option value=\"Khandyga\">Khandyga<\/option><option value=\"Kolkata\">Kolkata<\/option><option value=\"Krasnoyarsk\">Krasnoyarsk<\/option><option value=\"Kuala_Lumpur\">Kuala_Lumpur<\/option><option value=\"Kuching\">Kuching<\/option><option value=\"Kuwait\">Kuwait<\/option><option value=\"Macau\">Macau<\/option><option value=\"Magadan\">Magadan<\/option><option value=\"Makassar\">Makassar<\/option><option value=\"Manila\">Manila<\/option><option value=\"Muscat\">Muscat<\/option><option value=\"Nicosia\">Nicosia<\/option><option value=\"Novokuznetsk\">Novokuznetsk<\/option><option value=\"Novosibirsk\">Novosibirsk<\/option><option value=\"Omsk\">Omsk<\/option><option value=\"Oral\">Oral<\/option><option value=\"Phnom_Penh\">Phnom_Penh<\/option><option value=\"Pontianak\">Pontianak<\/option><option value=\"Pyongyang\">Pyongyang<\/option><option value=\"Qatar\">Qatar<\/option><option value=\"Qostanay\">Qostanay<\/option><option value=\"Qyzylorda\">Qyzylorda<\/option><option value=\"Riyadh\">Riyadh<\/option><option value=\"Sakhalin\">Sakhalin<\/option><option value=\"Samarkand\">Samarkand<\/option><option value=\"Seoul\">Seoul<\/option><option value=\"Shanghai\">Shanghai<\/option><option value=\"Singapore\">Singapore<\/option><option value=\"Srednekolymsk\">Srednekolymsk<\/option><option value=\"Taipei\">Taipei<\/option><option value=\"Tashkent\">Tashkent<\/option><option value=\"Tbilisi\">Tbilisi<\/option><option value=\"Tehran\">Tehran<\/option><option value=\"Thimphu\">Thimphu<\/option><option value=\"Tokyo\">Tokyo<\/option><option value=\"Tomsk\">Tomsk<\/option><option value=\"Ulaanbaatar\">Ulaanbaatar<\/option><option value=\"Urumqi\">Urumqi<\/option><option value=\"Ust-Nera\">Ust-Nera<\/option><option value=\"Vientiane\">Vientiane<\/option><option value=\"Vladivostok\">Vladivostok<\/option><option value=\"Yakutsk\">Yakutsk<\/option><option value=\"Yangon\">Yangon<\/option><option value=\"Yekaterinburg\">Yekaterinburg<\/option><option value=\"Yerevan\">Yerevan<\/option>","Atlantic":"<option value=\"Azores\">Azores<\/option><option value=\"Bermuda\">Bermuda<\/option><option value=\"Canary\">Canary<\/option><option value=\"Cape_Verde\">Cape_Verde<\/option><option value=\"Faroe\">Faroe<\/option><option value=\"Madeira\">Madeira<\/option><option value=\"Reykjavik\">Reykjavik<\/option><option value=\"South_Georgia\">South_Georgia<\/option><option value=\"St_Helena\">St_Helena<\/option><option value=\"Stanley\">Stanley<\/option>","Australia":"<option value=\"Adelaide\">Adelaide<\/option><option value=\"Brisbane\">Brisbane<\/option><option value=\"Broken_Hill\">Broken_Hill<\/option><option value=\"Currie\">Currie<\/option><option value=\"Darwin\">Darwin<\/option><option value=\"Eucla\">Eucla<\/option><option value=\"Hobart\">Hobart<\/option><option value=\"Lindeman\">Lindeman<\/option><option value=\"Lord_Howe\">Lord_Howe<\/option><option value=\"Melbourne\">Melbourne<\/option><option value=\"Perth\">Perth<\/option><option value=\"Sydney\">Sydney<\/option>","Europe":"<option value=\"Amsterdam\"selected>Amsterdam<\/option><option value=\"Andorra\">Andorra<\/option><option value=\"Astrakhan\">Astrakhan<\/option><option value=\"Athens\">Athens<\/option><option value=\"Belgrade\">Belgrade<\/option><option value=\"Berlin\">Berlin<\/option><option value=\"Bratislava\">Bratislava<\/option><option value=\"Brussels\">Brussels<\/option><option value=\"Bucharest\">Bucharest<\/option><option value=\"Budapest\">Budapest<\/option><option value=\"Busingen\">Busingen<\/option><option value=\"Chisinau\">Chisinau<\/option><option value=\"Copenhagen\">Copenhagen<\/option><option value=\"Dublin\">Dublin<\/option><option value=\"Gibraltar\">Gibraltar<\/option><option value=\"Guernsey\">Guernsey<\/option><option value=\"Helsinki\">Helsinki<\/option><option value=\"Isle_of_Man\">Isle_of_Man<\/option><option value=\"Istanbul\">Istanbul<\/option><option value=\"Jersey\">Jersey<\/option><option value=\"Kaliningrad\">Kaliningrad<\/option><option value=\"Kiev\">Kiev<\/option><option value=\"Kirov\">Kirov<\/option><option value=\"Lisbon\">Lisbon<\/option><option value=\"Ljubljana\">Ljubljana<\/option><option value=\"London\">London<\/option><option value=\"Luxembourg\">Luxembourg<\/option><option value=\"Madrid\">Madrid<\/option><option value=\"Malta\">Malta<\/option><option value=\"Mariehamn\">Mariehamn<\/option><option value=\"Minsk\">Minsk<\/option><option value=\"Monaco\">Monaco<\/option><option value=\"Moscow\">Moscow<\/option><option value=\"Oslo\">Oslo<\/option><option value=\"Paris\">Paris<\/option><option value=\"Podgorica\">Podgorica<\/option><option value=\"Prague\">Prague<\/option><option value=\"Riga\">Riga<\/option><option value=\"Rome\">Rome<\/option><option value=\"Samara\">Samara<\/option><option value=\"San_Marino\">San_Marino<\/option><option value=\"Sarajevo\">Sarajevo<\/option><option value=\"Saratov\">Saratov<\/option><option value=\"Simferopol\">Simferopol<\/option><option value=\"Skopje\">Skopje<\/option><option value=\"Sofia\">Sofia<\/option><option value=\"Stockholm\">Stockholm<\/option><option value=\"Tallinn\">Tallinn<\/option><option value=\"Tirane\">Tirane<\/option><option value=\"Ulyanovsk\">Ulyanovsk<\/option><option value=\"Uzhgorod\">Uzhgorod<\/option><option value=\"Vaduz\">Vaduz<\/option><option value=\"Vatican\">Vatican<\/option><option value=\"Vienna\">Vienna<\/option><option value=\"Vilnius\">Vilnius<\/option><option value=\"Volgograd\">Volgograd<\/option><option value=\"Warsaw\">Warsaw<\/option><option value=\"Zagreb\">Zagreb<\/option><option value=\"Zaporozhye\">Zaporozhye<\/option><option value=\"Zurich\">Zurich<\/option>","Indian":"<option value=\"Antananarivo\">Antananarivo<\/option><option value=\"Chagos\">Chagos<\/option><option value=\"Christmas\">Christmas<\/option><option value=\"Cocos\">Cocos<\/option><option value=\"Comoro\">Comoro<\/option><option value=\"Kerguelen\">Kerguelen<\/option><option value=\"Mahe\">Mahe<\/option><option value=\"Maldives\">Maldives<\/option><option value=\"Mauritius\">Mauritius<\/option><option value=\"Mayotte\">Mayotte<\/option><option value=\"Reunion\">Reunion<\/option>","Pacific":"<option value=\"Apia\">Apia<\/option><option value=\"Auckland\">Auckland<\/option><option value=\"Bougainville\">Bougainville<\/option><option value=\"Chatham\">Chatham<\/option><option value=\"Chuuk\">Chuuk<\/option><option value=\"Easter\">Easter<\/option><option value=\"Efate\">Efate<\/option><option value=\"Enderbury\">Enderbury<\/option><option value=\"Fakaofo\">Fakaofo<\/option><option value=\"Fiji\">Fiji<\/option><option value=\"Funafuti\">Funafuti<\/option><option value=\"Galapagos\">Galapagos<\/option><option value=\"Gambier\">Gambier<\/option><option value=\"Guadalcanal\">Guadalcanal<\/option><option value=\"Guam\">Guam<\/option><option value=\"Honolulu\">Honolulu<\/option><option value=\"Kiritimati\">Kiritimati<\/option><option value=\"Kosrae\">Kosrae<\/option><option value=\"Kwajalein\">Kwajalein<\/option><option value=\"Majuro\">Majuro<\/option><option value=\"Marquesas\">Marquesas<\/option><option value=\"Midway\">Midway<\/option><option value=\"Nauru\">Nauru<\/option><option value=\"Niue\">Niue<\/option><option value=\"Norfolk\">Norfolk<\/option><option value=\"Noumea\">Noumea<\/option><option value=\"Pago_Pago\">Pago_Pago<\/option><option value=\"Palau\">Palau<\/option><option value=\"Pitcairn\">Pitcairn<\/option><option value=\"Pohnpei\">Pohnpei<\/option><option value=\"Port_Moresby\">Port_Moresby<\/option><option value=\"Rarotonga\">Rarotonga<\/option><option value=\"Saipan\">Saipan<\/option><option value=\"Tahiti\">Tahiti<\/option><option value=\"Tarawa\">Tarawa<\/option><option value=\"Tongatapu\">Tongatapu<\/option><option value=\"Wake\">Wake<\/option><option value=\"Wallis\">Wallis<\/option>","UTC":"<option value=\"UTC\">UTC<\/option>"};</script>
    <form method="POST" action="" name="configform" id="configform">
	<input type="hidden" name="token" value="f5b3588fe9d39b9972d80cbdb16e1af211aeb8bd">
	<table border="0" cellpadding="20">

	    <tr><td><b>Page title:</b></td><td><input type="text" name="title" id="title" size="50" value="Shaarli v0.41 πŸš€"></td></tr>

	    <tr><td valign="top"><b>Timezone:</b></td><td valign="top">Continent: <select name="continent" id="continent" onChange="onChangecontinent();"><option  value="Africa">Africa</option><option  value="America">America</option><option  value="Antarctica">Antarctica</option><option  value="Arctic">Arctic</option><option  value="Asia">Asia</option><option  value="Atlantic">Atlantic</option><option  value="Australia">Australia</option><option  value="Europe"selected>Europe</option><option  value="Indian">Indian</option><option  value="Pacific">Pacific</option><option  value="UTC">UTC</option></select>&nbsp;&nbsp;&nbsp;&nbsp;City: <select name="city" id="city"><option value="Amsterdam"selected>Amsterdam</option><option value="Andorra">Andorra</option><option value="Astrakhan">Astrakhan</option><option value="Athens">Athens</option><option value="Belgrade">Belgrade</option><option value="Berlin">Berlin</option><option value="Bratislava">Bratislava</option><option value="Brussels">Brussels</option><option value="Bucharest">Bucharest</option><option value="Budapest">Budapest</option><option value="Busingen">Busingen</option><option value="Chisinau">Chisinau</option><option value="Copenhagen">Copenhagen</option><option value="Dublin">Dublin</option><option value="Gibraltar">Gibraltar</option><option value="Guernsey">Guernsey</option><option value="Helsinki">Helsinki</option><option value="Isle_of_Man">Isle_of_Man</option><option value="Istanbul">Istanbul</option><option value="Jersey">Jersey</option><option value="Kaliningrad">Kaliningrad</option><option value="Kiev">Kiev</option><option value="Kirov">Kirov</option><option value="Lisbon">Lisbon</option><option value="Ljubljana">Ljubljana</option><option value="London">London</option><option value="Luxembourg">Luxembourg</option><option value="Madrid">Madrid</option><option value="Malta">Malta</option><option value="Mariehamn">Mariehamn</option><option value="Minsk">Minsk</option><option value="Monaco">Monaco</option><option value="Moscow">Moscow</option><option value="Oslo">Oslo</option><option value="Paris">Paris</option><option value="Podgorica">Podgorica</option><option value="Prague">Prague</option><option value="Riga">Riga</option><option value="Rome">Rome</option><option value="Samara">Samara</option><option value="San_Marino">San_Marino</option><option value="Sarajevo">Sarajevo</option><option value="Saratov">Saratov</option><option value="Simferopol">Simferopol</option><option value="Skopje">Skopje</option><option value="Sofia">Sofia</option><option value="Stockholm">Stockholm</option><option value="Tallinn">Tallinn</option><option value="Tirane">Tirane</option><option value="Ulyanovsk">Ulyanovsk</option><option value="Uzhgorod">Uzhgorod</option><option value="Vaduz">Vaduz</option><option value="Vatican">Vatican</option><option value="Vienna">Vienna</option><option value="Vilnius">Vilnius</option><option value="Volgograd">Volgograd</option><option value="Warsaw">Warsaw</option><option value="Zagreb">Zagreb</option><option value="Zaporozhye">Zaporozhye</option><option value="Zurich">Zurich</option></select><br /></td></tr>

	    <tr><td valign="top"><b>Redirector</b></td><td><input type="text" name="redirector" id="redirector" size="50" value=""><br>(e.g. <i>http://anonym.to/?</i> will mask the HTTP_REFERER)</td></tr>

        <tr><td valign="top"><b>Security:</b></td><td><input type="checkbox" name="disablesessionprotection" id="disablesessionprotection" ><label for="disablesessionprotection">&nbsp;Disable session cookie hijacking protection (Check this if you get disconnected often or if your IP address changes often.)</label></td></tr>

        <tr><td valign="top"><b>Features:</b></td><td>
        	<input type="checkbox" name="disablejquery" id="disablejquery" ><label for="disablejquery">&nbsp;Disable jQuery and all heavy javascript (for example: Autocomplete in tags. Useful for slow computers.)</label>
        </td></tr>
        <tr><td valign="top"><b>New link:</b></td><td>
        	<input type="checkbox" name="privateLinkByDefault" id="privateLinkByDefault" /><label for="privateLinkByDefault">&nbsp;All new link are private by default</label></td>
        </tr>
	  <tr><td></td><td align="right"><input type="submit" name="Save" value="Save config" class="bigbutton"></td></tr>
	</table>
	</form>
</div>
<div id="footer">
    <b><a href="http://sebsauvage.net/wiki/doku.php?id=php:shaarli">Shaarli 0.0.41 beta</a></b> - The personal, minimalist, super-fast, no-database delicious clone. By <a href="http://sebsauvage.net" target="_blank">sebsauvage.net</a>. Theme by <a href="http://blog.idleman.fr" target="_blank">idleman.fr</a>.
</div>


<script language="JavaScript">function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>


</body>
</html>
\ No newline at end of file

R testdata/v0.10.2/login.html => test/data/v0.10.2/login.html +0 -0
A test/dune => test/dune +8 -0
@@ 0,0 1,8 @@
; http://cumulus.github.io/Syndic/syndic/Syndic__/Syndic_atom/
(tests
 (names
   soup_test
   url_test
   simple_test
 )
 (libraries Lib alcotest))

A test/simple_test.ml => test/simple_test.ml +34 -0
@@ 0,0 1,34 @@
(* Build with `ocamlbuild -pkg alcotest simple.byte` *)

(* A module with functions to test *)
module To_test = struct
  let lowercase = String.lowercase_ascii
  let capitalize = String.capitalize_ascii
  let str_concat = String.concat ""
  let list_concat = List.append
end

(* The tests *)
let test_lowercase () =
  Alcotest.(check string) "same string" "hello!" (To_test.lowercase "hELLO!")

let test_capitalize () =
  Alcotest.(check string) "same string" "World." (To_test.capitalize "world.")

let test_str_concat () =
  Alcotest.(check string) "same string" "foobar" (To_test.str_concat ["foo"; "bar"])

let test_list_concat () =
  Alcotest.(check (list int)) "same lists" [1; 2; 3] (To_test.list_concat [1] [2; 3])

(* Run it *)
let () =
  let open Alcotest in
  run "Utils" [
      "string-case", [
          test_case "Lower case"     `Quick test_lowercase;
          test_case "Capitalization" `Quick test_capitalize;
        ];
      "string-concat", [ test_case "String mashing" `Quick test_str_concat  ];
      "list-concat",   [ test_case "List mashing"   `Slow  test_list_concat ];
    ]

A test/soup_test.ml => test/soup_test.ml +16 -0
@@ 0,0 1,16 @@
open Soup (* https://aantron.github.io/lambdasoup/ *)
(* https://caml.inria.fr/pub/docs/manual-ocaml/libref/Sys.html *)

let () = assert (1 = 1)
let () = assert (1 + 1 = 2)

let test_configure_find_title() = 
  let soup = read_file "configure.1.html" |> parse in
  let ti = soup $ "html > head > title" |> R.leaf_text in
  (* ti |> print_endline; *)
  assert(ti = "Shaarli v0.41 πŸš€")

let () = 
  Unix.chdir "../../../test/data/soup_test/";
  test_configure_find_title()


A test/url_test.ml => test/url_test.ml +17 -0
@@ 0,0 1,17 @@

open Lib.Url

let () =
  let raw = "https://uid:pwd@example.com:123/a/b.c/d.e&foo=bar&bar=baz&foo=bar" in
  let url = parse raw in
  assert(Scheme "https"       = url.scheme);
  assert(Uid    "uid"         = url.uid);
  assert(Pwd    "pwd"         = url.pwd);
  assert(Host   "example.com" = url.host);
  assert(Port   "123"         = url.port);
  assert(Path   "/a/b.c/d.e"  = url.path);
  assert(                   3 = List.length url.query);
  let p0 = List.hd url.query in
  assert(Name   "foo"         = p0.name);
  assert(Value  "bar"         = p0.value);


D version.go => version.go +0 -3
@@ 1,3 0,0 @@
package main

const version = "0.0.1"