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
60 func handleMux(w http
.ResponseWriter
, r
*http
.Request
) {
61 defer un(trace(strings
.Join([]string{"v", version
, "+", GitSHA1
, " ", r
.RemoteAddr
, " ", r
.Method
, " ", r
.URL
.String()}, "")))
62 // w.Header().Set("Server", strings.Join([]string{myselfNamespace, CurrentShaarliGoVersion}, "#"))
63 w
.Header().Set("X-Powered-By", strings
.Join([]string{"https://code.mro.name/mro/Shaarli-API-test", "#", version
, "+", GitSHA1
}, ""))
66 path_info
:= os
.Getenv("PATH_INFO")
68 base
.Path
= base
.Path
[0:len(base
.Path
)-len(path_info
)] + "/../index.php"
69 // script_name := os.Getenv("SCRIPT_NAME")
70 // urlBase := mustParseURL(string(xmlBaseFromRequestURL(r.URL, os.Getenv("SCRIPT_NAME"))))
71 w
.Header().Set("Content-Type", "text/plain; charset=utf-8")
73 // https://stackoverflow.com/a/18414432
74 options
:= cookiejar
.Options
{
75 PublicSuffixList
: publicsuffix
.List
,
77 jar
, err
:= cookiejar
.New(&options
)
79 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
82 client
:= http
.Client
{Jar
: jar
}
86 io
.WriteString(w
, "r.URL: "+r
.URL
.String()+"\n")
87 io
.WriteString(w
, "base: "+base
.String()+"\n")
92 // agent := r.Header.Get("User-Agent")
95 uid
, pwd
, ok
:= r
.BasicAuth()
97 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
101 if "GET" != r
.Method
{
102 w
.Header().Set("Allow", "GET")
103 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
107 params
:= r
.URL
.Query()
108 if 1 != len(params
["url"]) {
109 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
112 p_url
:= params
["url"][0]
114 if 1 != len(params
["description"]) {
115 http
.Error(w
, "Required parameter missing: description", http
.StatusBadRequest
)
118 p_description
:= params
["description"][0]
121 if 1 == len(params
["extended"]) {
122 p_extended
= params
["extended"][0]
126 if 1 == len(params
["tags"]) {
127 p_tags
= params
["tags"][0]
132 base
.RawQuery
= v
.Encode()
134 resp
, err
:= client
.Get(base
.String())
136 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
139 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
142 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
146 formLogi
.Set("login", uid
)
147 formLogi
.Set("password", pwd
)
148 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
150 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
154 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
157 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
160 // if we do not have a linkform, auth must have failed.
161 if 0 == len(formLink
) {
162 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
166 // formLink.Set("lf_linkdate", "20190106_172531")
167 // formLink.Set("lf_url", p_url)
168 formLink
.Set("lf_title", p_description
)
169 formLink
.Set("lf_description", p_extended
)
170 formLink
.Set("lf_tags", p_tags
)
172 formLink
.Del("lf_private")
174 formLink
.Set("lf_private", "lf_private")
177 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLink
)
179 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
184 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
185 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8'?><result code='done' />")
188 case "/v1/posts/delete":
189 _
, _
, ok
:= r
.BasicAuth()
191 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
195 if "GET" != r
.Method
{
196 w
.Header().Set("Allow", "GET")
197 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
201 params
:= r
.URL
.Query()
202 if 1 != len(params
["url"]) {
203 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
206 // p_url := params["url"][0]
208 io
.WriteString(w
, "bhb")
210 case "/v1/posts/update":
211 _
, _
, ok
:= r
.BasicAuth()
213 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
217 if "GET" != r
.Method
{
218 w
.Header().Set("Allow", "GET")
219 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
223 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
224 io
.WriteString(w
, "<?xml version='1.0' encoding='UTF-8' ?><update time='2011-03-24T19:02:07Z' />")
226 case "/v1/posts/get":
227 // pretend to add, but don't actually do it, but return the form preset values.
228 uid
, pwd
, ok
:= r
.BasicAuth()
230 http
.Error(w
, "Basic Pre-Authentication required.", http
.StatusUnauthorized
)
234 if "GET" != r
.Method
{
235 w
.Header().Set("Allow", "GET")
236 http
.Error(w
, "All API methods are GET requests, even when good REST habits suggest they should use a different verb.", http
.StatusMethodNotAllowed
)
240 params
:= r
.URL
.Query()
241 if 1 != len(params
["url"]) {
242 http
.Error(w
, "Required parameter missing: url", http
.StatusBadRequest
)
245 p_url
:= params
["url"][0]
248 if 1 != len(params["description"]) {
249 http.Error(w, "Required parameter missing: description", http.StatusBadRequest)
252 p_description := params["description"][0]
255 if 1 == len(params["extended"]) {
256 p_extended = params["extended"][0]
260 if 1 == len(params["tags"]) {
261 p_tags = params["tags"][0]
267 base
.RawQuery
= v
.Encode()
269 resp
, err
:= client
.Get(base
.String())
271 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
274 formLogi
, err
:= formValuesFromReader(resp
.Body
, "loginform")
277 http
.Error(w
, err
.Error(), http
.StatusInternalServerError
)
281 formLogi
.Set("login", uid
)
282 formLogi
.Set("password", pwd
)
283 resp
, err
= client
.PostForm(resp
.Request
.URL
.String(), formLogi
)
285 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
289 formLink
, err
:= formValuesFromReader(resp
.Body
, "linkform")
292 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
295 // if we do not have a linkform, auth must have failed.
296 if 0 == len(formLink
) {
297 http
.Error(w
, "Authentication failed", http
.StatusForbidden
)
301 tim
, err
:= time
.ParseInLocation("20060102_150405", formLink
.Get("lf_linkdate"), time
.Now().Location()) // can we do any better?
303 http
.Error(w
, err
.Error(), http
.StatusBadGateway
)
307 w
.Header().Set("Content-Type", "text/xml; charset=utf-8")
309 rawText
:= func(s
string) { io
.WriteString(w
, s
) }
310 xmlText
:= func(s
string) { xml
.EscapeText(w
, []byte(s
)) }
311 xmlForm
:= func(s
string) { xmlText(formLink
.Get(s
)) }
313 rawText("<?xml version='1.0' encoding='UTF-8' ?>")
314 rawText("<posts user='")
317 xmlText(now
.Format("2006-01-02"))
319 rawText("<post href='")
322 xmlForm("lf_linkdate")
323 rawText("' description='")
325 rawText("' extended='")
326 xmlForm("lf_description")
330 xmlText(tim
.Format(time
.RFC3339
))
331 rawText("' others='")
337 case "/v1/posts/recent":
338 case "/v1/posts/dates":
339 case "/v1/posts/suggest":
341 case "/v1/tags/delete":
342 case "/v1/tags/rename":
343 case "/v1/user/secret":
344 case "/v1/user/api_token":
345 case "/v1/notes/list":
347 http
.Error(w
, "Not Implemented", http
.StatusNotImplemented
)
353 func formValuesFromReader(r io
.Reader
, name
string) (ret url
.Values
, err error
) {
354 root
, err
:= html
.Parse(r
) // assumes r is UTF8
359 for _
, form
:= range scrape
.FindAll(root
, func(n
*html
.Node
) bool { return atom
.Form
== n
.DataAtom
}) {
360 if name
!= scrape
.Attr(form
, "name") && name
!= scrape
.Attr(form
, "id") {
364 for _
, inp
:= range scrape
.FindAll(form
, func(n
*html
.Node
) bool { return atom
.Input
== n
.DataAtom || atom
.Textarea
== n
.DataAtom
}) {
365 n
:= scrape
.Attr(inp
, "name")
367 n
= scrape
.Attr(inp
, "id")
370 ty
:= scrape
.Attr(inp
, "type")
371 v
:= scrape
.Attr(inp
, "value")
372 if atom
.Textarea
== inp
.DataAtom
{
374 } else if v
== "" && ty
== "checkbox" {
375 v
= scrape
.Attr(inp
, "checked")
379 return ret
, err
// return on first occurence