32 files changed, 1305 insertions(+), 1541 deletions(-)
M .gitignore
M README.md
A bindata.go
R pinboard.sh => build.sh
M doap.rdf
A model.go
A model_test.go
A openapi.yaml
D patches/sebsauvage/Shaarli/archive/master/001.patch
M pinboard.go
M pinboard_test.go
A post.msc
A post.png
D scripts/assert.sh
D scripts/categories.rng
D scripts/download.sh
D scripts/form2post.rb
D scripts/run-tests.sh
D scripts/tagcloud-html2atom.xslt
D tests/test-atom-empty.sh
D tests/test-delete-all-ok.sh.xslt
D tests/test-delete-ok.sh.xslt
D tests/test-index.sh
D tests/test-login-ok.sh
D tests/test-pinboard-info.sh
D tests/test-post.sh
D tests/test-tagcloud.sh
D tests/test-title.sh
D tests/test_delete-all-ok.sh
D tests/test_delete-ok.sh
D tests/test_login-fail.sh
D tmp/.gitkeep
M .gitignore => .gitignore +1 -1
@@ 3,4 3,4 @@ scripts/response.rnc
scripts/categories.rnc
source.tar.gz
tmp/
-pinboard-linux-amd64-*.gz>
\ No newline at end of file
+pinboard4shaarli-linux-amd64-*.gz
M README.md => README.md +44 -2
@@ 1,5 1,47 @@
-# Shaarli-API-test
+# pinboard4shaarli.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)
-See https://github.com/mro/ShaarliOS/issues/1
+## Why?
+
+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.
+
+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/).
+
+## How?
+
+You find a single, statically linked, zero-dependencies ([Go](https://golang.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.
+
+![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.
+
+Just the delicious API calls in [openapi.yaml](openapi.yaml)
+
+The API server code (delicious implementation) is the same as in [ShaarliGo](https://code.mro.name/mro/ShaarliGo).
+
+## Design Goals
+
+| Quality | very good | good | normal | irrelevant |
+|-----------------|:---------:|:----:|:------:|:----------:|
+| Functionality | | | × | |
+| Reliability | × | | | |
+| Usability | | × | | |
+| Efficiency | | | × | |
+| Changeability | | × | | |
+| Portability | × | | | |
+
A bindata.go => bindata.go +258 -0
@@ 0,0 1,258 @@
+// 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, "/")...)...)
+}
R pinboard.sh => build.sh +8 -5
@@ 13,13 13,16 @@ 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
}
-PROG_NAME="pinboard"
+"$(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
@@ 30,8 33,9 @@ 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/pinboard.cgi || { echo "Aua" 1>&2 && exit 1; }
+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
@@ 45,13 49,12 @@ env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.GitSHA1=$(git rev-p
# 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" > pinboard.cgi && chmod a+x pinboard.cgi && ls -l pinboard?cgi*'"
+&& 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"
@@ 60,6 63,6 @@ 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/pinboard_cgi.gz | tee pinboard_cgi.gz | gunzip > pinboard.cgi && chmod a+x pinboard.cgi && ls -l pinboard?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 +13 -11
@@ 2,20 2,22 @@
<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/Shaarli-API-test/issues"/>
- <homepage rdf:resource="https://code.mro.name/mro/Shaarli-API-test/"/>
- <license rdf:resource="https://code.mro.name/mro/Shaarli-API-test/src/master/LICENSE"/>
- <maintainer rdf:resource="http://mro.name/~me"/>
- <name>Shaarli-API-test</name>
- <programming-language>shell</programming-language>
+ <bug-database rdf:resource="https://code.mro.name/mro/pin4sha/issues"/>
+ <homepage rdf:resource="https://code.mro.name/mro/pin4sha/"/>
+ <implements rdf:resource="pinboard.in/v1/openapi.yaml"/>
+ <license rdf:resource="LICENSE"/>
+ <maintainer rdf:resource="https://code.mro.name/mro"/>
+ <name>pin4sha.cgi</name>
<programming-language>golang</programming-language>
+ <programming-language>ocaml</programming-language>
<repository>
<GitRepository>
- <browse rdf:resource="https://code.mro.name/mro/Shaarli-API-test"/>
- <location rdf:resource="https://code.mro.name/mro/Shaarli-API-test.git"/>
+ <browse rdf:resource="https://code.mro.name/mro/pin4sha"/>
+ <location rdf:resource="https://code.mro.name/mro/pin4sha.git"/>
</GitRepository>
</repository>
- <short-description>Run sh test scripts against multiple PHP & http://github.com/shaarli/Shaarli versions.</short-description>
- <wiki rdf:resource="https://code.mro.name/mro/Shaarli-API-test/wiki"/>
+ <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"/>
</Project>
-</rdf:RDF>>
\ No newline at end of file
+</rdf:RDF>
A model.go => model.go +48 -0
@@ 0,0 1,48 @@
+//
+// 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"`
+}
A model_test.go => model_test.go +74 -0
@@ 0,0 1,74 @@
+//
+// 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")
+}
A openapi.yaml => openapi.yaml +412 -0
@@ 0,0 1,412 @@
+---
+openapi: 3.0.1
+info:
+ title: Subset of the Pinboard API
+ version: '1'
+ description: |-
+ The Pinboard API is a way to interact programatically with your bookmarks,
+ notes and other Pinboard data.
+
+ Wherever possible the Pinboard API uses the same syntax and method names as
+ the Delicious V1 API. See differences from Delicious for a full list of
+ areas where the APIs diverge.
+
+ Future features may be
+ - auto-fill on /posts/get for unknown urls
+ - non UTC times
+ - enclosures (images, thumbnails, audio, video)
+ contact:
+ email: pinboard-api@mro.name
+ name: mro.name
+ url: https://code.mro.name/mro/pinboard4shaarli
+externalDocs:
+ url: "../about"
+ description: DOAP RDF
+servers:
+- url: https://demo.mro.name/shaarli-v0.41b/pinboard4shaarli.cgi/v1
+- url: https://demo.mro.name/shaarli-v0.10.2/pinboard4shaarli.cgi/v1
+- url: https://demo.mro.name/shaarligo/shaarligo.cgi/v1
+- url: https://api.pinboard.in/v1
+paths:
+ "/openapi.yaml":
+ get:
+ tags:
+ - api
+ summary: API description
+ operationId: aboutOpenapi
+ responses:
+ '200':
+ description: API description
+ content:
+ text/x-yaml:
+ schema:
+ type: string
+ "/user/api_token":
+ get:
+ tags:
+ - user
+ summary: Returns the user's API token (for making API calls without a password).
+ operationId: apiToken
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ responses:
+ '200':
+ description: A auth-token
+ content:
+ application/xml:
+ schema:
+ xml:
+ name: result
+ type: string
+ example: XOG86E7JIYMI
+ application/json:
+ schema:
+ properties:
+ result:
+ example: XOG86E7JIYMI
+ '501':
+ description: |-
+ Cannot tell a token. Imagine a stateless façade hiding a shaarli backend that has no idea of such a thing as an API token.
+ Only if auth was successful, otherwise you get a 401/403.
+ content:
+ text/plain:
+ schema:
+ type: string
+ example: 501 token auth not supported, use HTTP Basic.
+ "/posts/get":
+ get:
+ tags:
+ - posts
+ summary: Returns one or more posts on a single day matching the arguments. If
+ no date or url is given, date of most recent bookmark will be used.
+ description: If called with a yet unknown URL, http GET the title and guess
+ tags from the URL
+ operationId: getPost
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ - name: tag
+ in: query
+ description: filter by up to three tags
+ schema:
+ "$ref": "#/components/schemas/tag"
+ - name: dt
+ in: query
+ description: return results bookmarked on this day
+ schema:
+ "$ref": "#/components/schemas/date"
+ - "$ref": "#/components/parameters/urlParamOpt"
+ - name: meta
+ in: query
+ description: include a change detection signature in a meta attribute
+ schema:
+ "$ref": "#/components/schemas/yes_no"
+ responses:
+ '200':
+ description: foo
+ content:
+ application/xml:
+ schema:
+ "$ref": "#/components/schemas/posts"
+ "/posts/add":
+ get:
+ tags:
+ - posts
+ summary: Add a bookmark. Arguments with shaded background are required.
+ operationId: addPost
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ - "$ref": "#/components/parameters/urlParam"
+ - name: description
+ in: query
+ description: Title of the item. This field is unfortunately named 'description'
+ for backwards compatibility with the delicious API
+ required: true
+ schema:
+ "$ref": "#/components/schemas/title"
+ - name: extended
+ in: query
+ description: Description of the item. Called 'extended' for backwards compatibility
+ with delicious API
+ schema:
+ "$ref": "#/components/schemas/text"
+ - name: tags
+ in: query
+ description: List of up to 100 tags
+ schema:
+ "$ref": "#/components/schemas/tag"
+ - name: dt
+ in: query
+ description: creation time for this bookmark. Defaults to current time. Datestamps
+ more than 10 minutes ahead of server time will be reset to current server
+ time
+ schema:
+ "$ref": "#/components/schemas/datetime"
+ - name: replace
+ in: query
+ description: Replace any existing bookmark with this URL. Default is yes.
+ If set to no, will throw an error if bookmark exists
+ schema:
+ "$ref": "#/components/schemas/yes_no"
+ - name: shared
+ in: query
+ description: Make bookmark public. Default is 'yes' unless user has enabled
+ the 'save all bookmarks as private' user setting, in which case default
+ is 'no'
+ schema:
+ "$ref": "#/components/schemas/yes_no"
+ - name: toread
+ in: query
+ description: Marks the bookmark as unread. Default is 'no'
+ schema:
+ "$ref": "#/components/schemas/yes_no"
+ responses:
+ '200':
+ "$ref": "#/components/responses/Done"
+ "/posts/delete":
+ get:
+ tags:
+ - posts
+ summary: Delete a bookmark.
+ operationId: deletePost
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ - "$ref": "#/components/parameters/urlParam"
+ responses:
+ '200':
+ "$ref": "#/components/responses/Done"
+ "/tags/get":
+ get:
+ tags:
+ - tags
+ summary: Returns a full list of the user's tags along with the number of times
+ they were used.
+ operationId: getTags
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ responses:
+ '200':
+ description: All tags
+ content:
+ application/xml:
+ schema:
+ xml:
+ name: tags
+ properties:
+ tag:
+ type: array
+ items:
+ type: object
+ properties:
+ count:
+ xml:
+ attribute: true
+ type: integer
+ example: 5
+ tag:
+ xml:
+ attribute: true
+ example: foo
+ "/tags/delete":
+ get:
+ tags:
+ - tags
+ summary: Delete an existing tag.
+ operationId: deleteTag
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ - name: tag
+ in: query
+ required: true
+ schema:
+ "$ref": "#/components/schemas/tag"
+ responses:
+ '200':
+ "$ref": "#/components/responses/Done"
+ "/tags/rename":
+ get:
+ tags:
+ - tags
+ summary: Rename an tag, or fold it in to an existing tag.
+ operationId: renameTag
+ parameters:
+ - "$ref": "#/components/parameters/formatParam"
+ - name: old
+ in: query
+ description: 'note: match is not case sensitive'
+ required: true
+ schema:
+ "$ref": "#/components/schemas/tag"
+ - name: new
+ in: query
+ description: if empty, nothing will happen
+ required: true
+ schema:
+ "$ref": "#/components/schemas/tag"
+ responses:
+ '200':
+ "$ref": "#/components/responses/Done"
+components:
+ parameters:
+ formatParam:
+ name: format
+ in: query
+ schema:
+ "$ref": "#/components/schemas/format"
+ urlParam:
+ name: url
+ in: query
+ required: true
+ schema:
+ "$ref": "#/components/schemas/URL"
+ urlParamOpt:
+ name: url
+ in: query
+ schema:
+ "$ref": "#/components/schemas/URL"
+ responses:
+ Done:
+ description: A normal response
+ content:
+ application/xml:
+ schema:
+ xml:
+ name: result
+ properties:
+ code:
+ example:
+ - done
+ - something went wrong
+ xml:
+ attribute: true
+ schemas:
+ tag:
+ type: string
+ description: up to 255 characters. May not contain commas or whitespace. Please
+ be aware that tags beginning with a period are treated as private and trigger
+ special private tag semantics.
+ URL:
+ type: string
+ description: as defined by RFC 3986. Allowed schemes are http, https, javascript,
+ mailto, ftp and file. The Safari-specific feed scheme is allowed but will
+ be treated as a synonym for http.
+ title:
+ type: string
+ description: up to 255 characters long
+ text:
+ type: string
+ description: up to 65536 characters long. Any URLs will be auto-linkified when
+ displayed.
+ datetime:
+ type: string
+ description: 'UTC timestamp in this format: 2010-12-11T19:48:02Z. Valid date
+ range is Jan 1, 1 AD to January 1, 2100 (but see note below about future timestamps).'
+ example: '2010-12-11T19:48:02Z'
+ date:
+ type: string
+ description: 'UTC date in this format: 2010-12-11. Same range as datetime above'
+ example: '2010-12-11'
+ yes_no:
+ type: string
+ description: the literal string 'yes' or 'no'
+ example: 'no'
+ md5:
+ type: string
+ description: 32 character hexadecimal MD5 hash
+ integer:
+ type: integer
+ description: integer in the range 0..2^32
+ format:
+ type: string
+ description: the literal string 'json' or 'xml'
+ example: json
+ default: xml
+ posts:
+ type: array
+ properties:
+ dt:
+ type: string
+ xml:
+ attribute: true
+ example: '2010-12-11T19:48:02Z'
+ tag:
+ type: string
+ xml:
+ attribute: true
+ example: webdev
+ user:
+ type: string
+ xml:
+ attribute: true
+ example: user
+ items:
+ type: object
+ xml:
+ name: post
+ properties:
+ href:
+ type: string
+ xml:
+ attribute: true
+ example: http://www.howtocreate.co.uk/tutorials/texterise.php?dom=1
+ description:
+ type: string
+ xml:
+ attribute: true
+ example: JavaScript DOM reference
+ extended:
+ type: string
+ xml:
+ attribute: true
+ example: dom reference
+ hash:
+ type: string
+ xml:
+ attribute: true
+ example: c0238dc0c44f07daedd9a1fd9bbdeebd
+ meta:
+ type: string
+ xml:
+ attribute: true
+ example: 92959a96fd69146c5fe7cbde6e5720f2
+ others:
+ type: string
+ xml:
+ attribute: true
+ example: 55
+ tag:
+ type: string
+ xml:
+ attribute: true
+ example: dom javascript webdev
+ time:
+ type: string
+ xml:
+ attribute: true
+ example: '2005-11-28T05:26:09.000Z'
+ securitySchemes:
+ BasicAuth:
+ type: http
+ scheme: basic
+ AuthToken:
+ type: apiKey
+ in: query
+ name: auth_token
+ description: |-
+ An authentication token is a short opaque identifier in the form
+ 'username:TOKEN'.
+
+ Users can find their API token on their settings page. They can request
+ a new token at any time; this will invalidate their previous API token.
+
+ Any third-party sites making API requests on behalf of Pinboard users
+ from an outside server MUST use this authentication method instead of
+ storing the user's password. Violators will be blocked from using the
+ API.
+security:
+- AuthToken: []
+- BasicAuth: []
+tags:
+- name: api
+ externalDocs:
+ url: https://pinboard.in/api
+ description: The original Pinboard API
D patches/sebsauvage/Shaarli/archive/master/001.patch => patches/sebsauvage/Shaarli/archive/master/001.patch +0 -66
@@ 1,66 0,0 @@
-diff --git a/index.php b/index.php
-index c102e42..bafec87 100644
---- a/index.php
-+++ b/index.php
-@@ -43,7 +43,7 @@ define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUES
- // Force cookie path (but do not change lifetime)
- $cookie=session_get_cookie_params();
- $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
--session_set_cookie_params($cookie['lifetime'],$cookiedir,$_SERVER['HTTP_HOST']); // Set default cookie expiration and path.
-+session_set_cookie_params($cookie['lifetime'],$cookiedir,$_SERVER['SERVER_NAME']); // Set default cookie expiration and path.
-
- // Set session parameters on server side.
- define('INACTIVITY_TIMEOUT',3600); // (in seconds). If the user does not access any page within this time, his/her session is considered expired.
-@@ -413,14 +413,14 @@ if (isset($_POST['login']))
- $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // Set session expiration on server-side.
-
- $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
-- session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['HTTP_HOST']); // Set session cookie expiration on client side
-+ session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
- // Note: Never forget the trailing slash on the cookie path !
- session_regenerate_id(true); // Send cookie with new expiration date to browser.
- }
- else // Standard session expiration (=when browser closes)
- {
- $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
-- session_set_cookie_params(0,$cookiedir,$_SERVER['HTTP_HOST']); // 0 means "When browser closes"
-+ session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes"
- session_regenerate_id(true);
- }
- // Optional redirect after login:
-@@ -452,7 +452,7 @@ function serverUrl()
- {
- $https = (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS'])=='on')) || $_SERVER["SERVER_PORT"]=='443'; // HTTPS detection.
- $serverport = ($_SERVER["SERVER_PORT"]=='80' || ($https && $_SERVER["SERVER_PORT"]=='443') ? '' : ':'.$_SERVER["SERVER_PORT"]);
-- return 'http'.($https?'s':'').'://'.$_SERVER['HTTP_HOST'].$serverport;
-+ return 'http'.($https?'s':'').'://'.$_SERVER['SERVER_NAME'].$serverport;
- }
-
- // Returns the absolute URL of current script, without the query.
-@@ -1302,7 +1302,7 @@ function renderPage()
- if (is_numeric($_GET['linksperpage'])) { $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); }
- // Make sure the referer is from Shaarli itself.
- $referer = '?';
-- if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['HTTP_HOST'])==0)
-+ if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['SERVER_NAME'])==0)
- $referer = $_SERVER['HTTP_REFERER'];
- header('Location: '.$referer);
- exit;
-@@ -1321,7 +1321,7 @@ function renderPage()
- }
- // Make sure the referer is from Shaarli itself.
- $referer = '?';
-- if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['HTTP_HOST'])==0)
-+ if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['SERVER_NAME'])==0)
- $referer = $_SERVER['HTTP_REFERER'];
- header('Location: '.$referer);
- exit;
-@@ -2059,7 +2059,7 @@ function lazyThumbnail($url,$href=false)
- function install()
- {
- // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
-- if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
-+ if (endsWith($_SERVER['SERVER_NAME'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
-
-
- // This part makes sure sessions works correctly.
M pinboard.go => pinboard.go +336 -284
@@ 1,5 1,5 @@
//
-// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/Shaarli-API-test
+// 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
@@ 18,7 18,9 @@
package main
import (
+ "bytes"
"encoding/xml"
+ "fmt"
"io"
"log"
"net/http"
@@ 27,6 29,7 @@ import (
"net/url"
"os"
"path"
+ "path/filepath"
"strings"
"time"
@@ 51,119 54,98 @@ 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)
}
- if err := cgi.Serve(http.HandlerFunc(handleMux)); err != nil {
+ // - 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)
}
}
-// https://pinboard.in/api
-func handleMux(w http.ResponseWriter, r *http.Request) {
- raw := func(s ...string) {
- for _, txt := range s {
- io.WriteString(w, txt)
- }
+/// $ ./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
}
- elmS := func(e string, close bool, atts ...string) {
- raw("<", e)
- for i, v := range atts {
- if i%2 == 0 {
- raw(" ", v, "=")
- } else {
- raw("'")
- xml.EscapeText(w, []byte(v))
- raw("'")
- }
- }
- if close {
- raw(" /")
+
+ 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)
}
- raw(">", "\n")
+ 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)
}
- elmE := func(e string) { raw("</", e, ">", "\n") }
- defer un(trace(strings.Join([]string{"v", version, "+", GitSHA1, " ", r.RemoteAddr, " ", r.Method, " ", r.URL.String()}, "")))
- path_info := os.Getenv("PATH_INFO")
- base := *r.URL
- base.Path = path.Join(base.Path[0:len(base.Path)-len(path_info)], "..", "index.php")
+ return true
+}
- w.Header().Set(http.CanonicalHeaderKey("X-Powered-By"), strings.Join([]string{"https://code.mro.name/mro/Shaarli-API-test", "#", version, "+", GitSHA1}, ""))
- w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/xml; charset=utf-8")
+type reqWri struct {
+ r *http.Request
+ f io.Writer
+ h http.Header
+}
- // https://stackoverflow.com/a/18414432
- options := cookiejar.Options{
- PublicSuffixList: publicsuffix.List,
- }
- jar, err := cookiejar.New(&options)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+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, " "))
}
- client := http.Client{Jar: jar}
-
- switch path_info {
- case "",
- "/about":
- base := *r.URL
- base.Path = path.Join(base.Path[0:len(base.Path)-len(path_info)], "about") + "/"
- http.Redirect(w, r, base.Path, http.StatusFound)
+ fmt.Fprintf(w.f, LF)
+}
- return
- case "/about/":
- // w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/rdf+xml")
- raw(`<?xml version="1.0" encoding="utf-8"?>
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://usefulinc.com/ns/doap#">
- <Project>
- <name xml:lang="en">🛠 Shaarli Pinboard API</name>
- <short-description xml:lang="en">subset conforming https://pinboard.in/api/</short-description>
- <implements rdf:resource="https://pinboard.in/api/"/>
- <platform rdf:resource="https://sebsauvage.net/wiki/doku.php?id=php:shaarli"/>
- <homepage rdf:resource="https://code.mro.name/mro/Shaarli-API-test/"/>
- <bug-database rdf:resource="https://code.mro.name/mro/Shaarli-API-test/issues"/>
- <wiki rdf:resource="https://code.mro.name/mro/Shaarli-API-test/wiki"/>
- <license rdf:resource="https://code.mro.name/mro/Shaarli-API-test/src/master/LICENSE"/>
- <maintainer rdf:resource="http://mro.name/~me"/>
- <programming-language>golang</programming-language>
- <category>microblogging</category>
- <category>shaarli</category>
- <category>nodb</category>
- <category>api</category>
- <category>pinboard</category>
- <category>delicious</category>
- <category>cgi</category>
- <repository>
- <GitRepository>
- <browse rdf:resource="https://code.mro.name/mro/Shaarli-API-test"/>
- <location rdf:resource="https://code.mro.name/mro/Shaarli-API-test.git"/>
- </GitRepository>
- </repository>
- <release>
- <Version>
- <name>`, version, "+", GitSHA1, `</name>
- <revision>`, GitSHA1, `</revision>
- <description>…</description>
- </Version>
- </release>
- </Project>
-</rdf:RDF>`)
+// 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()}, "")))
- 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.StatusUnauthorized)
- return
- }
+ 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)
@@ 171,151 153,173 @@ func handleMux(w http.ResponseWriter, r *http.Request) {
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)
+ jar, err := cookiejar.New(&options)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- p_description := params["description"][0]
+ client := http.Client{Jar: jar, Timeout: 2 * time.Second}
- p_extended := ""
- if 1 == len(params["extended"]) {
- p_extended = params["extended"][0]
- }
-
- p_tags := ""
- if 1 == len(params["tags"]) {
- p_tags = params["tags"][0]
+ 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)
+ }
}
- v := url.Values{}
- v.Set("post", p_url)
- v.Set("title", p_description)
- base.RawQuery = v.Encode()
+ base := *r.URL
+ base.Path = path.Join(base.Path[0:len(base.Path)-len(path_info)], "..", "index.php")
- resp, err := client.Get(base.String())
- 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)
+ switch path_info {
+ case "":
+ http.Redirect(w, r, "about", http.StatusFound)
return
- }
-
- formLogi.Set("login", uid)
- formLogi.Set("password", pwd)
- resp, err = client.PostForm(resp.Request.URL.String(), formLogi)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadGateway)
+ case "/about":
+ asset("doap.rdf", "application/rdf+xml")
return
- }
-
- formLink, err := formValuesFromReader(resp.Body, "linkform")
- resp.Body.Close()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadGateway)
+ case "/v1":
+ http.Redirect(w, r, "v1/openapi.yaml", http.StatusFound)
return
- }
- // if we do not have a linkform, auth must have failed.
- if 0 == len(formLink) {
- http.Error(w, "Authentication failed", http.StatusForbidden)
+ case "/v1/openapi.yaml":
+ asset("openapi.yaml", "text/x-yaml; charset=utf-8")
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")
- }
- resp, err = client.PostForm(resp.Request.URL.String(), formLink)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadGateway)
- return
- }
- resp.Body.Close()
+ // 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
+ }
- raw(xml.Header)
- elmS("result", true,
- "code", "done")
+ 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
+ }
- return
- case "/v1/posts/delete":
- _, _, ok := r.BasicAuth()
- if !ok {
- http.Error(w, "Basic Pre-Authentication required.", http.StatusUnauthorized)
- return
- }
+ formLogi.Set("login", uid)
+ formLogi.Set("password", pwd)
- 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
- }
+ 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
+ }
- params := r.URL.Query()
- if 1 != len(params["url"]) {
- http.Error(w, "Required parameter missing: url", http.StatusBadRequest)
- return
- }
- // p_url := params["url"][0]
+ 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
+ }
- elmS("result", true,
- "code", "not implemented yet")
- return
- case "/v1/posts/update":
- _, _, ok := r.BasicAuth()
- if !ok {
- http.Error(w, "Basic Pre-Authentication required.", http.StatusUnauthorized)
- return
- }
+ fv := func(s string) string { return formLink.Get(s) }
- 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
- }
+ 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
+ }
- raw(xml.Header)
- elmS("update", true,
- "time", "2011-03-24T19:02:07Z")
- return
- 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.StatusUnauthorized)
- 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()
- 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
- }
+ 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]
+ 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
@@ 331,82 335,130 @@ func handleMux(w http.ResponseWriter, r *http.Request) {
if 1 == len(params["tags"]) {
p_tags = params["tags"][0]
}
- */
- v := url.Values{}
- v.Set("post", p_url)
- base.RawQuery = v.Encode()
+ v := url.Values{}
+ v.Set("post", p_url)
+ v.Set("title", p_description)
+ base.RawQuery = v.Encode()
- resp, err := client.Get(base.String())
- 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
- }
+ 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)
- resp, err = client.PostForm(resp.Request.URL.String(), formLogi)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadGateway)
- return
- }
+ formLogi.Set("login", uid)
+ formLogi.Set("password", pwd)
- 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
- }
+ 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
+ }
- fv := func(s string) string { return formLink.Get(s) }
+ 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()
- 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
- }
+ case
+ "/v1/posts/delete":
+ _, _, ok := r.BasicAuth()
+ if !ok {
+ http.Error(w, "Basic Pre-Authentication required.", http.StatusUnauthorized)
+ return
+ }
- raw(xml.Header)
- elmS("posts", false,
- "user", uid,
- "dt", tim.Format(IsoDate),
- "tag", fv("lf_tags"))
- elmS("post", true,
- "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),
- "others", "0")
- elmE("posts")
+ params := r.URL.Query()
+ if 1 != len(params["url"]) {
+ http.Error(w, "Required parameter missing: url", http.StatusBadRequest)
+ return
+ }
+ // p_url := params["url"][0]
- return
- case "/v1/posts/recent",
- "/v1/posts/dates",
- "/v1/posts/suggest",
- "/v1/tags/get",
- "/v1/tags/delete",
- "/v1/tags/rename",
- "/v1/user/secret",
- "/v1/user/api_token",
- "/v1/notes/list",
- "/v1/notes/ID":
- http.Error(w, "Not Implemented", http.StatusNotImplemented)
- return
+ 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)
}
- http.NotFound(w, r)
}
func formValuesFromReader(r io.Reader, name string) (ret url.Values, err error) {
M pinboard_test.go => pinboard_test.go +75 -1
@@ 1,5 1,5 @@
//
-// Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/Shaarli-API-test
+// 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
@@ 18,15 18,77 @@
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()
@@ 48,6 110,18 @@ func TestURL(t *testing.T) {
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")
A post.msc => post.msc +36 -0
@@ 0,0 1,36 @@
+#
+# http://www.mcternan.me.uk/mscgen/
+# mscgen -T png post.msc ; optipng -o 7 post.png
+msc {
+ hscale = "1";
+
+ ios [label="Client"],
+ cgi [label="pinboard4shaarli.cgi"],
+ php [label="shaarli.php"],
+ www [label="www.example.com"];
+
+ ios -> cgi [ label = "GET get?url=..." ];
+ cgi -> php [ label = "GET post=..." ];
+ cgi << php [ label = "loginform"];
+ cgi -> php [ label = "POST login" ];
+ cgi << php [ label = "301" ];
+ cgi -> php [ label = "GET post=..." ];
+ php -> www [ label = "GET" ];
+ php << www [ label = "<title>" ];
+ cgi << php [ label = "linkform" ];
+ ios << cgi [ label = "cookie+token?" ];
+ ...;
+ --- [ label = "manually edit description & tags" ];
+ ios -> cgi [ label = "GET add?url=..." ];
+ cgi -> php [ label = "GET post=..." ];
+ cgi << php [ label = "loginform"];
+ cgi -> php [ label = "POST login" ];
+ cgi << php [ label = "301" ];
+ cgi -> php [ label = "GET post=..." ];
+ php -> www [ label = "GET" ];
+ php << www [ label = "<title>" ];
+ cgi << php [ label = "linkform"];
+ cgi -> php [ label = "POST post=..." ];
+ cgi << php;
+ ios << cgi;
+}
A post.png => post.png +0 -0
D scripts/assert.sh => scripts/assert.sh +0 -61
@@ 1,61 0,0 @@
-#
-# Copyright (c) 2015 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-
-# insipired by https://github.com/lehmannro/assert.sh but much more primitive.
-
-# terminal colors (require bash)
-# http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html
-# http://wiki.bash-hackers.org/scripting/terminalcodes
-FGC_NONE="\033[0m"
-FGC_GRAY="\033[1;30m"
-FGC_RED="\033[1;31m"
-FGC_GREEN="\033[1;32m"
-FGC_YELLOW="\033[1;33m"
-FGC_BLUE="\033[1;34m"
-FGC_PURPLE="\033[1;35m"
-FGC_CYAN="\033[1;36m"
-FGC_WHITE="\033[1;37m"
-BGC_GRAY="\033[7;30m"
-BGC_RED="\033[7;31m"
-BGC_GREEN="\033[7;32m"
-BGC_YELLOW="\033[7;33m"
-BGC_BLUE="\033[7;34m"
-BGC_PURPLE="\033[7;35m"
-BGC_CYAN="\033[7;36m"
-BGC_WHITE="\033[7;37m"
-
-assert_fail() {
- local CODE="${1}" MESSAGE="${2}"
- echo -e "${BGC_RED}assert_fail (${CODE}): ${MESSAGE}${FGC_NONE}"
- exit $1
-}
-
-assert_equal() {
- local EXPECTED="${1}" ACTUAL="${2}" CODE="${3}" MESSAGE="${4}"
- if [ ! "${EXPECTED}" = "${ACTUAL}" ] ; then
- echo -e "${BGC_RED}assert_equal${FGC_NONE} (${CODE}): ${MESSAGE} expected: \"${FGC_GREEN}${EXPECTED}${FGC_NONE}\" != actual: \"${FGC_RED}${ACTUAL}${FGC_NONE}\""
- exit $3
- fi
-}
-
-assert_fgrep() {
- local PATTERN="${1}" ACTUAL="${2}" CODE="${3}" MESSAGE="${4}"
- echo "${ACTUAL}" | fgrep "${PATTERN}" 1> /dev/null 2>&1 || { \
- echo -e "${BGC_RED}assert_equal${FGC_NONE} (${CODE}): ${MESSAGE} pattern: \"${FGC_GREEN}${PATTERN}${FGC_NONE}\" != actual: \"${FGC_RED}${ACTUAL}${FGC_NONE}\"" \
- && exit $3 ; \
- }
-}
D scripts/categories.rng => scripts/categories.rng +0 -139
@@ 1,139 0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- https://tools.ietf.org/html/rfc5023#appendix-B
- not: https://web.archive.org/web/20130716150512/http://www.atomenabled.org:80/developers/protocol/atom-protocol-spec.php#rfc.section.8.3.3
--->
-<!-- -*- rnc -*- # RELAX NG Compact Syntax Grammar for the Atom Protocol -->
-<grammar xmlns:app="http://www.w3.org/2007/app" ns="http://www.w3.org/2005/Atom" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
- <start>
- <ref name="appCategories"/>
- </start>
- <define name="atomCommonAttributes">
- <optional>
- <attribute name="xml:base">
- <ref name="atomURI"/>
- </attribute>
- </optional>
- <optional>
- <attribute name="xml:lang">
- <ref name="atomLanguageTag"/>
- </attribute>
- </optional>
- <zeroOrMore>
- <ref name="undefinedAttribute"/>
- </zeroOrMore>
- </define>
- <define name="undefinedAttribute">
- <attribute>
- <anyName>
- <except>
- <name>xml:base</name>
- <name>xml:lang</name>
- <nsName ns=""/>
- </except>
- </anyName>
- </attribute>
- </define>
- <define name="atomURI">
- <text/>
- </define>
- <define name="atomLanguageTag">
- <data type="string">
- <param name="pattern">([A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*)?</param>
- </data>
- </define>
- <define name="atomCategory">
- <element name="atom:category">
- <ref name="atomCommonAttributes"/>
- <attribute name="term"/>
- <optional>
- <attribute name="scheme">
- <ref name="atomURI"/>
- </attribute>
- </optional>
- <optional>
- <attribute name="label"/>
- </optional>
- <ref name="undefinedContent"/>
- </element>
- </define>
- <define name="appInlineCategories">
- <element name="app:categories">
- <optional>
- <attribute name="fixed">
- <choice>
- <value>yes</value>
- <value>no</value>
- </choice>
- </attribute>
- </optional>
- <optional>
- <attribute name="scheme">
- <ref name="atomURI"/>
- </attribute>
- </optional>
- <group>
- <zeroOrMore>
- <ref name="atomCategory"/>
- </zeroOrMore>
- <ref name="undefinedContent"/>
- </group>
- </element>
- </define>
- <define name="appOutOfLineCategories">
- <element name="app:categories">
- <attribute name="href">
- <ref name="atomURI"/>
- </attribute>
- <empty/>
- </element>
- </define>
- <define name="appCategories">
- <choice>
- <ref name="appInlineCategories"/>
- <ref name="appOutOfLineCategories"/>
- </choice>
- </define>
- <!-- Extensibility -->
- <define name="undefinedContent">
- <zeroOrMore>
- <choice>
- <text/>
- <ref name="anyForeignElement"/>
- </choice>
- </zeroOrMore>
- </define>
- <define name="anyElement">
- <element>
- <anyName/>
- <zeroOrMore>
- <choice>
- <attribute>
- <anyName/>
- </attribute>
- <text/>
- <ref name="anyElement"/>
- </choice>
- </zeroOrMore>
- </element>
- </define>
- <define name="anyForeignElement">
- <element>
- <anyName>
- <except>
- <nsName/>
- </except>
- </anyName>
- <zeroOrMore>
- <choice>
- <attribute>
- <anyName/>
- </attribute>
- <text/>
- <ref name="anyElement"/>
- </choice>
- </zeroOrMore>
- </element>
- </define>
-</grammar>
-<!-- EOF -->
D scripts/download.sh => scripts/download.sh +0 -24
@@ 1,24 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/.." || exit 1
-
-[ "${GITHUB}" != "" ] || { echo 'I need ${GITHUB}, e.g. shaarli/Shaarli/archive/v0.0.40beta' && exit 2; }
-
-# Download the tarball...
-GITHUB_SRC_URL="https://github.com/${GITHUB}.tar.gz"
-curl --location --output source.tar.gz --url "${GITHUB_SRC_URL}" || { echo "ouch" && exit 3; }
D scripts/form2post.rb => scripts/form2post.rb +0 -23
@@ 1,23 0,0 @@
-#!/usr/bin/env ruby
-#
-# Copyright (c) 2015 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-require 'rexml/document'
-require 'cgi'
-
-REXML::Document.new($stdin).elements.each('//textarea | //input[@type = "text" or @type = "hidden"]') do |element|
- puts "#{CGI.escape(element.attributes['name'] || '')}=#{CGI.escape(element.attributes['value'] || '')}&"
-end
D scripts/run-tests.sh => scripts/run-tests.sh +0 -126
@@ 1,126 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-
-# Check preliminaries
-curl --version >/dev/null || { echo "I need curl." && exit 101 ; }
-xmllint --version 2> /dev/null || { echo "I need xmllint." && exit 102 ; }
-ruby --version > /dev/null || { echo "I need ruby." && exit 103 ; }
-
-[ "${GITHUB_SRC_SUBDIR}" != "" ] || { echo 'I need ${GITHUB_SRC_SUBDIR}, e.g. Shaarli-*' && exit 2; }
-[ "${BASE_URL}" != "" ] || { echo 'I need ${BASE_URL}, e.g. http://127.0.0.1:8000' && exit 2; }
-[ "${USERNAME}" != "" ] || { echo 'I need ${USERNAME}, e.g. tast' && exit 2; }
-[ "${PASSWORD}" != "" ] || { echo 'I need ${PASSWORD}, e.g. tust' && exit 2; }
-
-cd "$(dirname "$0")/.." || exit 1
-CWD="$(pwd)"
-WORK_DIR="${CWD}/tmp"
-
-# terminal colors (require bash)
-# http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html
-# http://wiki.bash-hackers.org/scripting/terminalcodes
-FGC_NONE="\033[0m"
-FGC_GRAY="\033[1;30m"
-FGC_RED="\033[1;31m"
-FGC_GREEN="\033[1;32m"
-FGC_YELLOW="\033[1;33m"
-FGC_BLUE="\033[1;34m"
-FGC_PURPLE="\033[1;35m"
-FGC_CYAN="\033[1;36m"
-FGC_WHITE="\033[1;37m"
-BGC_GRAY="\033[7;30m"
-BGC_RED="\033[7;31m"
-BGC_GREEN="\033[7;32m"
-BGC_YELLOW="\033[7;33m"
-BGC_BLUE="\033[7;34m"
-BGC_PURPLE="\033[7;35m"
-BGC_CYAN="\033[7;36m"
-BGC_WHITE="\033[7;37m"
-
-echo "\$ curl --version" ; curl --version
-
-status_code=0
-test_counter=1
-echo "1..$(ls "${CWD}/tests"/test-*.sh | wc -l)"
-for tst in "${CWD}/tests"/test-*.sh
-do
- test_name="$(basename "${tst}")"
- echo -n "travis_fold:start:${test_name}\r"
- echo -n "# run ${test_counter} - ${test_name} "
-
- # prepare a clean test environment from scratch
- cd "${CWD}"
- rm -rf "${WORK_DIR}" && mkdir "${WORK_DIR}"
- cd "${WORK_DIR}"
-
- # ...and unpack into directory 'WebAppRoot'...
- tar -xzf "${CWD}/source.tar.gz" || { echo "ouch" && exit 1 ; }
- mv ${GITHUB_SRC_SUBDIR} "WebAppRoot"
- cp "${CWD}/pinboard.cgi" "WebAppRoot/"
-
- for patchfile in "${CWD}/patches/${GITHUB}"/*.patch
- do
- [ -r "${patchfile}" ] && patch -p1 -d "WebAppRoot" < "${patchfile}"
- done
-
- # https://github.com/shaarli/Shaarli/issues/613
- cd "WebAppRoot" && composer update --no-dev ; cd "${WORK_DIR}"
-
- # http://robbiemackay.com/2013/05/03/automating-behat-and-mink-tests-with-travis-ci/
- # webserver setup
- php -S 127.0.0.1:8000 -t "WebAppRoot" 1> php.stdout 2> php.stderr &
- sleep 1 # how could we get rid of this stupid sleep?
-
- ls -l "WebAppRoot/index.php" >/dev/null || { echo "ouch" && exit 2 ; }
-
- curl --silent --show-error \
- --url "${BASE_URL}" \
- --data-urlencode "setlogin=${USERNAME}" \
- --data-urlencode "setpassword=${PASSWORD}" \
- --data-urlencode "continent=Europe" \
- --data-urlencode "city=Brussels" \
- --data-urlencode "title=Review Shaarli" \
- --data-urlencode "Save=Save config" \
- --output /dev/null
-
- # execute each test
- /usr/bin/env bash "${tst}"
- code=$?
-
- killall php 1>/dev/null 2>&1
- wait
- cd "${WORK_DIR}"
-
- if [ ${code} -ne 0 ] ; then
- for f in curl.* WebAppRoot/data/log.txt WebAppRoot/data/ipbans.php WebAppRoot/data/config.php ; do
- printf " _\$_cat%-50s\n" "_${f}_" | tr ' _' '# '
- cat "${f}"
- done
- echo " "
- fi
- echo -n "travis_fold:end:${test_name}\r"
-
- if [ ${code} -eq 0 ] ; then
- echo "${FGC_GREEN}ok ${test_counter}${FGC_NONE} - ${test_name}"
- else
- echo "${FGC_RED}not ok ${test_counter}${FGC_NONE} - ${test_name} (code: ${code})"
- status_code=1
- fi
- test_counter=$((test_counter+1))
-done
-
-exit ${status_code}
D scripts/tagcloud-html2atom.xslt => scripts/tagcloud-html2atom.xslt +0 -47
@@ 1,47 0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- $ curl "https://links.mro.name/?do=tagcloud" | xsltproc - -html tagcloud-html2atom.xslt - | xmllint - -relaxng categories.rng -
-
- http://www.w3.org/TR/xslt/
- http://www.w3.org/TR/xpath/
- https://tools.ietf.org/html/rfc5023#appendix-B
--->
-<xsl:stylesheet
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:dctype="http://purl.org/dc/dcmitype/"
- xmlns="http://www.w3.org/2005/Atom"
- xmlns:app="http://www.w3.org/2007/app"
- xmlns:foo="foo"
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
- exclude-result-prefixes="dctype rdf"
- version="1.0">
-
- <xsl:output method="xml" indent="yes"/>
-
- <xsl:template match="/html">
- <xsl:comment>
- https://tools.ietf.org/html/rfc5023#appendix-B
- http://atomenabled.org/developers/protocol/#appCategories1
- https://web.archive.org/web/20130716150512/http://www.atomenabled.org:80/developers/protocol/atom-protocol-spec.php#rfc.section.8.3.3
- https://web.archive.org/web/20130716150512/http://www.atomenabled.org:80/developers/protocol/atom-protocol-spec.php#schema
-</xsl:comment>
- <app:categories scheme="?searchtags=">
- <xsl:variable name="traditional_count_prefix" select="1 = count(.//a[starts-with(@href, '?searchtags=')][1]/preceding-sibling::*[1])"/>
-
- <xsl:for-each select=".//a[starts-with(@href, '?searchtags=')]">
- <xsl:sort select="." data-type="text" order="ascending"/>
-
- <xsl:variable name="count">
- <xsl:choose>
- <xsl:when test="$traditional_count_prefix"><xsl:value-of select="preceding-sibling::*[1]"/></xsl:when>
- <xsl:otherwise><xsl:value-of select="following-sibling::*[1]"/></xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
-
- <category term="{.}" foo:count="{$count}"/>
- </xsl:for-each>
- </app:categories>
- </xsl:template>
-
-</xsl:stylesheet>
D tests/test-atom-empty.sh => tests/test-atom-empty.sh +0 -28
@@ 1,28 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${BASE_URL}" != "" ] || assert_fail 1 "How strange, BASE_URL is unset."
-
-curl --silent --show-error --output "curl.tmp.atom" "${BASE_URL}/?do=atom"
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' "curl.tmp.atom")
-assert_equal 1 "${entries}" 28 "expected exactly one <entry>"
D tests/test-delete-all-ok.sh.xslt => tests/test-delete-all-ok.sh.xslt +0 -1
@@ 1,1 0,0 @@
-test-delete-ok.sh.xslt>
\ No newline at end of file
D tests/test-delete-ok.sh.xslt => tests/test-delete-ok.sh.xslt +0 -47
@@ 1,47 0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
- Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-
- 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/>.
-
-
- Find all 'delete_link' POST forms and list lf_linkdate and token.
-
- $ xsltproc - -html ../tests/test-delete-ok.sh.xslt curl.tmp.html
-
- http://www.w3.org/TR/xslt
--->
-<xsl:stylesheet
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
- exclude-result-prefixes="xsl"
- version="1.0">
- <xsl:output method="text"/>
-
- <xsl:template match="/">
- <xsl:for-each select="html/body//form[.//input/@name='delete_link' and .//input/@name='lf_linkdate' and .//input/@name='token']">
- <xsl:variable name="lf_linkdate" select=".//input[@name='lf_linkdate']/@value"/>
- <xsl:variable name="token" select=".//input[@name='token']/@value"/>
- <xsl:value-of select="$lf_linkdate"/><xsl:text> </xsl:text><xsl:value-of select="$token"/><xsl:text>
-</xsl:text>
- </xsl:for-each>
- <xsl:for-each select="html/body//a[contains(@href, '?delete_link=') and contains(@href, '&token=')]">
- <xsl:variable name="lf_linkdate" select="substring-before(substring-after(@href,'?delete_link='), '&token=')"/>
- <xsl:variable name="token" select="substring-after(@href,'&token=')"/>
- <xsl:value-of select="$lf_linkdate"/><xsl:text> </xsl:text><xsl:value-of select="$token"/><xsl:text>
-</xsl:text>
- </xsl:for-each>
- </xsl:template>
-
-</xsl:stylesheet>
D tests/test-index.sh => tests/test-index.sh +0 -28
@@ 1,28 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${BASE_URL}" != "" ] || assert_fail 1 "How strange, BASE_URL is unset."
-
-curl --silent "${BASE_URL}/" | xmllint --html --encode utf8 --format - 2>/dev/null >/dev/null
-
-exit $?
D tests/test-login-ok.sh => tests/test-login-ok.sh +0 -90
@@ 1,90 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${USERNAME}" != "" ] || assert_fail 1 "How strange, USERNAME is unset."
-[ "${PASSWORD}" != "" ] || assert_fail 2 "How strange, PASSWORD is unset."
-[ "${BASE_URL}" != "" ] || assert_fail 3 "How strange, BASE_URL is unset."
-
-echo "###################################################"
-echo "## non-logged-in GET /?post return: 302 "
-http_code=$(curl --url "${BASE_URL}/?post" \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{http_code}' 2>/dev/null)
-assert_equal 302 "${http_code}" 35 "login check."
-
-echo "####################################################"
-echo "## Step 1: fetch token to login "
-echo "GET ${BASE_URL}?do=login"
-rm curl.tmp.*
-# http://unix.stackexchange.com/a/157219
-LOCATION=$(curl --get --url "${BASE_URL}/?do=login" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 58 "error: '${errmsg}'"
-TOKEN=$(xmllint --html --nowarning --xpath 'string(/html/body//form[@name="loginform"]//input[@name="token"]/@value)' curl.tmp.html)
-# string(..) http://stackoverflow.com/a/18390404
-
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 63 "found TOKEN=${TOKEN}"
-
-echo "######################################################"
-echo "## Step 2: follow the redirect, do the login and redirect to ?do=changepasswd "
-echo "POST ${LOCATION}"
-rm curl.tmp.*
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?do=changepasswd" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 80 "error during login"
-assert_equal "${BASE_URL}/?do=changepasswd" "${LOCATION}" 81 "redirect after login"
-
-# [ 1 -eq $(xmllint --html --nowarning --xpath "count(/html/body//a[@href = '?do=logout'])" curl.tmp.html 2>/dev/null) ] || assert_fail 13 "I expected a logout link."
-
-# check presence of various mandatory form fields:
-for field in oldpassword setpassword token
-do
- assert_equal 1 $(xmllint --html --nowarning --xpath "count(/html/body//form[@name = 'changepasswordform']//input[@name='${field}'])" curl.tmp.html) 88 "expected to have a '${field}'"
-done
-
-
-echo "###################################################"
-echo "## logged-in GET /?post return: 200 "
-http_code=$(curl --url "${BASE_URL}/?post" \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{http_code}' 2>/dev/null)
-assert_equal 200 "${http_code}" 90 "login check."
D tests/test-pinboard-info.sh => tests/test-pinboard-info.sh +0 -26
@@ 1,26 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2019-2019 Marcus Rohrmoser https://code.mro.name/mro/Shaarli-API-test. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${BASE_URL}" != "" ] || assert_fail 1 "How strange, BASE_URL is unset."
-
-curl --url "${BASE_URL}/pinboard.cgi/v1/info" 2>/dev/null | file -
D tests/test-post.sh => tests/test-post.sh +0 -119
@@ 1,119 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-ruby --version > /dev/null || assert_fail 103 "I need ruby."
-[ "${USERNAME}" != "" ] || assert_fail 1 "How strange, USERNAME is unset."
-[ "${PASSWORD}" != "" ] || assert_fail 2 "How strange, PASSWORD is unset."
-[ "${BASE_URL}" != "" ] || assert_fail 3 "How strange, BASE_URL is unset."
-assert_equal "" "$(echo "${BASE_URL}" | egrep -e "/$")" 28 "BASE_URL must be without trailing /"
-
-echo "###################################################"
-echo "## Non-logged-in Atom feed before adding a link (should have only the initial public default entry):"
-curl --silent --show-error --output curl.tmp.atom "${BASE_URL}/?do=atom"
-xmllint --encode utf8 --format curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal 1 "${entries}" 35 "Atom feed expected 1 = ${entries}"
-
-echo "####################################################"
-echo "## Step 1: fetch token to login and add a new link: "
-echo "GET ${BASE_URL}?post=..."
-rm curl.tmp.*
-# http://unix.stackexchange.com/a/157219
-LOCATION=$(curl --get --url "${BASE_URL}" \
- --data-urlencode "post=https://github.com/sebsauvage/Shaarli/commit/450342737ced8ef2864b4f83a4107a7fafcc4add" \
- --data-urlencode "title=Initial Commit to Shaarli on Github." \
- --data-urlencode "source=Source Text" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 52 "error: '${errmsg}'"
-TOKEN=$(xmllint --html --nowarning --xpath 'string(/html/body//form[@name="loginform"]//input[@name="token"]/@value)' curl.tmp.html)
-# string(..) http://stackoverflow.com/a/18390404
-
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 57 "expected TOKEN of 40 characters, but found ${TOKEN} of $(printf "%s" ${TOKEN} | wc -c)"
-
-echo "######################################################"
-echo "## Step 2: follow the redirect, do the login and get the post form: "
-echo "POST ${LOCATION}"
-rm curl.tmp.*
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 73 "error: '${errmsg}'"
-# check presence of various mandatory form fields:
-for field in lf_url lf_title lf_linkdate lf_tags token
-do
- assert_equal 1 $(xmllint --html --nowarning --xpath "count(/html/body//form[@name = 'linkform']//input[@name='${field}'])" curl.tmp.html) 77 "expected to have a '${field}'"
-done
-for field in lf_description
-do
- assert_equal 1 $(xmllint --html --nowarning --xpath "count(/html/body//form[@name = 'linkform']//textarea[@name='${field}'])" curl.tmp.html) 81 "expected to have a '${field}'"
-done
-
-# turn form field data into curl post data file
-xmllint --html --nowarning --xmlout curl.tmp.html | xmllint --xpath '/html/body//form[@name="linkform"]' - | /usr/bin/env ruby ../scripts/form2post.rb > curl.post
-
-echo "######################################################"
-echo "## Step 3: finally post the link: "
-echo "POST ${LOCATION}"
-rm curl.tmp.*
-LOCATION=$(curl --url "${LOCATION}" \
- --data "@curl.post" \
- --data-urlencode "lf_linkdate=20130226_100941" \
- --data-urlencode "lf_source=$0" \
- --data-urlencode "lf_description=Must be older because http://sebsauvage.github.io/Shaarli/ mentions 'Copyright (c) 2011 Sébastien SAUVAGE (sebsauvage.net)'." \
- --data-urlencode "lf_tags=t1 t2" \
- --data-urlencode "save_edit=Save" \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{redirect_url}' 2>/dev/null)
-# don't use --location and url_effective because this strips /?#... on curl 7.30.0 (x86_64-apple-darwin13.0)
-echo "final ${LOCATION}"
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html 2>/dev/null)
-assert_equal "" "${errmsg}" 106 "error: '${errmsg}'"
-echo "${LOCATION}" | egrep -e "^${BASE_URL}/\?#[a-zA-Z0-9@_-]{6}\$" || assert_fail 108 "expected link hash url, but got '${LOCATION}'"
-# don't follow the redirect => no html => no logout link [ 1 -eq "$(xmllint --html --nowarning --xpath "count(/html/body//a[@href = '?do=logout'])" curl.tmp.html 2>/dev/null)" ] || assert_fail 13 "I expected a logout link."
-
-#####################################################
-# TODO: watch out for error messages like e.g. ip bans or the like.
-
-# check post-condition - there must be more entries now:
-echo "###################################################"
-echo "## Non-logged-in Atom feed after adding a link (should have the added + the initial public default entry):"
-curl --silent --show-error --output curl.tmp.atom "${BASE_URL}/?do=atom"
-xmllint --encode utf8 --format curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal 2 "${entries}" 119 "Atom feed expected 2 = ${entries}"
D tests/test-tagcloud.sh => tests/test-tagcloud.sh +0 -29
@@ 1,29 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${BASE_URL}" != "" ] || assert_fail 1 "How strange, BASE_URL is unset."
-
-entries=$(curl --silent "${BASE_URL}/?do=tagcloud" | xsltproc --html ../scripts/tagcloud-html2atom.xslt - | xmllint --relaxng ../scripts/categories.rng - | xmllint --xpath 'count(/*/*[local-name()="category"])' -)
-assert_equal "2" "${entries}" 27 "Categories"
-
-exit $?
D tests/test-title.sh => tests/test-title.sh +0 -35
@@ 1,35 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${BASE_URL}" != "" ] || assert_fail 1 "How strange, BASE_URL is unset."
-
-curl --url "${BASE_URL}" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.html \
- --trace-ascii curl.trace --dump-header curl.head \
- 2>/dev/null
-
-title="$(xmllint --html --xpath 'normalize-space(string(/html/body//*[@id="shaarli_title"]))' curl.html 2>/dev/null)"
-# ignore @id, use a/@href
-title="$(xmllint --html --xpath 'normalize-space(string(/html/body//a[@href="?"]))' curl.html 2>/dev/null)"
-assert_equal "Review Shaarli" "${title}" 33 "expected 'Review Shaarli' found '${title}'"
D tests/test_delete-all-ok.sh => tests/test_delete-all-ok.sh +0 -110
@@ 1,110 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-xsltproc --version 1>/dev/null || assert_fail 102 "I need xsltproc (libxml2)."
-[ "${USERNAME}" != "" ] || assert_fail 1 "How strange, USERNAME is unset."
-[ "${PASSWORD}" != "" ] || assert_fail 2 "How strange, PASSWORD is unset."
-[ "${BASE_URL}" != "" ] || assert_fail 3 "How strange, BASE_URL is unset."
-[ -r "$0".xslt ] || assert_fail 4 "How strange, helper xslt script '$0.xslt' not readable."
-
-echo "####################################################"
-echo "## Step 1: fetch token to login "
-echo "GET ${BASE_URL}?do=login"
-rm curl.tmp.*
-# http://unix.stackexchange.com/a/157219
-LOCATION=$(curl --get --url "${BASE_URL}/?do=login" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 42 "fetch token error"
-TOKEN=$(xmllint --html --nowarning --xpath 'string(/html/body//form[@name="loginform"]//input[@name="token"]/@value)' curl.tmp.html)
-# string(..) http://stackoverflow.com/a/18390404
-
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 47 "expected TOKEN of 40 characters, but found ${TOKEN} of $(printf "%s" ${TOKEN} | wc -c)"
-
-echo "######################################################"
-echo "## Step 2: follow the redirect, do the login and redirect to ${BASE_URL}/? "
-echo "POST ${LOCATION}"
-rm curl.tmp.*
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 64 "do login error"
-assert_equal "${BASE_URL}/?" "${LOCATION}" 65 "redirect to BASE_URL"
-
-# check pre-condition
-echo "###################################################"
-echo "## Logged-in Atom feed prior doing anything (should have 2 entries)"
-curl --url "${BASE_URL}/?do=atom" \
- --silent --show-error \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal "2" "${entries}" 74 "Atom feed entries"
-
-# now figure out the precise lf_linkdate and token for each entry to delete
-while true
-do
- # re-extract the token from the most recent HTTP response as it's consumed after each
- # HTTP request. So a simple for loop doesn't do the trick.
-
- line="$(xsltproc --html --nonet "${0}".xslt curl.tmp.html 2>/dev/null | head -n 1)"
- [ "" = "$line" ] && break
-
- echo "${line}" | while read lf_linkdate token
- do
- echo "lf_linkdate=${lf_linkdate} token=${token}"
- http_code=$(curl --url "${BASE_URL}/" \
- --data-urlencode "lf_linkdate=${lf_linkdate}" \
- --data-urlencode "token=${token}" \
- --data-urlencode "delete_link=" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{http_code}' 2>/dev/null)
- assert_equal "200" "${http_code}" 92 "POST lf_linkdate=${lf_linkdate}&token=..."
- break # process only one line at a time.
- done
-done
-
-
-# check post-condition
-echo "###################################################"
-echo "## Logged-in Atom feed after deleting all entries"
-curl --url "${BASE_URL}/?do=atom" \
- --silent --show-error \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal "0" "${entries}" 103 "Atom feed entries"
D tests/test_delete-ok.sh => tests/test_delete-ok.sh +0 -104
@@ 1,104 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-xsltproc --version 1>/dev/null || assert_fail 102 "I need xsltproc (libxml2)."
-[ "${USERNAME}" != "" ] || assert_fail 1 "How strange, USERNAME is unset."
-[ "${PASSWORD}" != "" ] || assert_fail 2 "How strange, PASSWORD is unset."
-[ "${BASE_URL}" != "" ] || assert_fail 3 "How strange, BASE_URL is unset."
-[ -r "$0".xslt ] || assert_fail 4 "How strange, helper xslt script '$0.xslt' not readable."
-
-echo "####################################################"
-echo "## Step 1: fetch token to login "
-echo "GET ${BASE_URL}?do=login"
-rm curl.tmp.*
-# http://unix.stackexchange.com/a/157219
-LOCATION=$(curl --get --url "${BASE_URL}/?do=login" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-# todo:
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 42 "fetch token error"
-TOKEN=$(xmllint --html --nowarning --xpath 'string(/html/body//form[@name="loginform"]//input[@name="token"]/@value)' curl.tmp.html)
-# string(..) http://stackoverflow.com/a/18390404
-
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 47 "expected TOKEN of 40 characters, but found ${TOKEN} of $(printf "%s" ${TOKEN} | wc -c)"
-
-echo "######################################################"
-echo "## Step 2: follow the redirect, do the login and redirect to ${BASE_URL}/? "
-echo "POST ${LOCATION}"
-rm curl.tmp.*
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_equal "" "${errmsg}" 64 "do login error"
-assert_equal "${BASE_URL}/?" "${LOCATION}" 65 "redirect to BASE_URL"
-
-# check pre-condition
-echo "###################################################"
-echo "## Logged-in Atom feed prior doing anything (should have 2 entries)"
-curl --url "${BASE_URL}/?do=atom" \
- --silent --show-error \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal "2" "${entries}" 74 "Atom feed entries"
-
-
-# That's maybe a quirk here – deleting consumes the token, but subsequent
-# deletes fail silently. So the loop below deletes only the first entry.
-
-# now figure out the precise lf_linkdate and token for each entry to delete
-xsltproc --html --nonet "$0".xslt curl.tmp.html 2>/dev/null | while read lf_linkdate token
-do
- echo "lf_linkdate=${lf_linkdate} token=${token}"
- http_code=$(curl --url "${BASE_URL}/" \
- --data-urlencode "lf_linkdate=${lf_linkdate}" \
- --data-urlencode "token=${token}" \
- --data-urlencode "delete_link=" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.b.html \
- --trace-ascii curl.tmp.b.trace --dump-header curl.tmp.b.head \
- --write-out '%{http_code}' 2>/dev/null)
- assert_equal "200" "${http_code}" 92 "POST lf_linkdate=${lf_linkdate}&token=..."
-done
-
-# check post-condition
-echo "###################################################"
-echo "## Logged-in Atom feed after deleting one entry"
-curl --url "${BASE_URL}/?do=atom" \
- --silent --show-error \
- --cookie curl.cook --cookie-jar curl.cook \
- --output curl.tmp.atom
-entries=$(xmllint --xpath 'count(/*/*[local-name()="entry"])' curl.tmp.atom)
-assert_equal "1" "${entries}" 103 "Atom feed entries"
-assert_equal "My secret stuff... - Pastebin.com" "$(xmllint --xpath 'string(/*/*[local-name()="entry"]/*[local-name()="title"])' curl.tmp.atom)" 104 "remaining Atom feed entry title"
D tests/test_login-fail.sh => tests/test_login-fail.sh +0 -134
@@ 1,134 0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2015-2016 Marcus Rohrmoser http://mro.name/me. All rights reserved.
-#
-# 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/>.
-#
-cd "$(dirname "$0")/../tmp"
-. ../scripts/assert.sh
-
-# Check preliminaries
-curl --version >/dev/null || assert_fail 101 "I need curl."
-xmllint --version 2> /dev/null || assert_fail 102 "I need xmllint (libxml2)."
-[ "${USERNAME}" != "" ] || assert_fail 1 "How strange, USERNAME is unset."
-[ "${PASSWORD}" != "" ] || assert_fail 2 "How strange, PASSWORD is unset."
-[ "${BASE_URL}" != "" ] || assert_fail 3 "How strange, BASE_URL is unset."
-
-fetch_token() {
- echo "GET $1" 1>&2
- # http://unix.stackexchange.com/a/157219
- LOCATION=$(curl --get --url "$1" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
- # todo:
- errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
- assert_equal "" "${errmsg}" 107 "error: '${errmsg}'"
- echo $(xmllint --html --nowarning --xpath 'string(/html/body//form[@name="loginform"]//input[@name="token"]/@value)' curl.tmp.html)
- # string(..) http://stackoverflow.com/a/18390404
-}
-
-echo "#### Test wrong token"
-rm curl.*
-LOCATION="${BASE_URL}/?do=login"
-TOKEN="just some bogus"
-
-echo "POST ${LOCATION}"
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?do=changepasswd" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_fgrep "alert(\"Wrong login/password.\");document.location='?do=login" "${errmsg}" 59 "expected failure"
-
-
-
-echo "#### Test wrong username"
-rm curl.*
-LOCATION="${BASE_URL}/?do=login"
-TOKEN="$(fetch_token "${LOCATION}")"
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 68 "found TOKEN=${TOKEN}"
-
-echo "POST ${LOCATION}"
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=f o o" \
- --data-urlencode "password=${PASSWORD}" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?do=changepasswd" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_fgrep "alert(\"Wrong login/password.\");document.location='?do=login" "${errmsg}" 81 "expected failure"
-
-
-
-echo "#### Test wrong password"
-rm curl.*
-LOCATION="${BASE_URL}/?do=login"
-TOKEN="$(fetch_token "${LOCATION}")"
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 90 "found TOKEN=${TOKEN}"
-
-echo "POST ${LOCATION}"
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=f o o" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?do=changepasswd" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_fgrep "alert(\"Wrong login/password.\");document.location='?do=login" "${errmsg}" 103 "expected failure"
-
-
-
-echo "#### Test wrong password (again)"
-rm curl.*
-LOCATION="${BASE_URL}/?do=login"
-TOKEN="$(fetch_token "${LOCATION}")"
-# the precise length doesn't matter, it just has to be significantly larger than ''
-assert_equal 40 $(printf "%s" ${TOKEN} | wc -c) 112 "found TOKEN=${TOKEN}"
-
-echo "POST ${LOCATION}"
-LOCATION=$(curl --url "${LOCATION}" \
- --data-urlencode "login=${USERNAME}" \
- --data-urlencode "password=f o o" \
- --data-urlencode "token=${TOKEN}" \
- --data-urlencode "returnurl=${BASE_URL}/?do=changepasswd" \
- --cookie curl.cook --cookie-jar curl.cook \
- --location --output curl.tmp.html \
- --trace-ascii curl.tmp.trace --dump-header curl.tmp.head \
- --write-out '%{url_effective}' 2>/dev/null)
-errmsg=$(xmllint --html --nowarning --xpath 'string(/html[1 = count(*)]/head[1 = count(*)]/script[starts-with(.,"alert(")])' curl.tmp.html)
-assert_fgrep "alert(\"Wrong login/password.\");document.location='?do=login" "${errmsg}" 125 "expected failure"
-
-
-
-echo "#### Test banned ip (4 previous failures)"
-rm curl.*
-LOCATION="${BASE_URL}/?do=login"
-TOKEN="$(fetch_token "${LOCATION}")"
-errmsg=$(xmllint --html --nowarning --xpath 'string(normalize-space(/html/body//*[@id="headerform"]))' curl.tmp.html)
-assert_equal "You have been banned from login after too many failed attempts. Try later." "${errmsg}" 134 "expected failure"
D tmp/.gitkeep => tmp/.gitkeep +0 -0