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/>.
32 "github.com/yhat/scrape"
33 "golang.org/x/net/html"
34 "golang.org/x/net/html/atom"
35 // "golang.org/x/net/html/charset"
36 "golang.org/x/net/publicsuffix"
39 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
41 // even cooler: https://stackoverflow.com/a/8363629
43 // inspired by // https://coderwall.com/p/cp5fya/measuring-execution-time-in-go
44 func trace(name
string) (string, time
.Time
) { return name
, time
.Now() }
45 func un(name
string, start time
.Time
) { log
.Printf("%s took %s", name
, time
.Since(start
)) }
49 // lighttpd doesn't seem to like more than one (per-vhost) server.breakagelog
50 log
.SetOutput(os
.Stderr
)
51 } else { // log to custom logfile rather than stderr (may not be reachable on shared hosting)
54 if err
:= cgi
.Serve(http
.HandlerFunc(handleMux
)); err
!= nil {
59 // https://pinboard.in/api
61 // All API methods are GET requests, even when good REST habits suggest they should use a different verb.
66 func handleMux(w http
.ResponseWriter
, r
*http
.Request
) {
67 defer un(trace(strings
.Join([]string{"v", version
, "+", GitSHA1
, " ", r
.RemoteAddr
, " ", r
.Method
, " ", r
.URL
.String()}, "")))
68 // w.Header().Set("Server", strings.Join([]string{myselfNamespace, CurrentShaarliGoVersion}, "#"))
69 // w.Header().Set("X-Powered-By", strings.Join([]string{myselfNamespace, CurrentShaarliGoVersion}, "#"))
72 path_info
:= os
.Getenv("PATH_INFO")
74 base
.Path
= base
.Path
[0:len(base
.Path
)-len(path_info
)] + "/../index.php"
75 // script_name := os.Getenv("SCRIPT_NAME")
76 // urlBase := mustParseURL(string(xmlBaseFromRequestURL(r.URL, os.Getenv("SCRIPT_NAME"))))
77 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
81 io
.WriteString(w
, "r.URL: "+r
.URL
.String()+"\n")
82 io
.WriteString(w
, "base: "+base
.String()+"\n")
86 uid
, pwd
, ok
:= r
.BasicAuth()
88 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
92 if "GET" != r
.Method
{
93 w
.Header().Set("Allow", "GET")
94 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
98 params
:= r
.URL
.Query()
99 if 1 != len(params
["url"]) {
100 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
103 p_url
:= params
["url"][0]
105 if 1 != len(params
["description"]) {
106 http
.Error(w
, "Required parameter missing: description", http
.StatusBadRequest
)
109 p_description
:= params
["description"][0]
112 if 1 == len(params
["extended"]) {
113 p_extended
= params
["extended"][0]
117 if 1 == len(params
["tags"]) {
118 p_tags
= params
["tags"][0]
123 base
.RawQuery
= v
.Encode()
125 // https://stackoverflow.com/a/18414432
126 options
:= cookiejar
.Options
{
127 PublicSuffixList
: publicsuffix
.List
,
129 jar
, err
:= cookiejar
.New(&options
)
131 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
134 client
:= http
.Client
{Jar
: jar
}
136 resp
, err
:= client
.Get(base
.String())
138 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
141 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
144 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
148 formLogi
.Set("login", uid
)
149 formLogi
.Set("password", pwd
)
150 formLogi
.Set("returnurl", r
.URL
.String())
151 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
153 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
157 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
160 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
163 // if we do not have a linkform, auth must have failed.
164 if 0 == len(formLink
) {
165 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
169 // formLink.Set("lf_linkdate", "20190106_172531")
170 // formLink.Set("lf_url", p_url)
171 formLink
.Set("lf_title", p_description
)
172 formLink
.Set("lf_description", p_extended
)
173 formLink
.Set("lf_tags", p_tags
)
175 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLink
)
178 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
182 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
183 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8' ?><result code='done' />")
185 case "/v1/posts/delete":
186 _
, _
, ok
:= r
.BasicAuth()
188 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
192 if "GET" != r
.Method
{
193 w
.Header().Set("Allow", "GET")
194 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
198 params
:= r
.URL
.Query()
199 if 1 != len(params
["url"]) {
200 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
203 // p_url := params["url"][0]
205 io
.WriteString(w
, "bhb")
207 case "/v1/posts/update":
208 _
, _
, ok
:= r
.BasicAuth()
210 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
214 if "GET" != r
.Method
{
215 w
.Header().Set("Allow", "GET")
216 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
220 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
221 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8' ?><update time='2011-03-24T19:02:07Z' />")
223 case "/v1/posts/get":
224 // pretend to add, but don't actually do it, but return the form preset values.
225 uid
, pwd
, ok
:= r
.BasicAuth()
227 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
231 if "GET" != r
.Method
{
232 w
.Header().Set("Allow", "GET")
233 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
237 params
:= r
.URL
.Query()
238 if 1 != len(params
["url"]) {
239 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
242 p_url
:= params
["url"][0]
245 if 1 != len(params["description"]) {
246 http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
249 p_description := params["description"][0]
252 if 1 == len(params["extended"]) {
253 p_extended = params["extended"][0]
257 if 1 == len(params["tags"]) {
258 p_tags = params["tags"][0]
264 base
.RawQuery
= v
.Encode()
266 // https://stackoverflow.com/a/18414432
267 options
:= cookiejar
.Options
{
268 PublicSuffixList
: publicsuffix
.List
,
270 jar
, err
:= cookiejar
.New(&options
)
272 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
275 client
:= http
.Client
{Jar
: jar
}
277 resp
, err
:= client
.Get(base
.String())
279 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
282 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
285 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
289 formLogi
.Set("login", uid
)
290 formLogi
.Set("password", pwd
)
291 formLogi
.Set("returnurl", r
.URL
.String())
292 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
294 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
298 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
301 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
304 // if we do not have a linkform, auth must have failed.
305 if 0 == len(formLink
) {
306 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
310 t
, err
:= time
.Parse("20060102_150405", formLink
.Get("lf_linkdate")) // rather ParseInLocation
312 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
316 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
318 rawText
:= func(s
string) { io
.WriteString(w
, s
) }
319 xmlText
:= func(s
string) { xml
.EscapeText(w
, []byte(s
)) }
320 xmlForm
:= func(s
string) { xmlText(formLink
.Get(s
)) }
322 rawText("<?xml version='1.0' encoding='UTF-8' ?>")
323 rawText("<posts user='")
326 xmlText(time
.Now().Format("2006-01-02"))
328 rawText("<post href='")
331 xmlForm("lf_linkdate")
332 rawText("' description='")
334 rawText("' extended='")
335 xmlForm("lf_description")
339 xmlText(t
.Format(time
.RFC3339
))
340 rawText("' others='")
346 case "/v1/posts/recent":
347 case "/v1/posts/dates":
348 case "/v1/posts/suggest":
350 case "/v1/tags/delete":
351 case "/v1/tags/rename":
352 case "/v1/user/secret":
353 case "/v1/user/api_token":
354 case "/v1/notes/list":
356 http
.Error(w
, "Not Implemented", http
.StatusNotImplemented
)
362 func formValuesFromReader(r io
.Reader
, name
string) (ret url
.Values
, err error
) {
363 root
, err
:= html
.Parse(r
) // assumes r is UTF8
368 for _
, form
:= range scrape
.FindAll(root
, func(n
*html
.Node
) bool { return atom
.Form
== n
.DataAtom
}) {
369 if name
!= scrape
.Attr(form
, "name") && name
!= scrape
.Attr(form
, "id") {
373 for _
, inp
:= range scrape
.FindAll(form
, func(n
*html
.Node
) bool { return atom
.Input
== n
.DataAtom || atom
.Textarea
== n
.DataAtom
}) {
374 n
:= scrape
.Attr(inp
, "name")
376 n
= scrape
.Attr(inp
, "id")
379 ty
:= scrape
.Attr(inp
, "type")
380 v
:= scrape
.Attr(inp
, "value")
381 if atom
.Textarea
== inp
.DataAtom
{
383 } else if v
== "" && ty
== "checkbox" {
384 v
= scrape
.Attr(inp
, "checked")
388 return ret
, err
// return on first occurence