2 // Copyright (C) 2019-2019 Marcus Rohrmoser, https://code.mro.name/mro/Shaarli-API-test
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
33 "github.com/yhat/scrape"
34 "golang.org/x/net/html"
35 "golang.org/x/net/html/atom"
36 // "golang.org/x/net/html/charset"
37 "golang.org/x/net/publicsuffix"
41 ShaarliDate
= "20060102_150405"
42 IsoDate
= "2006-01-02"
45 var GitSHA1
= "Please set -ldflags \"-X main.GitSHA1=$(git rev-parse --short HEAD)\"" // https://medium.com/@joshroppo/setting-go-1-5-variables-at-compile-time-for-versioning-5b30a965d33e
47 // even cooler: https://stackoverflow.com/a/8363629
49 // inspired by // https://coderwall.com/p/cp5fya/measuring-execution-time-in-go
50 func trace(name
string) (string, time
.Time
) { return name
, time
.Now() }
51 func un(name
string, start time
.Time
) { log
.Printf("%s took %s", name
, time
.Since(start
)) }
55 // lighttpd doesn't seem to like more than one (per-vhost) server.breakagelog
56 log
.SetOutput(os
.Stderr
)
57 } else { // log to custom logfile rather than stderr (may not be reachable on shared hosting)
60 if err
:= cgi
.Serve(http
.HandlerFunc(handleMux
)); err
!= nil {
65 // https://pinboard.in/api
66 func handleMux(w http
.ResponseWriter
, r
*http
.Request
) {
67 raw
:= func(s
...string) {
68 for _
, txt
:= range s
{
69 io
.WriteString(w
, txt
)
72 elmS
:= func(e
string, close bool, atts
...string) {
74 for i
, v
:= range atts
{
79 xml
.EscapeText(w
, []byte(v
))
88 elmE
:= func(e
string) { raw("</", e
, ">", "\n") }
90 defer un(trace(strings
.Join([]string{"v", version
, "+", GitSHA1
, " ", r
.RemoteAddr
, " ", r
.Method
, " ", r
.URL
.String()}, "")))
91 path_info
:= os
.Getenv("PATH_INFO")
93 base
.Path
= path
.Join(base
.Path
[0:len(base
.Path
)-len(path_info
)], "..", "index.php")
95 w
.Header().Set(http
.CanonicalHeaderKey("X-Powered-By"), strings
.Join([]string{"https://code.mro.name/mro/Shaarli-API-test", "#", version
, "+", GitSHA1
}, ""))
96 w
.Header().Set(http
.CanonicalHeaderKey("Content-Type"), "text/xml; charset=utf-8")
98 // https://stackoverflow.com/a/18414432
99 options
:= cookiejar
.Options
{
100 PublicSuffixList
: publicsuffix
.List
,
102 jar
, err
:= cookiejar
.New(&options
)
104 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
107 client
:= http
.Client
{Jar
: jar
}
113 base
.Path
= path
.Join(base
.Path
[0:len(base
.Path
)-len(path_info
)], "about") + "/"
114 http
.Redirect(w
, r
, base
.Path
, http
.StatusFound
)
118 // w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/rdf+xml")
119 raw(`<?xml version="1.0" encoding="utf-8"?>
120 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
121 xmlns="http://usefulinc.com/ns/doap#">
123 <name xml:lang="en">🛠 Shaarli Pinboard API</name>
124 <short-description xml:lang="en">subset conforming https://pinboard.in/api/</short-description>
125 <implements rdf:resource="https://pinboard.in/api/"/>
126 <platform rdf:resource="https://sebsauvage.net/wiki/doku.php?id=php:shaarli"/>
127 <homepage rdf:resource="https://code.mro.name/mro/Shaarli-API-test/"/>
128 <bug-database rdf:resource="https://code.mro.name/mro/Shaarli-API-test/issues"/>
129 <wiki rdf:resource="https://code.mro.name/mro/Shaarli-API-test/wiki"/>
130 <license rdf:resource="https://code.mro.name/mro/Shaarli-API-test/src/master/LICENSE"/>
131 <maintainer rdf:resource="http://mro.name/~me"/>
132 <programming-language>golang</programming-language>
133 <category>microblogging</category>
134 <category>shaarli</category>
135 <category>nodb</category>
136 <category>api</category>
137 <category>pinboard</category>
138 <category>delicious</category>
139 <category>cgi</category>
142 <browse rdf:resource="https://code.mro.name/mro/Shaarli-API-test"/>
143 <location rdf:resource="https://code.mro.name/mro/Shaarli-API-test.git"/>
148 <name>`, version
, "+", GitSHA1
, `</name>
149 <revision>`, GitSHA1
, `</revision>
150 <description>…</description>
157 case "/v1/posts/add":
158 // extract parameters
159 // agent := r.Header.Get("User-Agent")
162 uid
, pwd
, ok
:= r
.BasicAuth()
164 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
168 if http
.MethodGet
!= r
.Method
{
169 w
.Header().Set(http
.CanonicalHeaderKey("Allow"), http
.MethodGet
)
170 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
174 params
:= r
.URL
.Query()
175 if 1 != len(params
["url"]) {
176 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
179 p_url
:= params
["url"][0]
181 if 1 != len(params
["description"]) {
182 http
.Error(w
, "Required parameter missing: description", http
.StatusBadRequest
)
185 p_description
:= params
["description"][0]
188 if 1 == len(params
["extended"]) {
189 p_extended
= params
["extended"][0]
193 if 1 == len(params
["tags"]) {
194 p_tags
= params
["tags"][0]
199 v
.Set("title", p_description
)
200 base
.RawQuery
= v
.Encode()
202 resp
, err
:= client
.Get(base
.String())
204 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
207 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
210 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
214 formLogi
.Set("login", uid
)
215 formLogi
.Set("password", pwd
)
216 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
218 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
222 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
225 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
228 // if we do not have a linkform, auth must have failed.
229 if 0 == len(formLink
) {
230 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
234 // formLink.Set("lf_linkdate", ShaarliDate)
235 // formLink.Set("lf_url", p_url)
236 // formLink.Set("lf_title", p_description)
237 formLink
.Set("lf_description", p_extended
)
238 formLink
.Set("lf_tags", p_tags
)
240 formLink
.Del("lf_private")
242 formLink
.Set("lf_private", "lf_private")
245 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLink
)
247 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
257 case "/v1/posts/delete":
258 _
, _
, ok
:= r
.BasicAuth()
260 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
264 if http
.MethodGet
!= r
.Method
{
265 w
.Header().Set(http
.CanonicalHeaderKey("Allow"), http
.MethodGet
)
266 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
270 params
:= r
.URL
.Query()
271 if 1 != len(params
["url"]) {
272 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
275 // p_url := params["url"][0]
278 "code", "not implemented yet")
280 case "/v1/posts/update":
281 _
, _
, ok
:= r
.BasicAuth()
283 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
287 if http
.MethodGet
!= r
.Method
{
288 w
.Header().Set(http
.CanonicalHeaderKey("Allow"), http
.MethodGet
)
289 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
295 "time", "2011-03-24T19:02:07Z")
297 case "/v1/posts/get":
298 // pretend to add, but don't actually do it, but return the form preset values.
299 uid
, pwd
, ok
:= r
.BasicAuth()
301 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
305 if http
.MethodGet
!= r
.Method
{
306 w
.Header().Set(http
.CanonicalHeaderKey("Allow"), http
.MethodGet
)
307 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
311 params
:= r
.URL
.Query()
312 if 1 != len(params
["url"]) {
313 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
316 p_url
:= params
["url"][0]
319 if 1 != len(params["description"]) {
320 http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
323 p_description := params["description"][0]
326 if 1 == len(params["extended"]) {
327 p_extended = params["extended"][0]
331 if 1 == len(params["tags"]) {
332 p_tags = params["tags"][0]
338 base
.RawQuery
= v
.Encode()
340 resp
, err
:= client
.Get(base
.String())
342 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
345 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
348 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
352 formLogi
.Set("login", uid
)
353 formLogi
.Set("password", pwd
)
354 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
356 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
360 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
363 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
366 // if we do not have a linkform, auth must have failed.
367 if 0 == len(formLink
) {
368 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
372 fv
:= func(s
string) string { return formLink
.Get(s
) }
374 tim
, err
:= time
.ParseInLocation(ShaarliDate
, fv("lf_linkdate"), time
.Local
) // can we do any better?
376 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
383 "dt", tim
.Format(IsoDate
),
384 "tag", fv("lf_tags"))
386 "href", fv("lf_url"),
387 "hash", fv("lf_linkdate"),
388 "description", fv("lf_title"),
389 "extended", fv("lf_description"),
390 "tag", fv("lf_tags"),
391 "time", tim
.Format(time
.RFC3339
),
396 case "/v1/posts/recent",
403 "/v1/user/api_token",
406 http
.Error(w
, "Not Implemented", http
.StatusNotImplemented
)
412 func formValuesFromReader(r io
.Reader
, name
string) (ret url
.Values
, err error
) {
413 root
, err
:= html
.Parse(r
) // assumes r is UTF8
418 for _
, form
:= range scrape
.FindAll(root
, func(n
*html
.Node
) bool {
419 return atom
.Form
== n
.DataAtom
&&
420 (name
== scrape
.Attr(n
, "name") || name
== scrape
.Attr(n
, "id"))
423 for _
, inp
:= range scrape
.FindAll(form
, func(n
*html
.Node
) bool {
424 return atom
.Input
== n
.DataAtom || atom
.Textarea
== n
.DataAtom
426 n
:= scrape
.Attr(inp
, "name")
428 n
= scrape
.Attr(inp
, "id")
431 ty
:= scrape
.Attr(inp
, "type")
432 v
:= scrape
.Attr(inp
, "value")
433 if atom
.Textarea
== inp
.DataAtom
{
435 } else if v
== "" && ty
== "checkbox" {
436 v
= scrape
.Attr(inp
, "checked")
440 return ret
, err
// return on first occurence