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 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
97 params
:= r
.URL
.Query()
98 if 1 != len(params
["url"]) {
99 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
102 p_url
:= params
["url"][0]
104 if 1 != len(params
["description"]) {
105 http
.Error(w
, "Required parameter missing: description", http
.StatusBadRequest
)
108 p_description
:= params
["description"][0]
111 if 1 == len(params
["extended"]) {
112 p_extended
= params
["extended"][0]
116 if 1 == len(params
["tags"]) {
117 p_tags
= params
["tags"][0]
122 base
.RawQuery
= v
.Encode()
124 // https://stackoverflow.com/a/18414432
125 options
:= cookiejar
.Options
{
126 PublicSuffixList
: publicsuffix
.List
,
128 jar
, err
:= cookiejar
.New(&options
)
130 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
133 client
:= http
.Client
{Jar
: jar
}
135 resp
, err
:= client
.Get(base
.String())
137 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
140 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
143 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
147 formLogi
.Set("login", uid
)
148 formLogi
.Set("password", pwd
)
149 formLogi
.Set("returnurl", r
.URL
.String())
150 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
152 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
156 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
159 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
162 // if we do not have a linkform, auth must have failed.
163 if 0 == len(formLink
) {
164 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
168 // formLink.Set("lf_linkdate", "20190106_172531")
169 // formLink.Set("lf_url", p_url)
170 formLink
.Set("lf_title", p_description
)
171 formLink
.Set("lf_description", p_extended
)
172 formLink
.Set("lf_tags", p_tags
)
174 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLink
)
177 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
181 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
182 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8' ?><result code='done' />")
184 case "/v1/posts/delete":
185 _
, _
, ok
:= r
.BasicAuth()
187 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
191 if "GET" != r
.Method
{
192 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
196 params
:= r
.URL
.Query()
197 if 1 != len(params
["url"]) {
198 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
201 // p_url := params["url"][0]
203 io
.WriteString(w
, "bhb")
205 case "/v1/posts/update":
206 _
, _
, ok
:= r
.BasicAuth()
208 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
212 if "GET" != r
.Method
{
213 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
217 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
218 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8' ?><update time='2011-03-24T19:02:07Z' />")
220 case "/v1/posts/get":
221 // pretend to add, but don't actually do it, but return the form preset values.
222 uid
, pwd
, ok
:= r
.BasicAuth()
224 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
228 if "GET" != r
.Method
{
229 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
233 params
:= r
.URL
.Query()
234 if 1 != len(params
["url"]) {
235 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
238 p_url
:= params
["url"][0]
241 if 1 != len(params["description"]) {
242 http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
245 p_description := params["description"][0]
248 if 1 == len(params["extended"]) {
249 p_extended = params["extended"][0]
253 if 1 == len(params["tags"]) {
254 p_tags = params["tags"][0]
260 base
.RawQuery
= v
.Encode()
262 // https://stackoverflow.com/a/18414432
263 options
:= cookiejar
.Options
{
264 PublicSuffixList
: publicsuffix
.List
,
266 jar
, err
:= cookiejar
.New(&options
)
268 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
271 client
:= http
.Client
{Jar
: jar
}
273 resp
, err
:= client
.Get(base
.String())
275 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
278 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
281 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
285 formLogi
.Set("login", uid
)
286 formLogi
.Set("password", pwd
)
287 formLogi
.Set("returnurl", r
.URL
.String())
288 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
290 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
294 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
297 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
300 // if we do not have a linkform, auth must have failed.
301 if 0 == len(formLink
) {
302 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
306 t
, err
:= time
.Parse("2006-01-02_150405", formLink
.Get("lf_linkdate")) // rather ParseInLocation
308 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
312 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
314 rawText
:= func(s
string) { io
.WriteString(w
, s
) }
315 xmlText
:= func(s
string) { xml
.EscapeText(w
, []byte(s
)) }
316 xmlForm
:= func(s
string) { xmlText(formLink
.Get(s
)) }
318 rawText("<?xml version='1.0' encoding='UTF-8' ?>")
319 rawText("<posts user='")
322 xmlText(time
.Now().Format("2006-01-02"))
324 rawText("<post href='")
328 rawText("' description='")
330 rawText("' extended='")
331 xmlForm("lf_description")
335 xmlText(t
.Format(time
.RFC3339
))
336 rawText("' others='")
342 case "/v1/posts/recent":
343 case "/v1/posts/dates":
344 case "/v1/posts/suggest":
346 case "/v1/tags/delete":
347 case "/v1/tags/rename":
348 case "/v1/user/secret":
349 case "/v1/user/api_token":
350 case "/v1/notes/list":
352 http
.Error(w
, "Not Implemented", http
.StatusNotImplemented
)
358 func formValuesFromReader(r io
.Reader
, name
string) (ret url
.Values
, err error
) {
359 root
, err
:= html
.Parse(r
) // assumes r is UTF8
364 for _
, form
:= range scrape
.FindAll(root
, func(n
*html
.Node
) bool { return atom
.Form
== n
.DataAtom
}) {
365 if name
!= scrape
.Attr(form
, "name") && name
!= scrape
.Attr(form
, "id") {
369 for _
, inp
:= range scrape
.FindAll(form
, func(n
*html
.Node
) bool { return atom
.Input
== n
.DataAtom || atom
.Textarea
== n
.DataAtom
}) {
370 n
:= scrape
.Attr(inp
, "name")
372 n
= scrape
.Attr(inp
, "id")
375 ty
:= scrape
.Attr(inp
, "type")
376 v
:= scrape
.Attr(inp
, "value")
377 if atom
.Textarea
== inp
.DataAtom
{
379 } else if v
== "" && ty
== "checkbox" {
380 v
= scrape
.Attr(inp
, "checked")
384 return ret
, err
// return on first occurence