From 6b7cd0f39bc048d232b3a6f73977c240ff581ded Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 18 Nov 2017 21:50:59 -0500 Subject: [PATCH 001/181] Add GitHub issue template --- .github/ISSUE_TEMPLATE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..1630276 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +**Overview** + + +**Background** + + +**Dependencies** + + +**Implementation** + From dc1ab5b9ea36db0f7ecebda5790ee9f2e13522db Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 18 Nov 2017 21:51:29 -0500 Subject: [PATCH 002/181] Move Contributing guidelines to .github dir --- CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/README.md b/README.md index 542d004..5285667 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ GLOBAL OPTIONS: ## Contributing to the CLI -For a complete guide to contributing, see the [Contribution Guide](CONTRIBUTING.md). +For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). We welcome any kind of contributions including documentation, organizational improvements, tutorials, bug reports, feature requests, new features, answering questions, etc. From 44049df0b1b8b3e43efaef34bd4efb4bfc12f247 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 26 Aug 2018 10:18:32 -0400 Subject: [PATCH 003/181] Add Debian packaging --- debian/changelog | 9 +++++++++ debian/compat | 1 + debian/control | 29 +++++++++++++++++++++++++++++ debian/copyright | 32 ++++++++++++++++++++++++++++++++ debian/gbp.conf | 2 ++ debian/rules | 4 ++++ debian/source/format | 1 + debian/watch | 4 ++++ 8 files changed, 82 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/gbp.conf create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/watch diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..06d28d8 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,9 @@ +writeas-cli (1.0+git20171119.dc1ab5b-1) xenial; urgency=medium + + * All logging and errors go to stderr, not stdout (Closes: #11) + * Verbose logging requires `-v` or `--verbose` flag + * Executable now lives in `cmd/` directory + * All errors exit with status 1 + * Fixed `cli` library deprecation (Closes: #8) + + -- Write.as Sun, 19 Nov 2017 09:36:28 -0500 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..61cc087 --- /dev/null +++ b/debian/control @@ -0,0 +1,29 @@ +Source: writeas-cli +Section: utils +Priority: optional +Maintainer: Matt Baer +Uploaders: Matt Baer +Build-Depends: debhelper (>= 9), + dh-golang, + golang-any, + golang-github-mitchellh-go-homedir-dev +Standards-Version: 4.1.1 +Homepage: https://write.as/apps/cli +Vcs-Browser: https://github.com/writeas/writeas-cli +Vcs-Git: https://github.com/writeas/writeas-cli +XS-Go-Import-Path: github.com/writeas/writeas-cli +Testsuite: autopkgtest-pkg-go + +Package: writeas-cli +Architecture: any +Built-Using: ${misc:Built-Using} +Depends: ${shlibs:Depends}, + ${misc:Depends} +Description: Text publishing utility + The Write.as CLI enables you to publish text to Write.as directly from the + command-line. + . + Write.as is a simple writing and publishing tool that lets you share text on + the web and keep your privacy in the process. There's no sign up required to + use it, and whether you're on the web, a mobile device, or the command-line, + you can start publishing instantly. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..1cbabad --- /dev/null +++ b/debian/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: writeas-cli +Source: https://github.com/writeas/writeas-cli +Files-Excluded: vendor Godeps/_workspace + +Files: * +Copyright: 2018 A Bunch Tell LLC +License: MIT + +License: MIT + The MIT License (MIT) + . + Copyright (c) 2015 Write.as + . + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..cec628c --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,2 @@ +[DEFAULT] +pristine-tar = True diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..8cce5e0 --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem=golang --with=golang diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..6f20c89 --- /dev/null +++ b/debian/watch @@ -0,0 +1,4 @@ +version=3 +opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/writeas-cli-\$1\.tar\.gz/,\ +uversionmangle=s/(\d)[_\.\-\+]?(RC|rc|pre|dev|beta|alpha)[.]?(\d*)$/\$1~\$2\$3/ \ + https://github.com/writeas/writeas-cli/tags .*/v?(\d\S*)\.tar\.gz From 32a4bd6bd711a473ec2c31d5063471ee15e9d3b4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 09:29:19 -0400 Subject: [PATCH 004/181] Move API-related funcs to api.go --- cmd/writeas/api.go | 189 +++++++++++++++++++++++++++++++++++++++++++++ cmd/writeas/cli.go | 184 ------------------------------------------- 2 files changed, 189 insertions(+), 184 deletions(-) create mode 100644 cmd/writeas/api.go diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go new file mode 100644 index 0000000..0f8bb89 --- /dev/null +++ b/cmd/writeas/api.go @@ -0,0 +1,189 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/atotto/clipboard" + "gopkg.in/urfave/cli.v1" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" +) + +func client(read, tor bool, path, query string) (string, *http.Client) { + var u *url.URL + var client *http.Client + if tor { + u, _ = url.ParseRequestURI(hiddenAPIURL) + u.Path = "/api/" + path + client = torClient() + } else { + u, _ = url.ParseRequestURI(apiURL) + u.Path = "/api/" + path + client = &http.Client{} + } + if query != "" { + u.RawQuery = query + } + urlStr := fmt.Sprintf("%v", u) + + return urlStr, client +} + +// DoFetch retrieves the Write.as post with the given friendlyID, +// optionally via the Tor hidden service. +func DoFetch(friendlyID string, tor bool) error { + path := friendlyID + + urlStr, client := client(true, tor, path, "") + + r, _ := http.NewRequest("GET", urlStr, nil) + r.Header.Add("User-Agent", "writeas-cli v"+version) + + resp, err := client.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + fmt.Printf("%s\n", string(content)) + } else if resp.StatusCode == http.StatusNotFound { + return ErrPostNotFound + } else if resp.StatusCode == http.StatusGone { + } else { + return fmt.Errorf("Unable to get post: %s", resp.Status) + } + return nil +} + +// DoPost creates a Write.as post, returning an error if it was +// unsuccessful. +func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { + data := url.Values{} + data.Set("w", string(post)) + if encrypt { + data.Add("e", "") + } + data.Add("font", getFont(code, font)) + + urlStr, client := client(false, tor, "", "") + + r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) + r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + + resp, err := client.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + nlPos := strings.Index(string(content), "\n") + url := content[:nlPos] + idPos := strings.LastIndex(string(url), "/") + 1 + id := string(url[idPos:]) + token := string(content[nlPos+1 : len(content)-1]) + + addPost(id, token) + + // Copy URL to clipboard + err = clipboard.WriteAll(string(url)) + if err != nil { + Errorln("writeas: Didn't copy to clipboard: %s", err) + } else { + Info(c, "Copied to clipboard.") + } + + // Output URL + fmt.Printf("%s\n", url) + } else { + return fmt.Errorf("Unable to post: %s", resp.Status) + } + + return nil +} + +// DoUpdate updates the given post on Write.as. +func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { + urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) + + data := url.Values{} + data.Set("w", string(post)) + + if code || font != "" { + // Only update font if explicitly changed + data.Add("font", getFont(code, font)) + } + + r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) + r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + + resp, err := client.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + if tor { + Info(c, "Post updated via hidden service.") + } else { + Info(c, "Post updated.") + } + } else { + if debug { + ErrorlnQuit("Problem updating: %s", resp.Status) + } else { + return fmt.Errorf("Post doesn't exist, or bad edit token given.") + } + } + return nil +} + +// DoDelete deletes the given post on Write.as. +func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { + urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) + + r, _ := http.NewRequest("DELETE", urlStr, nil) + r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + if tor { + Info(c, "Post deleted from hidden service.") + } else { + Info(c, "Post deleted.") + } + removePost(friendlyID) + } else { + if debug { + ErrorlnQuit("Problem deleting: %s", resp.Status) + } else { + return fmt.Errorf("Post doesn't exist, or bad edit token given.") + } + } + + return nil +} diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 4d67d0f..c5fc929 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -2,18 +2,10 @@ package main import ( "bufio" - "bytes" - "fmt" - "github.com/atotto/clipboard" "gopkg.in/urfave/cli.v1" "io" - "io/ioutil" "log" - "net/http" - "net/url" "os" - "strconv" - "strings" ) // API constants for communicating with Write.as. @@ -263,179 +255,3 @@ func handlePost(fullPost []byte, c *cli.Context) error { return DoPost(c, fullPost, c.String("font"), false, tor, c.Bool("code")) } - -func client(read, tor bool, path, query string) (string, *http.Client) { - var u *url.URL - var client *http.Client - if tor { - u, _ = url.ParseRequestURI(hiddenAPIURL) - u.Path = "/api/" + path - client = torClient() - } else { - u, _ = url.ParseRequestURI(apiURL) - u.Path = "/api/" + path - client = &http.Client{} - } - if query != "" { - u.RawQuery = query - } - urlStr := fmt.Sprintf("%v", u) - - return urlStr, client -} - -// DoFetch retrieves the Write.as post with the given friendlyID, -// optionally via the Tor hidden service. -func DoFetch(friendlyID string, tor bool) error { - path := friendlyID - - urlStr, client := client(true, tor, path, "") - - r, _ := http.NewRequest("GET", urlStr, nil) - r.Header.Add("User-Agent", "writeas-cli v"+version) - - resp, err := client.Do(r) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - fmt.Printf("%s\n", string(content)) - } else if resp.StatusCode == http.StatusNotFound { - return ErrPostNotFound - } else if resp.StatusCode == http.StatusGone { - } else { - return fmt.Errorf("Unable to get post: %s", resp.Status) - } - return nil -} - -// DoPost creates a Write.as post, returning an error if it was -// unsuccessful. -func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { - data := url.Values{} - data.Set("w", string(post)) - if encrypt { - data.Add("e", "") - } - data.Add("font", getFont(code, font)) - - urlStr, client := client(false, tor, "", "") - - r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", "writeas-cli v"+version) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") - r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) - - resp, err := client.Do(r) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - nlPos := strings.Index(string(content), "\n") - url := content[:nlPos] - idPos := strings.LastIndex(string(url), "/") + 1 - id := string(url[idPos:]) - token := string(content[nlPos+1 : len(content)-1]) - - addPost(id, token) - - // Copy URL to clipboard - err = clipboard.WriteAll(string(url)) - if err != nil { - Errorln("writeas: Didn't copy to clipboard: %s", err) - } else { - Info(c, "Copied to clipboard.") - } - - // Output URL - fmt.Printf("%s\n", url) - } else { - return fmt.Errorf("Unable to post: %s", resp.Status) - } - - return nil -} - -// DoUpdate updates the given post on Write.as. -func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) - - data := url.Values{} - data.Set("w", string(post)) - - if code || font != "" { - // Only update font if explicitly changed - data.Add("font", getFont(code, font)) - } - - r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", "writeas-cli v"+version) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") - r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) - - resp, err := client.Do(r) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - if tor { - Info(c, "Post updated via hidden service.") - } else { - Info(c, "Post updated.") - } - } else { - if debug { - ErrorlnQuit("Problem updating: %s", resp.Status) - } else { - return fmt.Errorf("Post doesn't exist, or bad edit token given.") - } - } - return nil -} - -// DoDelete deletes the given post on Write.as. -func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) - - r, _ := http.NewRequest("DELETE", urlStr, nil) - r.Header.Add("User-Agent", "writeas-cli v"+version) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - resp, err := client.Do(r) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - if tor { - Info(c, "Post deleted from hidden service.") - } else { - Info(c, "Post deleted.") - } - removePost(friendlyID) - } else { - if debug { - ErrorlnQuit("Problem deleting: %s", resp.Status) - } else { - return fmt.Errorf("Post doesn't exist, or bad edit token given.") - } - } - - return nil -} From f494e6d1854f1d5c40bb936f94950e9b8894cb33 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 10:40:55 -0400 Subject: [PATCH 005/181] Support setting custom User-Agent --- cmd/writeas/api.go | 22 +++++++++++++++++----- cmd/writeas/cli.go | 5 +++++ cmd/writeas/commands.go | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 0f8bb89..ab830ca 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -12,6 +12,18 @@ import ( "strings" ) +const ( + defaultUserAgent = "writeas-cli v" + version +) + +func userAgent(c *cli.Context) string { + ua := c.String("user-agent") + if ua == "" { + return defaultUserAgent + } + return ua + " (" + defaultUserAgent + ")" +} + func client(read, tor bool, path, query string) (string, *http.Client) { var u *url.URL var client *http.Client @@ -34,13 +46,13 @@ func client(read, tor bool, path, query string) (string, *http.Client) { // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. -func DoFetch(friendlyID string, tor bool) error { +func DoFetch(friendlyID, ua string, tor bool) error { path := friendlyID urlStr, client := client(true, tor, path, "") r, _ := http.NewRequest("GET", urlStr, nil) - r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("User-Agent", ua) resp, err := client.Do(r) if err != nil { @@ -76,7 +88,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e urlStr, client := client(false, tor, "", "") r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("User-Agent", userAgent(c)) r.Header.Add("Content-Type", "application/x-www-form-urlencoded") r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) @@ -130,7 +142,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, } r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("User-Agent", userAgent(c)) r.Header.Add("Content-Type", "application/x-www-form-urlencoded") r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) @@ -161,7 +173,7 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) r, _ := http.NewRequest("DELETE", urlStr, nil) - r.Header.Add("User-Agent", "writeas-cli v"+version) + r.Header.Add("User-Agent", userAgent(c)) r.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(r) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index c5fc929..12f7c80 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -49,6 +49,11 @@ var postFlags = []cli.Flag{ Usage: "Sets post font to given value", Value: defaultFont, }, + cli.StringFlag{ + Name: "user-agent", + Usage: "Sets the User-Agent for API requests", + Value: "", + }, } func main() { diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 90b09b0..b9d5399 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -120,7 +120,7 @@ func cmdGet(c *cli.Context) error { Info(c, "Getting...") } - return DoFetch(friendlyID, tor) + return DoFetch(friendlyID, userAgent(c), tor) } func cmdAdd(c *cli.Context) error { From cbb44f285928282142c12fe6b139518ecfc87cd9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 10:50:05 -0400 Subject: [PATCH 006/181] Use code.as/core/socks instead of h12.me/socks --- cmd/writeas/tor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/tor.go b/cmd/writeas/tor.go index 6914315..00f1821 100644 --- a/cmd/writeas/tor.go +++ b/cmd/writeas/tor.go @@ -1,8 +1,8 @@ package main import ( + "code.as/core/socks" "fmt" - "h12.me/socks" "net/http" ) From 91182658d61ae2821bbc9236abc7c45e624aec43 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 11:03:07 -0400 Subject: [PATCH 007/181] Add features to README and tweak copy --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5285667..98d7f8b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,17 @@ writeas-cli =========== ![MIT license](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) -Command line interface for [Write.as](https://write.as) and [Write.as on Tor](http://writeas7pm7rcdqg.onion/). Works on Windows, OS X, and Linux. +Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. -Like the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas), the command line client keeps track of the posts you make, so future editing / deleting is easier than [doing it with cURL](http://cmd.write.as/). The goal is for this to serve as the backend for any future GUI app we build for the desktop. +## Features + +* Publish anonymously to Write.as +* A stable, easy back-end for your GUI app or desktop-based workflow +* Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/) +* Locally keeps track of any posts you make +* Update and delete anonymous posts +* Fetch any post by ID +* Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing ## Installing The easiest way to get the CLI is to download a pre-built executable for your OS. From 6f3b42514414b8a113b1888a4732e88605eb7d00 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 11:28:35 -0400 Subject: [PATCH 008/181] Bump version to 1.1 --- README.md | 6 +++--- cmd/writeas/cli.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 98d7f8b..8d138d0 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. **Windows**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.0/writeas_1.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.0/writeas_1.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.0/writeas_1.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. **Linux**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.0/writeas_1.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.0/writeas_1.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. ### Go get it ```bash diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 12f7c80..2a41acd 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -17,7 +17,7 @@ const ( // Application constants. const ( - version = "1.0" + version = "1.1" ) // Defaults for posts on Write.as. From 4daad02a621b2078f1b062655a6840e423fa657b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 11:31:31 -0400 Subject: [PATCH 009/181] Fix macOS download URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d138d0..9bfa4e6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Get the latest version for your operating system as a standalone executable. Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. **Linux**
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. From 0fbdecb85c7d98281831fa1a0f9120db20bca778 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 12:56:21 -0400 Subject: [PATCH 010/181] Remove unused readAPIURL var --- cmd/writeas/cli.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 2a41acd..1c00a26 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -12,7 +12,6 @@ import ( const ( apiURL = "https://write.as" hiddenAPIURL = "http://writeas7pm7rcdqg.onion" - readAPIURL = "https://write.as" ) // Application constants. From 5c3deaa07f56009a3570ff263d02a69bbab3a269 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 6 Sep 2018 13:18:16 -0400 Subject: [PATCH 011/181] Migrate API calls to v2 / go-writeas library This closes #9 --- cmd/writeas/api.go | 185 +++++++++++++++------------------------------ cmd/writeas/cli.go | 4 +- 2 files changed, 63 insertions(+), 126 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index ab830ca..d0c010a 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -1,15 +1,10 @@ package main import ( - "bytes" "fmt" "github.com/atotto/clipboard" + "github.com/writeas/go-writeas" "gopkg.in/urfave/cli.v1" - "io/ioutil" - "net/http" - "net/url" - "strconv" - "strings" ) const ( @@ -24,178 +19,120 @@ func userAgent(c *cli.Context) string { return ua + " (" + defaultUserAgent + ")" } -func client(read, tor bool, path, query string) (string, *http.Client) { - var u *url.URL - var client *http.Client +func client(userAgent string, tor bool) *writeas.Client { + var client *writeas.Client if tor { - u, _ = url.ParseRequestURI(hiddenAPIURL) - u.Path = "/api/" + path - client = torClient() + client = writeas.NewTorClient(torPort) } else { - u, _ = url.ParseRequestURI(apiURL) - u.Path = "/api/" + path - client = &http.Client{} + client = writeas.NewClient() } - if query != "" { - u.RawQuery = query - } - urlStr := fmt.Sprintf("%v", u) + client.UserAgent = userAgent - return urlStr, client + return client } // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. func DoFetch(friendlyID, ua string, tor bool) error { - path := friendlyID - - urlStr, client := client(true, tor, path, "") - - r, _ := http.NewRequest("GET", urlStr, nil) - r.Header.Add("User-Agent", ua) + cl := client(ua, tor) - resp, err := client.Do(r) + p, err := cl.GetPost(friendlyID) if err != nil { return err } - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - fmt.Printf("%s\n", string(content)) - } else if resp.StatusCode == http.StatusNotFound { - return ErrPostNotFound - } else if resp.StatusCode == http.StatusGone { - } else { - return fmt.Errorf("Unable to get post: %s", resp.Status) - } + fmt.Printf("%s\n", string(p.Content)) return nil } // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { - data := url.Values{} - data.Set("w", string(post)) - if encrypt { - data.Add("e", "") - } - data.Add("font", getFont(code, font)) - - urlStr, client := client(false, tor, "", "") - - r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", userAgent(c)) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") - r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) + cl := client(userAgent(c), tor) - resp, err := client.Do(r) + p, err := cl.CreatePost(&writeas.PostParams{ + // TODO: extract title + Content: string(post), + Font: getFont(code, font), + }) if err != nil { - return err + return fmt.Errorf("Unable to post: %v", err) } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusOK { - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - nlPos := strings.Index(string(content), "\n") - url := content[:nlPos] - idPos := strings.LastIndex(string(url), "/") + 1 - id := string(url[idPos:]) - token := string(content[nlPos+1 : len(content)-1]) - - addPost(id, token) + url := writeasBaseURL + if tor { + url = torBaseURL + } + url += "/" + p.ID - // Copy URL to clipboard - err = clipboard.WriteAll(string(url)) - if err != nil { - Errorln("writeas: Didn't copy to clipboard: %s", err) - } else { - Info(c, "Copied to clipboard.") - } + // Store post locally + addPost(p.ID, p.Token) - // Output URL - fmt.Printf("%s\n", url) + // Copy URL to clipboard + err = clipboard.WriteAll(string(url)) + if err != nil { + Errorln("writeas: Didn't copy to clipboard: %s", err) } else { - return fmt.Errorf("Unable to post: %s", resp.Status) + Info(c, "Copied to clipboard.") } + // Output URL + fmt.Printf("%s\n", url) + return nil } // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) - - data := url.Values{} - data.Set("w", string(post)) + cl := client(userAgent(c), tor) + params := writeas.PostParams{ + ID: friendlyID, + Token: token, + Content: string(post), + // TODO: extract title + } if code || font != "" { - // Only update font if explicitly changed - data.Add("font", getFont(code, font)) + params.Font = getFont(code, font) } - r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode())) - r.Header.Add("User-Agent", userAgent(c)) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") - r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) - - resp, err := client.Do(r) + _, err := cl.UpdatePost(¶ms) if err != nil { - return err + if debug { + ErrorlnQuit("Problem updating: %v", err) + } + return fmt.Errorf("Post doesn't exist, or bad edit token given.") } - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - if tor { - Info(c, "Post updated via hidden service.") - } else { - Info(c, "Post updated.") - } + if tor { + Info(c, "Post updated via hidden service.") } else { - if debug { - ErrorlnQuit("Problem updating: %s", resp.Status) - } else { - return fmt.Errorf("Post doesn't exist, or bad edit token given.") - } + Info(c, "Post updated.") } return nil } // DoDelete deletes the given post on Write.as. func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - urlStr, client := client(false, tor, friendlyID, fmt.Sprintf("t=%s", token)) - - r, _ := http.NewRequest("DELETE", urlStr, nil) - r.Header.Add("User-Agent", userAgent(c)) - r.Header.Add("Content-Type", "application/x-www-form-urlencoded") + cl := client(userAgent(c), tor) - resp, err := client.Do(r) + err := cl.DeletePost(&writeas.PostParams{ + ID: friendlyID, + Token: token, + }) if err != nil { - return err + if debug { + ErrorlnQuit("Problem deleting: %v", err) + } + return fmt.Errorf("Post doesn't exist, or bad edit token given.") } - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - if tor { - Info(c, "Post deleted from hidden service.") - } else { - Info(c, "Post deleted.") - } - removePost(friendlyID) + if tor { + Info(c, "Post deleted from hidden service.") } else { - if debug { - ErrorlnQuit("Problem deleting: %s", resp.Status) - } else { - return fmt.Errorf("Post doesn't exist, or bad edit token given.") - } + Info(c, "Post deleted.") } + removePost(friendlyID) return nil } diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 1c00a26..9c941a2 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -10,8 +10,8 @@ import ( // API constants for communicating with Write.as. const ( - apiURL = "https://write.as" - hiddenAPIURL = "http://writeas7pm7rcdqg.onion" + writeasBaseURL = "https://write.as" + torBaseURL = "http://writeas7pm7rcdqg.onion" ) // Application constants. From 4a092cfbf8032919f209779d4c5490c4aa26c552 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 8 Sep 2018 13:43:42 -0400 Subject: [PATCH 012/181] Move userAgent func to options.go --- cmd/writeas/api.go | 8 -------- cmd/writeas/options.go | 13 +++++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 cmd/writeas/options.go diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index d0c010a..9637ff5 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -11,14 +11,6 @@ const ( defaultUserAgent = "writeas-cli v" + version ) -func userAgent(c *cli.Context) string { - ua := c.String("user-agent") - if ua == "" { - return defaultUserAgent - } - return ua + " (" + defaultUserAgent + ")" -} - func client(userAgent string, tor bool) *writeas.Client { var client *writeas.Client if tor { diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go new file mode 100644 index 0000000..67e6642 --- /dev/null +++ b/cmd/writeas/options.go @@ -0,0 +1,13 @@ +package main + +import ( + "gopkg.in/urfave/cli.v1" +) + +func userAgent(c *cli.Context) string { + ua := c.String("user-agent") + if ua == "" { + return defaultUserAgent + } + return ua + " (" + defaultUserAgent + ")" +} From d6c9ede87c1a16ab18db62f3cb16ffa905198805 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 8 Sep 2018 15:07:13 -0400 Subject: [PATCH 013/181] Move --tor flag checking to options.go --- cmd/writeas/cli.go | 2 +- cmd/writeas/commands.go | 6 +++--- cmd/writeas/options.go | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 9c941a2..016a42c 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -247,7 +247,7 @@ func readStdIn() []byte { } func handlePost(fullPost []byte, c *cli.Context) error { - tor := c.Bool("tor") || c.Bool("t") + tor := isTor(c) if c.Int("tor-port") != 0 { torPort = c.Int("tor-port") } diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index b9d5399..d2c4bcd 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -59,7 +59,7 @@ func cmdDelete(c *cli.Context) error { } } - tor := c.Bool("tor") || c.Bool("t") + tor := isTor(c) if c.Int("tor-port") != 0 { torPort = c.Int("tor-port") } @@ -91,7 +91,7 @@ func cmdUpdate(c *cli.Context) error { // Read post body fullPost := readStdIn() - tor := c.Bool("tor") || c.Bool("t") + tor := isTor(c) if c.Int("tor-port") != 0 { torPort = c.Int("tor-port") } @@ -110,7 +110,7 @@ func cmdGet(c *cli.Context) error { return cli.NewExitError("usage: writeas get ", 1) } - tor := c.Bool("tor") || c.Bool("t") + tor := isTor(c) if c.Int("tor-port") != 0 { torPort = c.Int("tor-port") } diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go index 67e6642..061bfeb 100644 --- a/cmd/writeas/options.go +++ b/cmd/writeas/options.go @@ -11,3 +11,7 @@ func userAgent(c *cli.Context) string { } return ua + " (" + defaultUserAgent + ")" } + +func isTor(c *cli.Context) bool { + return c.Bool("tor") || c.Bool("t") +} From 02145b3acb43dfee63a381e1317ba1fe6938a2bb Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 8 Sep 2018 16:00:48 -0400 Subject: [PATCH 014/181] Support user authentication Adds new `writeas auth` command and saves user's access token to a new local configuration file. This closes #10 / T191 --- cmd/writeas/api.go | 21 ++++++++++++++++++ cmd/writeas/cli.go | 21 ++++++++++++++++++ cmd/writeas/commands.go | 19 +++++++++++++++++ cmd/writeas/userconfig.go | 45 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 cmd/writeas/userconfig.go diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 9637ff5..cb9f05b 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -128,3 +128,24 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { return nil } + +func DoLogIn(c *cli.Context, username, password string) error { + cl := client(userAgent(c), isTor(c)) + + uc, err := loadConfig() + if err != nil { + return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + } + + u, err := cl.LogIn(username, password) + if err != nil { + if debug { + ErrorlnQuit("Problem logging in: %v", err) + } + return err + } + + uc.API.Token = u.AccessToken + Info(c, "Success.") + return saveConfig(uc) +} diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 016a42c..492ce06 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -191,6 +191,27 @@ func main() { }, }, }, + { + Name: "auth", + Usage: "Authenticate with Write.as", + Action: cmdAuth, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.StringFlag{ + Name: "u", + Usage: "Username for authentication", + Value: "", + }, + }, + }, } cli.CommandHelpTemplate = `NAME: diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index d2c4bcd..34cb87b 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/howeyc/gopass" "gopkg.in/urfave/cli.v1" "os" ) @@ -152,3 +153,21 @@ func cmdList(c *cli.Context) error { } return nil } + +func cmdAuth(c *cli.Context) error { + username := c.String("u") + if username == "" { + return cli.NewExitError("usage: writeas auth -u ", 1) + } + + fmt.Print("Password: ") + pass, err := gopass.GetPasswdMasked() + if err != nil { + return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1) + } + // Validate password + if len(pass) == 0 { + return cli.NewExitError("Please enter your password.", 1) + } + return DoLogIn(c, username, string(pass)) +} diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go new file mode 100644 index 0000000..d54304d --- /dev/null +++ b/cmd/writeas/userconfig.go @@ -0,0 +1,45 @@ +package main + +import ( + "gopkg.in/ini.v1" + "path/filepath" +) + +const ( + userConfigFile = "config.ini" +) + +type ( + APIConfig struct { + Token string `ini:"token"` + } + + UserConfig struct { + API APIConfig `ini:"api"` + } +) + +func loadConfig() (*UserConfig, error) { + cfg, err := ini.LooseLoad(filepath.Join(userDataDir(), userConfigFile)) + if err != nil { + return nil, err + } + + // Parse INI file + uc := &UserConfig{} + err = cfg.MapTo(uc) + if err != nil { + return nil, err + } + return uc, nil +} + +func saveConfig(uc *UserConfig) error { + cfg := ini.Empty() + err := ini.ReflectFrom(cfg, uc) + if err != nil { + return err + } + + return cfg.SaveTo(filepath.Join(userDataDir(), userConfigFile)) +} From fca81a9456e932c879759d6ceec6121ed871d825 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 8 Sep 2018 16:09:10 -0400 Subject: [PATCH 015/181] Update docs with updated flags + new auth command --- GUIDE.md | 18 ++++++++++-------- README.md | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 900d04d..9978080 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -23,16 +23,18 @@ COMMANDS: get Read a raw post add Add an existing post locally list List local posts + auth Authenticate with Write.as help, h Shows a list of commands or help for one command - + GLOBAL OPTIONS: - --tor, -t Perform action on Tor hidden service - --tor-port "9150" Use a different port to connect to Tor - --code Specifies this post is code - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --help, -h show help - --version, -v print the version + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --user-agent value Sets the User-Agent for API requests + --help, -h show help + --version, -V print the version ``` #### Share something diff --git a/README.md b/README.md index 9bfa4e6..3469f13 100644 --- a/README.md +++ b/README.md @@ -63,16 +63,18 @@ COMMANDS: get Read a raw post add Add an existing post locally list List local posts + auth Authenticate with Write.as help, h Shows a list of commands or help for one command - + GLOBAL OPTIONS: - --tor, -t Perform action on Tor hidden service - --tor-port "9150" Use a different port to connect to Tor - --code Specifies this post is code - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --help, -h show help - --version, -v print the version + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --user-agent value Sets the User-Agent for API requests + --help, -h show help + --version, -V print the version ``` ## Contributing to the CLI From 48b51417ff5a9683d256bd6e8bfa4e3db09a0044 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 8 Sep 2018 16:17:05 -0400 Subject: [PATCH 016/181] Prevent authenticating when user already did that --- cmd/writeas/api.go | 7 +------ cmd/writeas/commands.go | 13 ++++++++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index cb9f05b..c4d3e62 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -129,14 +129,9 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { return nil } -func DoLogIn(c *cli.Context, username, password string) error { +func DoLogIn(c *cli.Context, uc *UserConfig, username, password string) error { cl := client(userAgent(c), isTor(c)) - uc, err := loadConfig() - if err != nil { - return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) - } - u, err := cl.LogIn(username, password) if err != nil { if debug { diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 34cb87b..1db023d 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -155,6 +155,16 @@ func cmdList(c *cli.Context) error { } func cmdAuth(c *cli.Context) error { + // Check configuration + uc, err := loadConfig() + if err != nil { + return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + } + if uc != nil && uc.API.Token != "" { + return cli.NewExitError("You're already authenticated.", 1) + } + + // Validate arguments and get password username := c.String("u") if username == "" { return cli.NewExitError("usage: writeas auth -u ", 1) @@ -165,9 +175,10 @@ func cmdAuth(c *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1) } + // Validate password if len(pass) == 0 { return cli.NewExitError("Please enter your password.", 1) } - return DoLogIn(c, username, string(pass)) + return DoLogIn(c, uc, username, string(pass)) } From 97f29319ee2d713a03524d11316fce90c7c3088a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 10 Sep 2018 12:33:05 -0400 Subject: [PATCH 017/181] Save authenticated user information in JSON Instead of using an INI file for the access token, this stores it and other user information in a single JSON file. --- cmd/writeas/api.go | 11 +++++++---- cmd/writeas/commands.go | 6 +++--- cmd/writeas/userconfig.go | 41 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index c4d3e62..fccecd0 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -129,7 +129,7 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { return nil } -func DoLogIn(c *cli.Context, uc *UserConfig, username, password string) error { +func DoLogIn(c *cli.Context, username, password string) error { cl := client(userAgent(c), isTor(c)) u, err := cl.LogIn(username, password) @@ -140,7 +140,10 @@ func DoLogIn(c *cli.Context, uc *UserConfig, username, password string) error { return err } - uc.API.Token = u.AccessToken - Info(c, "Success.") - return saveConfig(uc) + err = saveUser(u) + if err != nil { + return err + } + fmt.Printf("Logged in as %s.\n", u.User.Username) + return nil } diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 1db023d..b30bdc1 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -156,11 +156,11 @@ func cmdList(c *cli.Context) error { func cmdAuth(c *cli.Context) error { // Check configuration - uc, err := loadConfig() + u, err := loadUser() if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } - if uc != nil && uc.API.Token != "" { + if u != nil && u.AccessToken != "" { return cli.NewExitError("You're already authenticated.", 1) } @@ -180,5 +180,5 @@ func cmdAuth(c *cli.Context) error { if len(pass) == 0 { return cli.NewExitError("Please enter your password.", 1) } - return DoLogIn(c, uc, username, string(pass)) + return DoLogIn(c, username, string(pass)) } diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go index d54304d..b8b5d64 100644 --- a/cmd/writeas/userconfig.go +++ b/cmd/writeas/userconfig.go @@ -1,17 +1,21 @@ package main import ( + "encoding/json" + "github.com/writeas/go-writeas" + "github.com/writeas/writeas-cli/fileutils" "gopkg.in/ini.v1" + "io/ioutil" "path/filepath" ) const ( userConfigFile = "config.ini" + userFile = "user.json" ) type ( APIConfig struct { - Token string `ini:"token"` } UserConfig struct { @@ -43,3 +47,38 @@ func saveConfig(uc *UserConfig) error { return cfg.SaveTo(filepath.Join(userDataDir(), userConfigFile)) } + +func loadUser() (*writeas.AuthUser, error) { + fname := filepath.Join(userDataDir(), userFile) + userJSON, err := ioutil.ReadFile(fname) + if err != nil { + if !fileutils.Exists(fname) { + // Don't return a file-not-found error + return nil, nil + } + return nil, err + } + + // Parse JSON file + u := &writeas.AuthUser{} + err = json.Unmarshal(userJSON, u) + if err != nil { + return nil, err + } + return u, nil +} + +func saveUser(u *writeas.AuthUser) error { + // Marshal struct into pretty-printed JSON + userJSON, err := json.MarshalIndent(u, "", " ") + if err != nil { + return err + } + + // Save file + err = ioutil.WriteFile(filepath.Join(userDataDir(), userFile), userJSON, 0600) + if err != nil { + return err + } + return nil +} From 257b969216c603653976bb1e1b40b804a4760db9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 10 Sep 2018 14:57:16 -0400 Subject: [PATCH 018/181] Make authenticated requests If you're logged in, now requests will be made with the saved token --- cmd/writeas/api.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index fccecd0..abe8d7f 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -23,6 +23,22 @@ func client(userAgent string, tor bool) *writeas.Client { return client } +func newClient(c *cli.Context) *writeas.Client { + var client *writeas.Client + if isTor(c) { + client = writeas.NewTorClient(torPort) + } else { + client = writeas.NewClient() + } + client.UserAgent = userAgent(c) + u, _ := loadUser() + if u != nil { + client.SetToken(u.AccessToken) + } + + return client +} + // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. func DoFetch(friendlyID, ua string, tor bool) error { @@ -40,7 +56,7 @@ func DoFetch(friendlyID, ua string, tor bool) error { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { - cl := client(userAgent(c), tor) + cl := newClient(c) p, err := cl.CreatePost(&writeas.PostParams{ // TODO: extract title @@ -57,8 +73,10 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e } url += "/" + p.ID - // Store post locally - addPost(p.ID, p.Token) + if cl.Token() == "" { + // Store post locally, since we're not authenticated + addPost(p.ID, p.Token) + } // Copy URL to clipboard err = clipboard.WriteAll(string(url)) @@ -76,7 +94,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - cl := client(userAgent(c), tor) + cl := newClient(c) params := writeas.PostParams{ ID: friendlyID, @@ -106,7 +124,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, // DoDelete deletes the given post on Write.as. func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - cl := client(userAgent(c), tor) + cl := newClient(c) err := cl.DeletePost(&writeas.PostParams{ ID: friendlyID, From 5ec4a02c7a7bfbe6819be466285646d67a4fa9fd Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 10 Sep 2018 15:01:30 -0400 Subject: [PATCH 019/181] Support publishing to a blog Closes #16 --- cmd/writeas/api.go | 24 ++++++++++++++++-------- cmd/writeas/cli.go | 5 +++++ cmd/writeas/options.go | 10 ++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index abe8d7f..f23df90 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -58,20 +58,28 @@ func DoFetch(friendlyID, ua string, tor bool) error { func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { cl := newClient(c) - p, err := cl.CreatePost(&writeas.PostParams{ + pp := &writeas.PostParams{ // TODO: extract title - Content: string(post), - Font: getFont(code, font), - }) + Content: string(post), + Font: getFont(code, font), + Collection: collection(c), + } + p, err := cl.CreatePost(pp) if err != nil { return fmt.Errorf("Unable to post: %v", err) } - url := writeasBaseURL - if tor { - url = torBaseURL + var url string + if p.Collection != nil { + url = p.Collection.URL + p.Slug + } else { + if tor { + url = torBaseURL + } else { + url = writeasBaseURL + } + url += "/" + p.ID } - url += "/" + p.ID if cl.Token() == "" { // Store post locally, since we're not authenticated diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 492ce06..09add6f 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -26,6 +26,11 @@ const ( // Available flags for creating posts var postFlags = []cli.Flag{ + cli.StringFlag{ + Name: "c, b", + Usage: "Optional blog to post to", + Value: "", + }, cli.BoolFlag{ Name: "tor, t", Usage: "Perform action on Tor hidden service", diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go index 061bfeb..42b9231 100644 --- a/cmd/writeas/options.go +++ b/cmd/writeas/options.go @@ -15,3 +15,13 @@ func userAgent(c *cli.Context) string { func isTor(c *cli.Context) bool { return c.Bool("tor") || c.Bool("t") } + +func collection(c *cli.Context) string { + if coll := c.String("c"); coll != "" { + return coll + } + if coll := c.String("b"); coll != "" { + return coll + } + return "" +} From db3450b8ccbbfec6032eb1539a37f4ce85fa0550 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 12 Sep 2018 11:26:20 -0400 Subject: [PATCH 020/181] Support logging out --- cmd/writeas/api.go | 25 +++++++++++++++++++++++++ cmd/writeas/cli.go | 16 ++++++++++++++++ cmd/writeas/commands.go | 4 ++++ fileutils/fileutils.go | 4 ++++ 4 files changed, 49 insertions(+) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index f23df90..4faadf7 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -4,7 +4,9 @@ import ( "fmt" "github.com/atotto/clipboard" "github.com/writeas/go-writeas" + "github.com/writeas/writeas-cli/fileutils" "gopkg.in/urfave/cli.v1" + "path/filepath" ) const ( @@ -173,3 +175,26 @@ func DoLogIn(c *cli.Context, username, password string) error { fmt.Printf("Logged in as %s.\n", u.User.Username) return nil } + +func DoLogOut(c *cli.Context) error { + cl := newClient(c) + if cl.Token() == "" { + return fmt.Errorf("Not currently logged in. Authenticate with: writeas auth -u ") + } + + err := cl.LogOut() + if err != nil { + if debug { + ErrorlnQuit("Problem logging out: %v", err) + } + return err + } + + // Delete local user data + err = fileutils.DeleteFile(filepath.Join(userDataDir(), userFile)) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 09add6f..a7e91da 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -217,6 +217,22 @@ func main() { }, }, }, + { + Name: "logout", + Usage: "Log out of Write.as", + Action: cmdLogOut, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + }, + }, } cli.CommandHelpTemplate = `NAME: diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index b30bdc1..f8105fd 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -182,3 +182,7 @@ func cmdAuth(c *cli.Context) error { } return DoLogIn(c, username, string(pass)) } + +func cmdLogOut(c *cli.Context) error { + return DoLogOut(c) +} diff --git a/fileutils/fileutils.go b/fileutils/fileutils.go index ba36356..2e728a6 100644 --- a/fileutils/fileutils.go +++ b/fileutils/fileutils.go @@ -105,3 +105,7 @@ func FindLine(p, startsWith string) string { return "" } + +func DeleteFile(p string) error { + return os.Remove(p) +} From fe4c67ba00a86f7c1e9ffe16cce6be0db47e3227 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 12 Sep 2018 19:28:18 -0400 Subject: [PATCH 021/181] Move "not auth'd" error to newClient() This adds a new parameter that determines whether or not authentication is required, and also returns an error now. --- cmd/writeas/api.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 4faadf7..63554d8 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -25,7 +25,7 @@ func client(userAgent string, tor bool) *writeas.Client { return client } -func newClient(c *cli.Context) *writeas.Client { +func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { var client *writeas.Client if isTor(c) { client = writeas.NewTorClient(torPort) @@ -36,9 +36,11 @@ func newClient(c *cli.Context) *writeas.Client { u, _ := loadUser() if u != nil { client.SetToken(u.AccessToken) + } else if authRequired { + return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth -u ") } - return client + return client, nil } // DoFetch retrieves the Write.as post with the given friendlyID, @@ -58,7 +60,7 @@ func DoFetch(friendlyID, ua string, tor bool) error { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { - cl := newClient(c) + cl, _ := newClient(c, false) pp := &writeas.PostParams{ // TODO: extract title @@ -104,7 +106,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - cl := newClient(c) + cl, _ := newClient(c, false) params := writeas.PostParams{ ID: friendlyID, @@ -134,7 +136,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, // DoDelete deletes the given post on Write.as. func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - cl := newClient(c) + cl, _ := newClient(c, false) err := cl.DeletePost(&writeas.PostParams{ ID: friendlyID, @@ -177,12 +179,12 @@ func DoLogIn(c *cli.Context, username, password string) error { } func DoLogOut(c *cli.Context) error { - cl := newClient(c) - if cl.Token() == "" { - return fmt.Errorf("Not currently logged in. Authenticate with: writeas auth -u ") + cl, err := newClient(c, true) + if err != nil { + return err } - err := cl.LogOut() + err = cl.LogOut() if err != nil { if debug { ErrorlnQuit("Problem logging out: %v", err) From 052c792391d0106e4a9cc93cb8ca61b745b9a0b4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Sep 2018 19:42:29 +0200 Subject: [PATCH 022/181] Add `writeas fetch` command This configures a directory for posts and downloads all posts on the authenticated account into the directory, organizing collection posts into their own subdirectories. --- cmd/writeas/cli.go | 16 ++++++ cmd/writeas/sync.go | 112 ++++++++++++++++++++++++++++++++++++++ cmd/writeas/userconfig.go | 7 ++- 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 cmd/writeas/sync.go diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index a7e91da..f122d08 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -196,6 +196,22 @@ func main() { }, }, }, + { + Name: "fetch", + Usage: "Fetch authenticated user's Write.as posts", + Action: cmdPull, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + }, + }, { Name: "auth", Usage: "Authenticate with Write.as", diff --git a/cmd/writeas/sync.go b/cmd/writeas/sync.go new file mode 100644 index 0000000..f5ec3b7 --- /dev/null +++ b/cmd/writeas/sync.go @@ -0,0 +1,112 @@ +package main + +import ( + //"github.com/writeas/writeas-cli/sync" + "fmt" + "github.com/writeas/writeas-cli/fileutils" + "gopkg.in/urfave/cli.v1" + "io/ioutil" + "os" + "path/filepath" +) + +const ( + postFileExt = ".txt" +) + +func cmdPull(c *cli.Context) error { + cfg, err := loadConfig() + if err != nil { + return err + } + // Create posts directory if needed + if cfg.Posts.Directory == "" { + syncSetUp(cfg) + } + + // Fetch posts + cl, err := newClient(c, true) + if err != nil { + return err + } + + posts, err := cl.GetUserPosts() + if err != nil { + return err + } + + for _, p := range *posts { + postFilename := p.ID + collDir := "" + if p.Collection != nil { + postFilename = p.Slug + // Create directory for collection + collDir = p.Collection.Alias + if !fileutils.Exists(filepath.Join(cfg.Posts.Directory, collDir)) { + Info(c, "Creating folder "+collDir) + err = os.Mkdir(filepath.Join(cfg.Posts.Directory, collDir), 0755) + if err != nil { + Errorln("Error creating blog directory %s: %s. Skipping post %s.", collDir, err, postFilename) + continue + } + } + } + postFilename += postFileExt + + // Write file + err = ioutil.WriteFile(filepath.Join(cfg.Posts.Directory, collDir, postFilename), []byte(p.Content), 0644) + if err != nil { + Errorln("Error creating file %s: %s", postFilename, err) + } + Info(c, "Saved post "+postFilename) + + // Update mtime and atime on files + modTime := p.Updated.Local() + err = os.Chtimes(filepath.Join(cfg.Posts.Directory, collDir, postFilename), modTime, modTime) + if err != nil { + Errorln("Error setting time on %s: %s", postFilename, err) + } + } + + return nil +} + +// TODO: move UserConfig to its own package, and this to sync package +func syncSetUp(cfg *UserConfig) error { + // Prompt for posts directory + defaultDir, err := os.Getwd() + if err != nil { + return err + } + var dir string + fmt.Printf("Posts directory? [%s]: ", defaultDir) + fmt.Scanln(&dir) + if dir == "" { + dir = defaultDir + } + + // Create directory if needed + if !fileutils.Exists(dir) { + err = os.MkdirAll(dir, 0700) + if err != nil { + if debug { + Errorln("Error creating data directory: %s", err) + } + return err + } + fmt.Println("Created posts directory.") + } + + // Save preference + cfg.Posts.Directory = dir + err = saveConfig(cfg) + if err != nil { + if debug { + Errorln("Unable to save config: %s", err) + } + return err + } + fmt.Println("Saved config.") + + return nil +} diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go index b8b5d64..f02fda0 100644 --- a/cmd/writeas/userconfig.go +++ b/cmd/writeas/userconfig.go @@ -18,8 +18,13 @@ type ( APIConfig struct { } + PostsConfig struct { + Directory string `ini:"directory"` + } + UserConfig struct { - API APIConfig `ini:"api"` + API APIConfig `ini:"api"` + Posts PostsConfig `ini:"posts"` } ) From a63445f92a00b34140d8d3f3b91eb9f41f204d45 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 14 Sep 2018 19:44:10 +0200 Subject: [PATCH 023/181] Fix Exists() test in fileutils --- fileutils/fileutils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fileutils/fileutils.go b/fileutils/fileutils.go index 2e728a6..7e4c354 100644 --- a/fileutils/fileutils.go +++ b/fileutils/fileutils.go @@ -9,7 +9,7 @@ import ( // Exists returns whether or not the given file exists func Exists(p string) bool { - if _, err := os.Stat(p); err == nil { + if _, err := os.Stat(p); !os.IsNotExist(err) { return true } return false From a2569848ecfa7f56b487d1fe774691ff95ec4cef Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 15 Sep 2018 10:30:49 +0200 Subject: [PATCH 024/181] Include title in fetched posts --- cmd/writeas/sync.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/sync.go b/cmd/writeas/sync.go index f5ec3b7..0d17c4d 100644 --- a/cmd/writeas/sync.go +++ b/cmd/writeas/sync.go @@ -54,7 +54,11 @@ func cmdPull(c *cli.Context) error { postFilename += postFileExt // Write file - err = ioutil.WriteFile(filepath.Join(cfg.Posts.Directory, collDir, postFilename), []byte(p.Content), 0644) + txtFile := p.Content + if p.Title != "" { + txtFile = "# " + p.Title + "\n\n" + txtFile + } + err = ioutil.WriteFile(filepath.Join(cfg.Posts.Directory, collDir, postFilename), []byte(txtFile), 0644) if err != nil { Errorln("Error creating file %s: %s", postFilename, err) } From a83410b7173a3f412dcfd69b4cc2b0b2091cd5d9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 16 Sep 2018 02:29:15 +0200 Subject: [PATCH 025/181] Support publishing files Adds new `writeas publish ` command --- GUIDE.md | 1 + README.md | 1 + cmd/writeas/cli.go | 6 ++++++ cmd/writeas/commands.go | 13 +++++++++++++ 4 files changed, 21 insertions(+) diff --git a/GUIDE.md b/GUIDE.md index 9978080..a1fc1f0 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -18,6 +18,7 @@ writeas [global options] command [command options] [arguments...] COMMANDS: post Alias for default action: create post from stdin new Compose a new post from the command-line and publish + publish Publish a file to Write.as delete Delete a post update Update (overwrite) a post get Read a raw post diff --git a/README.md b/README.md index 3469f13..fb4d58a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ writeas [global options] command [command options] [arguments...] COMMANDS: post Alias for default action: create post from stdin new Compose a new post from the command-line and publish + publish Publish a file to Write.as delete Delete a post update Update (overwrite) a post get Read a raw post diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index f122d08..fd281bd 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -115,6 +115,12 @@ func main() { Action: cmdNew, Flags: postFlags, }, + { + Name: "publish", + Usage: "Publish a file to Write.as", + Action: cmdPublish, + Flags: postFlags, + }, { Name: "delete", Usage: "Delete a post", diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index f8105fd..f86e843 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/howeyc/gopass" "gopkg.in/urfave/cli.v1" + "io/ioutil" "os" ) @@ -44,6 +45,18 @@ func cmdNew(c *cli.Context) error { return nil } +func cmdPublish(c *cli.Context) error { + filename := c.Args().Get(0) + if filename == "" { + return cli.NewExitError("usage: writeas publish ", 1) + } + content, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + return handlePost(content, c) +} + func cmdDelete(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) From 2c0fd7583f63fe4b519037b3f4c5bbab4567e689 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 16 Sep 2018 02:43:35 +0200 Subject: [PATCH 026/181] Support sending post language with --lang flag --- cmd/writeas/api.go | 3 +++ cmd/writeas/cli.go | 5 +++++ cmd/writeas/options.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 63554d8..bb474cb 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -68,6 +68,9 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e Font: getFont(code, font), Collection: collection(c), } + if lang := language(c); lang != "" { + pp.Language = &lang + } p, err := cl.CreatePost(pp) if err != nil { return fmt.Errorf("Unable to post: %v", err) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index fd281bd..5185093 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -53,6 +53,11 @@ var postFlags = []cli.Flag{ Usage: "Sets post font to given value", Value: defaultFont, }, + cli.StringFlag{ + Name: "lang", + Usage: "Sets post language to given ISO 639-1 language code", + Value: "", + }, cli.StringFlag{ Name: "user-agent", Usage: "Sets the User-Agent for API requests", diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go index 42b9231..71ca49e 100644 --- a/cmd/writeas/options.go +++ b/cmd/writeas/options.go @@ -16,6 +16,10 @@ func isTor(c *cli.Context) bool { return c.Bool("tor") || c.Bool("t") } +func language(c *cli.Context) string { + return c.String("lang") +} + func collection(c *cli.Context) string { if coll := c.String("c"); coll != "" { return coll From 90641dd12b75a2ec83bffae268af7e49c05e75cd Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 01:38:02 +0100 Subject: [PATCH 027/181] Save post locally on publish Only with `writeas publish` right now, and only if posts directory has already been set up. --- cmd/writeas/api.go | 7 ++++--- cmd/writeas/cli.go | 3 ++- cmd/writeas/commands.go | 19 ++++++++++++++++--- cmd/writeas/posts.go | 17 +++++++++++++++++ cmd/writeas/userconfig.go | 1 + 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index bb474cb..809a591 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -33,6 +33,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { client = writeas.NewClient() } client.UserAgent = userAgent(c) + // TODO: load user into var shared across the app u, _ := loadUser() if u != nil { client.SetToken(u.AccessToken) @@ -59,7 +60,7 @@ func DoFetch(friendlyID, ua string, tor bool) error { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. -func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) error { +func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) (*writeas.Post, error) { cl, _ := newClient(c, false) pp := &writeas.PostParams{ @@ -73,7 +74,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e } p, err := cl.CreatePost(pp) if err != nil { - return fmt.Errorf("Unable to post: %v", err) + return nil, fmt.Errorf("Unable to post: %v", err) } var url string @@ -104,7 +105,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e // Output URL fmt.Printf("%s\n", url) - return nil + return p, nil } // DoUpdate updates the given post on Write.as. diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 5185093..2280d3b 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "github.com/writeas/go-writeas" "gopkg.in/urfave/cli.v1" "io" "log" @@ -315,7 +316,7 @@ func readStdIn() []byte { return fullPost } -func handlePost(fullPost []byte, c *cli.Context) error { +func handlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { tor := isTor(c) if c.Int("tor-port") != 0 { torPort = c.Int("tor-port") diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index f86e843..085f4ed 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -9,7 +9,7 @@ import ( ) func cmdPost(c *cli.Context) error { - err := handlePost(readStdIn(), c) + _, err := handlePost(readStdIn(), c) return err } @@ -30,7 +30,7 @@ func cmdNew(c *cli.Context) error { InfolnQuit("Empty post. Bye!") } - err := handlePost(*p, c) + _, err := handlePost(*p, c) if err != nil { Errorln("Error posting: %s", err) Errorln(messageRetryCompose(fname)) @@ -54,7 +54,20 @@ func cmdPublish(c *cli.Context) error { if err != nil { return err } - return handlePost(content, c) + p, err := handlePost(content, c) + if err != nil { + return err + } + + // Save post to posts folder + cfg, err := loadConfig() + if cfg.Posts.Directory != "" { + err = WritePost(cfg.Posts.Directory, p) + if err != nil { + return err + } + } + return nil } func cmdDelete(c *cli.Context) error { diff --git a/cmd/writeas/posts.go b/cmd/writeas/posts.go index 8150570..a7f426d 100644 --- a/cmd/writeas/posts.go +++ b/cmd/writeas/posts.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/writeas/go-writeas" "github.com/writeas/writeas-cli/fileutils" "io/ioutil" "os" @@ -146,3 +147,19 @@ func composeNewPost() (string, *[]byte) { } return f.Name(), &post } + +func WritePost(postsDir string, p *writeas.Post) error { + postFilename := p.ID + collDir := "" + if p.Collection != nil { + postFilename = p.Slug + collDir = p.Collection.Alias + } + postFilename += postFileExt + + txtFile := p.Content + if p.Title != "" { + txtFile = "# " + p.Title + "\n\n" + txtFile + } + return ioutil.WriteFile(filepath.Join(postsDir, collDir, postFilename), []byte(txtFile), 0644) +} diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go index f02fda0..c9951a3 100644 --- a/cmd/writeas/userconfig.go +++ b/cmd/writeas/userconfig.go @@ -29,6 +29,7 @@ type ( ) func loadConfig() (*UserConfig, error) { + // TODO: load config to var shared across app cfg, err := ini.LooseLoad(filepath.Join(userDataDir(), userConfigFile)) if err != nil { return nil, err From abd989376cda8d4398a89f048444980ca3040b15 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 01:54:07 +0100 Subject: [PATCH 028/181] Support deleting auth'd user posts --- cmd/writeas/commands.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 085f4ed..f0cd654 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -77,10 +77,11 @@ func cmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } + u, _ := loadUser() if token == "" { // Search for the token locally token = tokenFromID(friendlyID) - if token == "" { + if token == "" && u == nil { Errorln("Couldn't find an edit token locally. Did you create this post here?") ErrorlnQuit("If you have an edit token, use: writeas delete %s ", friendlyID) } From 9d0b93785b7f87fe2a8d6c747558b7f0c20cac04 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 01:55:00 +0100 Subject: [PATCH 029/181] Delete local file when deleting anonymous post --- cmd/writeas/commands.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index f0cd654..cc2d1c1 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -3,9 +3,11 @@ package main import ( "fmt" "github.com/howeyc/gopass" + "github.com/writeas/writeas-cli/fileutils" "gopkg.in/urfave/cli.v1" "io/ioutil" "os" + "path/filepath" ) func cmdPost(c *cli.Context) error { @@ -97,7 +99,22 @@ func cmdDelete(c *cli.Context) error { Info(c, "Deleting...") } - return DoDelete(c, friendlyID, token, tor) + err := DoDelete(c, friendlyID, token, tor) + if err != nil { + return err + } + + // Delete local file, if necessary + cfg, err := loadConfig() + if cfg.Posts.Directory != "" { + // TODO: handle deleting blog posts + err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+postFileExt)) + if err != nil { + return err + } + } + + return nil } func cmdUpdate(c *cli.Context) error { From e3dc39245cebe4c11c4c070db795a5dce6751e27 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 02:01:26 +0100 Subject: [PATCH 030/181] Show post title when fetching posts --- cmd/writeas/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 809a591..d60c1f2 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -54,6 +54,9 @@ func DoFetch(friendlyID, ua string, tor bool) error { return err } + if p.Title != "" { + fmt.Printf("# %s\n\n", string(p.Title)) + } fmt.Printf("%s\n", string(p.Content)) return nil } From 40880f31675016236d34465287346126dd48f1be Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 02:03:28 +0100 Subject: [PATCH 031/181] Support updating auth'd user posts --- cmd/writeas/commands.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index cc2d1c1..fd9339a 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -124,10 +124,11 @@ func cmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } + u, _ := loadUser() if token == "" { // Search for the token locally token = tokenFromID(friendlyID) - if token == "" { + if token == "" && u == nil { Errorln("Couldn't find an edit token locally. Did you create this post here?") ErrorlnQuit("If you have an edit token, use: writeas update %s ", friendlyID) } From b37d9f69969d0b8f9dc1ad92fc79bac180e25bb0 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 15:56:17 +0100 Subject: [PATCH 032/181] Use dev endpoints when env var WRITEAS_DEV=1 --- cmd/writeas/api.go | 14 ++++++++++++-- cmd/writeas/cli.go | 1 + cmd/writeas/commands.go | 6 +++++- cmd/writeas/dev.go | 9 +++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 cmd/writeas/dev.go diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index d60c1f2..bd9b3d7 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -18,7 +18,11 @@ func client(userAgent string, tor bool) *writeas.Client { if tor { client = writeas.NewTorClient(torPort) } else { - client = writeas.NewClient() + if isDev() { + client = writeas.NewDevClient() + } else { + client = writeas.NewClient() + } } client.UserAgent = userAgent @@ -30,7 +34,11 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { if isTor(c) { client = writeas.NewTorClient(torPort) } else { - client = writeas.NewClient() + if isDev() { + client = writeas.NewDevClient() + } else { + client = writeas.NewClient() + } } client.UserAgent = userAgent(c) // TODO: load user into var shared across the app @@ -86,6 +94,8 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( } else { if tor { url = torBaseURL + } else if isDev() { + url = devBaseURL } else { url = writeasBaseURL } diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 2280d3b..cdd2fdc 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -12,6 +12,7 @@ import ( // API constants for communicating with Write.as. const ( writeasBaseURL = "https://write.as" + devBaseURL = "https://development.write.as" torBaseURL = "http://writeas7pm7rcdqg.onion" ) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index fd9339a..3195b46 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -192,7 +192,11 @@ func cmdList(c *cli.Context) error { fmt.Printf("%s ", p.ID) } if urls { - fmt.Printf("https://write.as/%s ", p.ID) + base := writeasBaseURL + if isDev() { + base = devBaseURL + } + fmt.Printf("%s/%s ", base, p.ID) } fmt.Print("\n") } diff --git a/cmd/writeas/dev.go b/cmd/writeas/dev.go new file mode 100644 index 0000000..a290838 --- /dev/null +++ b/cmd/writeas/dev.go @@ -0,0 +1,9 @@ +package main + +import ( + "os" +) + +func isDev() bool { + return os.Getenv("WRITEAS_DEV") == "1" +} From 23d4a692048a10adb76342e13a7649a9dec805fa Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 19 Sep 2018 16:14:13 +0100 Subject: [PATCH 033/181] Extract title when publishing or updating posts --- cmd/writeas/api.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index bd9b3d7..721ddb9 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/atotto/clipboard" "github.com/writeas/go-writeas" + "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/fileutils" "gopkg.in/urfave/cli.v1" "path/filepath" @@ -75,11 +76,10 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( cl, _ := newClient(c, false) pp := &writeas.PostParams{ - // TODO: extract title - Content: string(post), Font: getFont(code, font), Collection: collection(c), } + pp.Title, pp.Content = posts.ExtractTitle(string(post)) if lang := language(c); lang != "" { pp.Language = &lang } @@ -126,11 +126,10 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, cl, _ := newClient(c, false) params := writeas.PostParams{ - ID: friendlyID, - Token: token, - Content: string(post), - // TODO: extract title + ID: friendlyID, + Token: token, } + params.Title, params.Content = posts.ExtractTitle(string(post)) if code || font != "" { params.Font = getFont(code, font) } From 9807b0090e645c449f79541c7556b37b50112d89 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Sep 2018 12:28:57 +0200 Subject: [PATCH 034/181] Automatically detect language when publishing --- cmd/writeas/api.go | 2 +- cmd/writeas/options.go | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 721ddb9..8dc934d 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -80,7 +80,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( Collection: collection(c), } pp.Title, pp.Content = posts.ExtractTitle(string(post)) - if lang := language(c); lang != "" { + if lang := language(c, true); lang != "" { pp.Language = &lang } p, err := cl.CreatePost(pp) diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go index 71ca49e..30222cd 100644 --- a/cmd/writeas/options.go +++ b/cmd/writeas/options.go @@ -1,6 +1,7 @@ package main import ( + "github.com/cloudfoundry/jibber_jabber" "gopkg.in/urfave/cli.v1" ) @@ -16,8 +17,20 @@ func isTor(c *cli.Context) bool { return c.Bool("tor") || c.Bool("t") } -func language(c *cli.Context) string { - return c.String("lang") +func language(c *cli.Context, auto bool) string { + if l := c.String("lang"); l != "" { + return l + } + if !auto { + return "" + } + // Automatically detect language + l, err := jibber_jabber.DetectLanguage() + if err != nil { + Info(c, "Language detection failed: %s", err) + return "" + } + return l } func collection(c *cli.Context) string { From 30116465535eb82c142e4a7d86680514285afc84 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Sep 2018 12:31:43 +0200 Subject: [PATCH 035/181] Support changing post language --- cmd/writeas/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 8dc934d..798126e 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -130,6 +130,9 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, Token: token, } params.Title, params.Content = posts.ExtractTitle(string(post)) + if lang := language(c, false); lang != "" { + params.Language = &lang + } if code || font != "" { params.Font = getFont(code, font) } From 62263e5ce69b54787d39c83fe6070d2f2ffafa34 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 24 Sep 2018 11:51:16 -0400 Subject: [PATCH 036/181] Import v2 of go-writeas repo --- cmd/writeas/api.go | 2 +- cmd/writeas/cli.go | 2 +- cmd/writeas/posts.go | 2 +- cmd/writeas/userconfig.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 798126e..52a499f 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -3,9 +3,9 @@ package main import ( "fmt" "github.com/atotto/clipboard" - "github.com/writeas/go-writeas" "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/fileutils" + "go.code.as/writeas.v2" "gopkg.in/urfave/cli.v1" "path/filepath" ) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index cdd2fdc..75ebc51 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -2,7 +2,7 @@ package main import ( "bufio" - "github.com/writeas/go-writeas" + "go.code.as/writeas.v2" "gopkg.in/urfave/cli.v1" "io" "log" diff --git a/cmd/writeas/posts.go b/cmd/writeas/posts.go index a7f426d..175341a 100644 --- a/cmd/writeas/posts.go +++ b/cmd/writeas/posts.go @@ -2,8 +2,8 @@ package main import ( "fmt" - "github.com/writeas/go-writeas" "github.com/writeas/writeas-cli/fileutils" + "go.code.as/writeas.v2" "io/ioutil" "os" "path/filepath" diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go index c9951a3..e904dff 100644 --- a/cmd/writeas/userconfig.go +++ b/cmd/writeas/userconfig.go @@ -2,8 +2,8 @@ package main import ( "encoding/json" - "github.com/writeas/go-writeas" "github.com/writeas/writeas-cli/fileutils" + "go.code.as/writeas.v2" "gopkg.in/ini.v1" "io/ioutil" "path/filepath" From 8f9418246a4a061f0e95783151f7ca76f97fa56d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 24 Sep 2018 14:42:34 -0400 Subject: [PATCH 037/181] Don't require -u flag for writeas auth Instead of writeas auth -u , now you can just use writeas auth --- cmd/writeas/cli.go | 5 ----- cmd/writeas/commands.go | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 75ebc51..4c5aba2 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -239,11 +239,6 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, - cli.StringFlag{ - Name: "u", - Usage: "Username for authentication", - Value: "", - }, }, }, { diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 3195b46..2157df7 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -214,9 +214,9 @@ func cmdAuth(c *cli.Context) error { } // Validate arguments and get password - username := c.String("u") + username := c.Args().Get(0) if username == "" { - return cli.NewExitError("usage: writeas auth -u ", 1) + return cli.NewExitError("usage: writeas auth ", 1) } fmt.Print("Password: ") From 2e95856826b658a396ae6967700814d1023372b7 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 25 Sep 2018 13:07:56 -0400 Subject: [PATCH 038/181] Add 1.1 changes to Debian changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 06d28d8..cb5373d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +writeas-cli (1.1+git20171119.dc1ab5b-1ubuntu1) xenial; urgency=medium + + * Add --user-agent flag + + -- Write.as Tue, 25 Sep 2018 13:02:41 -0400 + writeas-cli (1.0+git20171119.dc1ab5b-1) xenial; urgency=medium * All logging and errors go to stderr, not stdout (Closes: #11) From 42374c410209562b5dabb7acb3ddca3b573a849a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Sep 2018 09:00:12 -0400 Subject: [PATCH 039/181] Support the -v/--verbose flag on all commands --- cmd/writeas/cli.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 4c5aba2..73fa16a 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -142,6 +142,10 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, { @@ -166,6 +170,10 @@ func main() { Name: "font", Usage: "Sets post font to given value", }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, { @@ -182,6 +190,10 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, { @@ -223,6 +235,10 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, { @@ -239,6 +255,10 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, { @@ -255,6 +275,10 @@ func main() { Usage: "Use a different port to connect to Tor", Value: 9150, }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, }, }, } From 3f1ac7df4ff55639907a9b9a78fb14aa5a09a424 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 27 Sep 2018 15:08:28 -0400 Subject: [PATCH 040/181] Update "not logged in" error message Leave out -u option in writeas auth command --- cmd/writeas/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 52a499f..66c3fe0 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -47,7 +47,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { if u != nil { client.SetToken(u.AccessToken) } else if authRequired { - return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth -u ") + return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth ") } return client, nil From 13cf6d1edaea20f7b12290c97ef0bf607f52ebb2 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 27 Sep 2018 21:15:19 -0400 Subject: [PATCH 041/181] Bump version to 1.2-dev --- cmd/writeas/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 73fa16a..d399de5 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -18,7 +18,7 @@ const ( // Application constants. const ( - version = "1.1" + version = "1.2-dev" ) // Defaults for posts on Write.as. From be18239017a2910b1ff16e272b6cd36601722649 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 27 Sep 2018 21:17:07 -0400 Subject: [PATCH 042/181] Make "already auth'd" error more helpful --- cmd/writeas/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 2157df7..a99fb1b 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -210,7 +210,7 @@ func cmdAuth(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } if u != nil && u.AccessToken != "" { - return cli.NewExitError("You're already authenticated.", 1) + return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1) } // Validate arguments and get password From 3bd82e36e53d2f94e019ba5a0d6d420ad95e975e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 27 Sep 2018 21:24:01 -0400 Subject: [PATCH 043/181] Create .writeas_user file in Write.as posts dir --- cmd/writeas/sync.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/sync.go b/cmd/writeas/sync.go index 0d17c4d..110befa 100644 --- a/cmd/writeas/sync.go +++ b/cmd/writeas/sync.go @@ -11,7 +11,8 @@ import ( ) const ( - postFileExt = ".txt" + postFileExt = ".txt" + userFilename = "writeas_user" ) func cmdPull(c *cli.Context) error { @@ -77,6 +78,13 @@ func cmdPull(c *cli.Context) error { // TODO: move UserConfig to its own package, and this to sync package func syncSetUp(cfg *UserConfig) error { + // Get user information and fail early (before we make the user do + // anything), if we're going to + u, err := loadUser() + if err != nil { + return err + } + // Prompt for posts directory defaultDir, err := os.Getwd() if err != nil { @@ -89,6 +97,9 @@ func syncSetUp(cfg *UserConfig) error { dir = defaultDir } + // FIXME: This only works on non-Windows OSes (fix: https://www.reddit.com/r/golang/comments/5t3ezd/hidden_files_directories/) + userFilepath := filepath.Join(dir, "."+userFilename) + // Create directory if needed if !fileutils.Exists(dir) { err = os.MkdirAll(dir, 0700) @@ -98,6 +109,8 @@ func syncSetUp(cfg *UserConfig) error { } return err } + // Create username file in directory + err = ioutil.WriteFile(userFilepath, []byte(u.User.Username), 0644) fmt.Println("Created posts directory.") } From 70c9e28db35273eefafbe5696841a3cbd8628061 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:13:30 -0400 Subject: [PATCH 044/181] Update OSes in GUIDE --- GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDE.md b/GUIDE.md index 900d04d..5588261 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -46,7 +46,7 @@ https://write.as/aaaaaaaaaaaa This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting): -OS X / *nix: `cat writeas/cli.go | writeas --code` +macOS / Linux: `cat writeas/cli.go | writeas --code` Windows: `type writeas/cli.go | writeas.exe --code` From 72444b09aa5b6cb269e0b48da1277e199625ba03 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:13:59 -0400 Subject: [PATCH 045/181] Add --md flag This returns URLs with Markdown enabled. Part of #14 --- GUIDE.md | 6 +++++- cmd/writeas/api.go | 4 ++++ cmd/writeas/cli.go | 8 ++++++++ cmd/writeas/commands.go | 7 ++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 5588261..07b308f 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -61,7 +61,9 @@ Hello world! #### List all published posts -This lists all posts you've published from your device. Pass the `--url` flag to show the list with full URLs. +This lists all posts you've published from your device. + +Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. ```bash $ writeas list @@ -101,3 +103,5 @@ Customize your post's appearance with the `--font` flag: | `code` | Syntax-highlighted monospace | No | Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans` + +If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md` diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index ab830ca..1f441da 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -112,6 +112,10 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) e addPost(id, token) + // Output URL in requested format + if c.Bool("md") { + url = append(url, []byte(".md")...) + } // Copy URL to clipboard err = clipboard.WriteAll(string(url)) if err != nil { diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 2a41acd..b96e77d 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -40,6 +40,10 @@ var postFlags = []cli.Flag{ Name: "code", Usage: "Specifies this post is code", }, + cli.BoolFlag{ + Name: "md", + Usage: "Returns post URL with Markdown enabled", + }, cli.BoolFlag{ Name: "verbose, v", Usage: "Make the operation more talkative", @@ -186,6 +190,10 @@ func main() { Name: "id", Usage: "Show list with post IDs (default)", }, + cli.BoolFlag{ + Name: "md", + Usage: "Use with --url to return URLs with Markdown enabled", + }, cli.BoolFlag{ Name: "url", Usage: "Show list with URLs", diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index b9d5399..f984848 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -146,7 +146,12 @@ func cmdList(c *cli.Context) error { fmt.Printf("%s ", p.ID) } if urls { - fmt.Printf("https://write.as/%s ", p.ID) + ext := "" + // Output URL in requested format + if c.Bool("md") { + ext = ".md" + } + fmt.Printf("https://write.as/%s%s ", p.ID, ext) } fmt.Print("\n") } From f45f03b408b8a6996e7baf2a59346e3897e92902 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:18:05 -0400 Subject: [PATCH 046/181] Bump version to 1.2 --- cmd/writeas/cli.go | 2 +- debian/changelog | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index b96e77d..0db42f5 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -17,7 +17,7 @@ const ( // Application constants. const ( - version = "1.1" + version = "1.2" ) // Defaults for posts on Write.as. diff --git a/debian/changelog b/debian/changelog index cb5373d..eeb47d5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +writeas-cli (1.2) xenial; urgency=medium + + * Add --md flag + + -- Write.as Sat, 29 Sep 2018 12:15:27 -0400 + writeas-cli (1.1+git20171119.dc1ab5b-1ubuntu1) xenial; urgency=medium * Add --user-agent flag From 910b09274ea3b657d6efafe0252e85d734e111f7 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:32:13 -0400 Subject: [PATCH 047/181] Update README with v1.2 links --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9bfa4e6..e20c775 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. **Windows**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_windows_386.zip) executable and put it somewhere in your `%PATH%`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. -**Linux**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.1/writeas_1.1_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +**Linux (other)**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. ### Go get it ```bash From e05f5bd9432323459d91251782a0b1f71455584e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:32:27 -0400 Subject: [PATCH 048/181] Add Debian installation instructions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e20c775..fdebb13 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,13 @@ Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v **macOS**
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +**Debian-based Linux**
+```bash +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445 +sudo add-apt-repository "deb http://updates.writeas.org xenial main" +sudo apt-get update && sudo apt-get install writeas-cli +``` + **Linux (other)**
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. From 8156f21643e25c43fa34ea203a6dc56785f32c38 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 1 Oct 2018 11:42:03 -0400 Subject: [PATCH 049/181] Change WIP version to 1.99-dev --- cmd/writeas/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 84f1a9e..6092688 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -18,7 +18,7 @@ const ( // Application constants. const ( - version = "2.0-dev" + version = "1.99-dev" ) // Defaults for posts on Write.as. From 18ce1fc0e370d6197e3f363e8c57b11d5b7863bb Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 10 Oct 2018 15:12:46 -0400 Subject: [PATCH 050/181] Update UpdatePost and DeletePost calls to v2 --- cmd/writeas/api.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 230b3c8..670dba2 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -129,10 +129,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { cl, _ := newClient(c, false) - params := writeas.PostParams{ - ID: friendlyID, - Token: token, - } + params := writeas.PostParams{} params.Title, params.Content = posts.ExtractTitle(string(post)) if lang := language(c, false); lang != "" { params.Language = &lang @@ -141,7 +138,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, params.Font = getFont(code, font) } - _, err := cl.UpdatePost(¶ms) + _, err := cl.UpdatePost(friendlyID, token, ¶ms) if err != nil { if debug { ErrorlnQuit("Problem updating: %v", err) @@ -161,10 +158,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { cl, _ := newClient(c, false) - err := cl.DeletePost(&writeas.PostParams{ - ID: friendlyID, - Token: token, - }) + err := cl.DeletePost(friendlyID, token) if err != nil { if debug { ErrorlnQuit("Problem deleting: %v", err) From 035ffcde5fc9d35b354f605895db7337288a22f7 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 17 Dec 2018 09:52:47 -0500 Subject: [PATCH 051/181] Relicense under GPL --- LICENSE | 686 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 2 +- 2 files changed, 670 insertions(+), 18 deletions(-) diff --git a/LICENSE b/LICENSE index 7371932..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,674 @@ -The MIT License (MIT) + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Copyright (c) 2015 Write.as + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU General Public License is a free, copyleft license for +software and other kinds of works. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index fb4d58a..ef43b03 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ writeas-cli =========== -![MIT license](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) +![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. From 5a690720d10542029542b4bc37220c248380f0c3 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 16 Dec 2018 23:41:19 -0800 Subject: [PATCH 052/181] Add go.mod/sum This adds a go.mod and go.sum to the project. --- go.mod | 24 ++++++++++++++++++++++++ go.sum | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a416f43 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/writeas/writeas-cli + +require ( + code.as/core/socks v0.0.0-20180906144846-5be269b4e664 + github.com/atotto/clipboard v0.1.1 + github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 + github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/microcosm-cc/bluemonday v1.0.1 // indirect + github.com/mitchellh/go-homedir v1.0.0 + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect + github.com/writeas/impart v0.0.0-20180808220913-fef51864677b // indirect + github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect + github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 + go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect + golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect + golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect + gopkg.in/ini.v1 v1.39.3 + gopkg.in/urfave/cli.v1 v1.20.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11512e2 --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +code.as/core/socks v0.0.0-20180906144846-5be269b4e664 h1:zWSFbwkYSuZ2PjvHqYDE/dhd9CCcsbSvUIRx8hIed3I= +code.as/core/socks v0.0.0-20180906144846-5be269b4e664/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= +github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVGw= +github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= +github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= +github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= +github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= +github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f h1:yyFguE0EopK8e7I7/AB1JWM925OFOI1uFhTM/SwXAnQ= +github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= +github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 h1:mKD4DMZuiZWrn1k/f+1wLmBu9SYMrydy9om+eeo9kjA= +github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE= +go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e h1:emU11ZqEW7s+6/Ty52t0lQ9c3Mg+c97YSwswUeSpsG8= +go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e/go.mod h1:wH0YOXh4B2fcSJ/ihy+qru0XfCdGb4CPKaO0qS2g47k= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= +golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= +golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/ini.v1 v1.39.3 h1:+LGDwGPQXrK1zLmDY5GMdgX7uNvs4iS+9fIRAGaDBbg= +gopkg.in/ini.v1 v1.39.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= From 5dfb56ec480425749e286353ee950db7ac3e3f89 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Mon, 17 Dec 2018 07:49:15 -0800 Subject: [PATCH 053/181] style: Run goimports on all Go files As discussed in #20, this runs `goimports` on all Go files separating imports into two groups: standard library and everything else. --- cmd/writeas/api.go | 7 ++++--- cmd/writeas/cli.go | 5 +++-- cmd/writeas/commands.go | 7 ++++--- cmd/writeas/logging.go | 3 ++- cmd/writeas/options.go | 2 +- cmd/writeas/posts.go | 5 +++-- cmd/writeas/posts_nix.go | 3 ++- cmd/writeas/sync.go | 5 +++-- cmd/writeas/tor.go | 3 ++- cmd/writeas/userconfig.go | 7 ++++--- 10 files changed, 28 insertions(+), 19 deletions(-) diff --git a/cmd/writeas/api.go b/cmd/writeas/api.go index 670dba2..b46c022 100644 --- a/cmd/writeas/api.go +++ b/cmd/writeas/api.go @@ -2,12 +2,13 @@ package main import ( "fmt" + "path/filepath" + "github.com/atotto/clipboard" "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/fileutils" - "go.code.as/writeas.v2" - "gopkg.in/urfave/cli.v1" - "path/filepath" + writeas "go.code.as/writeas.v2" + cli "gopkg.in/urfave/cli.v1" ) const ( diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 6092688..8e0c548 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -2,11 +2,12 @@ package main import ( "bufio" - "go.code.as/writeas.v2" - "gopkg.in/urfave/cli.v1" "io" "log" "os" + + writeas "go.code.as/writeas.v2" + cli "gopkg.in/urfave/cli.v1" ) // API constants for communicating with Write.as. diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 20e2b7c..a2d0eb1 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -2,12 +2,13 @@ package main import ( "fmt" - "github.com/howeyc/gopass" - "github.com/writeas/writeas-cli/fileutils" - "gopkg.in/urfave/cli.v1" "io/ioutil" "os" "path/filepath" + + "github.com/howeyc/gopass" + "github.com/writeas/writeas-cli/fileutils" + cli "gopkg.in/urfave/cli.v1" ) func cmdPost(c *cli.Context) error { diff --git a/cmd/writeas/logging.go b/cmd/writeas/logging.go index 98fb5ec..988209c 100644 --- a/cmd/writeas/logging.go +++ b/cmd/writeas/logging.go @@ -2,8 +2,9 @@ package main import ( "fmt" - "gopkg.in/urfave/cli.v1" "os" + + cli "gopkg.in/urfave/cli.v1" ) // Info logs general diagnostic messages, shown only when the -v or --verbose diff --git a/cmd/writeas/options.go b/cmd/writeas/options.go index 30222cd..1b0e065 100644 --- a/cmd/writeas/options.go +++ b/cmd/writeas/options.go @@ -2,7 +2,7 @@ package main import ( "github.com/cloudfoundry/jibber_jabber" - "gopkg.in/urfave/cli.v1" + cli "gopkg.in/urfave/cli.v1" ) func userAgent(c *cli.Context) string { diff --git a/cmd/writeas/posts.go b/cmd/writeas/posts.go index 175341a..180693b 100644 --- a/cmd/writeas/posts.go +++ b/cmd/writeas/posts.go @@ -2,12 +2,13 @@ package main import ( "fmt" - "github.com/writeas/writeas-cli/fileutils" - "go.code.as/writeas.v2" "io/ioutil" "os" "path/filepath" "strings" + + "github.com/writeas/writeas-cli/fileutils" + writeas "go.code.as/writeas.v2" ) const ( diff --git a/cmd/writeas/posts_nix.go b/cmd/writeas/posts_nix.go index ee4d78b..52fbae5 100644 --- a/cmd/writeas/posts_nix.go +++ b/cmd/writeas/posts_nix.go @@ -4,8 +4,9 @@ package main import ( "fmt" - "github.com/mitchellh/go-homedir" "os/exec" + + homedir "github.com/mitchellh/go-homedir" ) const ( diff --git a/cmd/writeas/sync.go b/cmd/writeas/sync.go index 110befa..c14a8f5 100644 --- a/cmd/writeas/sync.go +++ b/cmd/writeas/sync.go @@ -3,11 +3,12 @@ package main import ( //"github.com/writeas/writeas-cli/sync" "fmt" - "github.com/writeas/writeas-cli/fileutils" - "gopkg.in/urfave/cli.v1" "io/ioutil" "os" "path/filepath" + + "github.com/writeas/writeas-cli/fileutils" + cli "gopkg.in/urfave/cli.v1" ) const ( diff --git a/cmd/writeas/tor.go b/cmd/writeas/tor.go index 00f1821..d4dc585 100644 --- a/cmd/writeas/tor.go +++ b/cmd/writeas/tor.go @@ -1,9 +1,10 @@ package main import ( - "code.as/core/socks" "fmt" "net/http" + + "code.as/core/socks" ) var ( diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go index e904dff..66a48e7 100644 --- a/cmd/writeas/userconfig.go +++ b/cmd/writeas/userconfig.go @@ -2,11 +2,12 @@ package main import ( "encoding/json" - "github.com/writeas/writeas-cli/fileutils" - "go.code.as/writeas.v2" - "gopkg.in/ini.v1" "io/ioutil" "path/filepath" + + "github.com/writeas/writeas-cli/fileutils" + writeas "go.code.as/writeas.v2" + ini "gopkg.in/ini.v1" ) const ( From 71d204ab1c72b9d2f4d0265907e60475cc30545b Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Mon, 17 Dec 2018 07:49:51 -0800 Subject: [PATCH 054/181] CONTRIBUTING: Add import group conventions As discussed in #20, this adds import grouping recommendations to the coding conventions section of CONTRIBUTING.md. Note that I replaced `go fmt` with `goimports` because `goimports` implements a superset of the `go fmt` functionality. --- .github/CONTRIBUTING.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8c14a19..997f669 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,11 +22,20 @@ Please follow our coding conventions below and make sure all of your commits are We strive for consistency above all. Reading the small codebase should give you a good idea of the conventions we follow. -* We use `go fmt` before committing anything +* We use `goimports` before committing anything * We aim to document all exported entities * Go files are broken up into logical functional components * General functions are extracted into modules when possible +### Import Groups + +We aim for two import groups: + +* Standard library imports +* Everything else + +`goimports` already does this for you along with running `go fmt`. + ## Design conventions We maintain a few high-level design principles in all decisions we make. Keep these in mind while devising new functionality: From 6b2d2408f9c37b1abef459fa9e2cf8c274d9882d Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 27 May 2019 13:39:28 -0700 Subject: [PATCH 055/181] fix env variable typo for WRITEAS_EDITOR --- cmd/writeas/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/writeas/cli.go b/cmd/writeas/cli.go index 8e0c548..82c3c62 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/cli.go @@ -114,7 +114,7 @@ func main() { On Windows, this will use 'copy con' to start reading what you input from the prompt. Press F6 or Ctrl-Z then Enter to end input. On *nix, this will use the best available text editor, starting with the - value set to the WRITAS_EDITOR or EDITOR environment variable, or vim, or + value set to the WRITEAS_EDITOR or EDITOR environment variable, or vim, or finally nano. Use the --code flag to indicate that the post should use syntax From f1e90e91b328ab9cf8da40353b55bf24a37cc5b4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 29 May 2019 12:51:35 -0400 Subject: [PATCH 056/181] Mention dev environment in logs --- cmd/writeas/commands.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index a2d0eb1..31dfe23 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -96,6 +96,8 @@ func cmdDelete(c *cli.Context) error { } if tor { Info(c, "Deleting via hidden service...") + } else if isDev() { + Info(c, "Deleting via dev environment...") } else { Info(c, "Deleting...") } @@ -144,6 +146,8 @@ func cmdUpdate(c *cli.Context) error { } if tor { Info(c, "Updating via hidden service...") + } else if isDev() { + Info(c, "Updating via dev environment...") } else { Info(c, "Updating...") } @@ -163,6 +167,8 @@ func cmdGet(c *cli.Context) error { } if tor { Info(c, "Getting via hidden service...") + } else if isDev() { + Info(c, "Getting via dev environment...") } else { Info(c, "Getting...") } From 0ebe4fc49c504a68a884fa06013d5c56fa8d8873 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 29 May 2019 11:20:41 -0700 Subject: [PATCH 057/181] add error check on log in attempt --- cmd/writeas/commands.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/writeas/commands.go b/cmd/writeas/commands.go index 31dfe23..deabf83 100644 --- a/cmd/writeas/commands.go +++ b/cmd/writeas/commands.go @@ -241,7 +241,12 @@ func cmdAuth(c *cli.Context) error { if len(pass) == 0 { return cli.NewExitError("Please enter your password.", 1) } - return DoLogIn(c, username, string(pass)) + err = DoLogIn(c, username, string(pass)) + if err != nil { + return cli.NewExitError(fmt.Sprintf("error logging in: %v", err), 1) + } + + return nil } func cmdLogOut(c *cli.Context) error { From efa3f32a356832bb646c711e5b780f9cfafb228f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 27 May 2019 17:18:45 -0700 Subject: [PATCH 058/181] Ref T597 split out shared code first moving everything out into the base package should be followed by some refactoring and reorganizing before creating two different binaries for write.as and writefreely --- cmd/writeas/api.go => api.go | 24 ++- cmd/flags.go | 51 ++++++ cmd/writeas/{cli.go => main.go} | 149 +++--------------- cmd/writeas/userconfig.go | 91 ----------- cmd/writeas/commands.go => commands.go | 39 ++--- cmd/writeas/config.go => config.go | 2 +- config/config.go | 51 ++++++ .../config_debug.go => config_debug.go | 2 +- cmd/writeas/dev.go => dev.go | 4 +- cmd/writeas/edit.go => edit.go | 4 +- cmd/writeas/errors.go => errors.go | 2 +- cmd/writeas/fonts.go => fonts.go | 8 +- go.mod | 2 + go.sum | 21 +++ cmd/writeas/logging.go => logging.go | 2 +- cmd/writeas/options.go => options.go | 13 +- cmd/writeas/posts.go => posts.go | 53 ++++++- cmd/writeas/posts_nix.go => posts_nix.go | 4 +- cmd/writeas/posts_win.go => posts_win.go | 2 +- cmd/writeas/sync.go => sync.go | 13 +- cmd/writeas/tor.go => tor.go | 2 +- user.go | 47 ++++++ 22 files changed, 308 insertions(+), 278 deletions(-) rename cmd/writeas/api.go => api.go (93%) create mode 100644 cmd/flags.go rename cmd/writeas/{cli.go => main.go} (68%) delete mode 100644 cmd/writeas/userconfig.go rename cmd/writeas/commands.go => commands.go (87%) rename cmd/writeas/config.go => config.go (69%) create mode 100644 config/config.go rename cmd/writeas/config_debug.go => config_debug.go (68%) rename cmd/writeas/dev.go => dev.go (61%) rename cmd/writeas/edit.go => edit.go (75%) rename cmd/writeas/errors.go => errors.go (80%) rename cmd/writeas/fonts.go => fonts.go (85%) rename cmd/writeas/logging.go => logging.go (97%) rename cmd/writeas/options.go => options.go (69%) rename cmd/writeas/posts.go => posts.go (77%) rename cmd/writeas/posts_nix.go => posts_nix.go (93%) rename cmd/writeas/posts_win.go => posts_win.go (96%) rename cmd/writeas/sync.go => sync.go (91%) rename cmd/writeas/tor.go => tor.go (94%) create mode 100644 user.go diff --git a/cmd/writeas/api.go b/api.go similarity index 93% rename from cmd/writeas/api.go rename to api.go index b46c022..b3c59ef 100644 --- a/cmd/writeas/api.go +++ b/api.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "fmt" @@ -11,16 +11,12 @@ import ( cli "gopkg.in/urfave/cli.v1" ) -const ( - defaultUserAgent = "writeas-cli v" + version -) - func client(userAgent string, tor bool) *writeas.Client { var client *writeas.Client if tor { client = writeas.NewTorClient(torPort) } else { - if isDev() { + if IsDev() { client = writeas.NewDevClient() } else { client = writeas.NewClient() @@ -36,7 +32,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { if isTor(c) { client = writeas.NewTorClient(torPort) } else { - if isDev() { + if IsDev() { client = writeas.NewDevClient() } else { client = writeas.NewClient() @@ -44,7 +40,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { } client.UserAgent = userAgent(c) // TODO: load user into var shared across the app - u, _ := loadUser() + u, _ := LoadUser(userDataDir()) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -94,11 +90,11 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( url = p.Collection.URL + p.Slug } else { if tor { - url = torBaseURL - } else if isDev() { - url = devBaseURL + url = TorBaseURL + } else if IsDev() { + url = DevBaseURL } else { - url = writeasBaseURL + url = WriteasBaseURL } url += "/" + p.ID // Output URL in requested format @@ -188,7 +184,7 @@ func DoLogIn(c *cli.Context, username, password string) error { return err } - err = saveUser(u) + err = SaveUser(userDataDir(), u) if err != nil { return err } @@ -211,7 +207,7 @@ func DoLogOut(c *cli.Context) error { } // Delete local user data - err = fileutils.DeleteFile(filepath.Join(userDataDir(), userFile)) + err = fileutils.DeleteFile(filepath.Join(userDataDir(), UserFile)) if err != nil { return err } diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 0000000..062c4be --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,51 @@ +package cmd + +import ( + writeascli "github.com/writeas/writeas-cli" + "gopkg.in/urfave/cli.v1" +) + +// Available flags for creating posts +var PostFlags = []cli.Flag{ + cli.StringFlag{ + Name: "c, b", + Usage: "Optional blog to post to", + Value: "", + }, + cli.BoolFlag{ + Name: "tor, t", + Usage: "Perform action on Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "code", + Usage: "Specifies this post is code", + }, + cli.BoolFlag{ + Name: "md", + Usage: "Returns post URL with Markdown enabled", + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + cli.StringFlag{ + Name: "font", + Usage: "Sets post font to given value", + Value: writeascli.DefaultFont, + }, + cli.StringFlag{ + Name: "lang", + Usage: "Sets post language to given ISO 639-1 language code", + Value: "", + }, + cli.StringFlag{ + Name: "user-agent", + Usage: "Sets the User-Agent for API requests", + Value: "", + }, +} diff --git a/cmd/writeas/cli.go b/cmd/writeas/main.go similarity index 68% rename from cmd/writeas/cli.go rename to cmd/writeas/main.go index 82c3c62..82ab356 100644 --- a/cmd/writeas/cli.go +++ b/cmd/writeas/main.go @@ -1,77 +1,13 @@ package main import ( - "bufio" - "io" - "log" "os" - writeas "go.code.as/writeas.v2" + writeascli "github.com/writeas/writeas-cli" + cmd "github.com/writeas/writeas-cli/cmd" cli "gopkg.in/urfave/cli.v1" ) -// API constants for communicating with Write.as. -const ( - writeasBaseURL = "https://write.as" - devBaseURL = "https://development.write.as" - torBaseURL = "http://writeas7pm7rcdqg.onion" -) - -// Application constants. -const ( - version = "1.99-dev" -) - -// Defaults for posts on Write.as. -const ( - defaultFont = PostFontMono -) - -// Available flags for creating posts -var postFlags = []cli.Flag{ - cli.StringFlag{ - Name: "c, b", - Usage: "Optional blog to post to", - Value: "", - }, - cli.BoolFlag{ - Name: "tor, t", - Usage: "Perform action on Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, - cli.BoolFlag{ - Name: "code", - Usage: "Specifies this post is code", - }, - cli.BoolFlag{ - Name: "md", - Usage: "Returns post URL with Markdown enabled", - }, - cli.BoolFlag{ - Name: "verbose, v", - Usage: "Make the operation more talkative", - }, - cli.StringFlag{ - Name: "font", - Usage: "Sets post font to given value", - Value: defaultFont, - }, - cli.StringFlag{ - Name: "lang", - Usage: "Sets post language to given ISO 639-1 language code", - Value: "", - }, - cli.StringFlag{ - Name: "user-agent", - Usage: "Sets the User-Agent for API requests", - Value: "", - }, -} - func main() { initialize() @@ -83,7 +19,7 @@ func main() { // Run the app app := cli.NewApp() app.Name = "writeas" - app.Version = version + app.Version = writeascli.Version app.Usage = "Publish text quickly" app.Authors = []cli.Author{ { @@ -91,14 +27,14 @@ func main() { Email: "hello@write.as", }, } - app.Action = cmdPost - app.Flags = postFlags + app.Action = writeascli.CmdPost + app.Flags = cmd.PostFlags app.Commands = []cli.Command{ { Name: "post", Usage: "Alias for default action: create post from stdin", - Action: cmdPost, - Flags: postFlags, + Action: writeascli.CmdPost, + Flags: cmd.PostFlags, Description: `Create a new post on Write.as from stdin. Use the --code flag to indicate that the post should use syntax @@ -124,19 +60,19 @@ func main() { If posting fails for any reason, 'writeas' will show you the temporary file location and how to pipe it to 'writeas' to retry.`, - Action: cmdNew, - Flags: postFlags, + Action: writeascli.CmdNew, + Flags: cmd.PostFlags, }, { Name: "publish", Usage: "Publish a file to Write.as", - Action: cmdPublish, - Flags: postFlags, + Action: writeascli.CmdPublish, + Flags: cmd.PostFlags, }, { Name: "delete", Usage: "Delete a post", - Action: cmdDelete, + Action: writeascli.CmdDelete, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -156,7 +92,7 @@ func main() { { Name: "update", Usage: "Update (overwrite) a post", - Action: cmdUpdate, + Action: writeascli.CmdUpdate, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -184,7 +120,7 @@ func main() { { Name: "get", Usage: "Read a raw post", - Action: cmdGet, + Action: writeascli.CmdGet, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -209,12 +145,12 @@ func main() { This requires a post ID (from https://write.as/[ID]) and an Edit Token (exported from another Write.as client, such as the Android app). `, - Action: cmdAdd, + Action: writeascli.CmdAdd, }, { Name: "list", Usage: "List local posts", - Action: cmdList, + Action: writeascli.CmdList, Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", @@ -233,7 +169,7 @@ func main() { { Name: "fetch", Usage: "Fetch authenticated user's Write.as posts", - Action: cmdPull, + Action: writeascli.CmdPull, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -253,7 +189,7 @@ func main() { { Name: "auth", Usage: "Authenticate with Write.as", - Action: cmdAuth, + Action: writeascli.CmdAuth, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -273,7 +209,7 @@ func main() { { Name: "logout", Usage: "Log out of Write.as", - Action: cmdLogOut, + Action: writeascli.CmdLogOut, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -311,50 +247,7 @@ OPTIONS: func initialize() { // Ensure we have a data directory to use - if !dataDirExists() { - createDataDir() - } -} - -func readStdIn() []byte { - numBytes, numChunks := int64(0), int64(0) - r := bufio.NewReader(os.Stdin) - fullPost := []byte{} - buf := make([]byte, 0, 1024) - for { - n, err := r.Read(buf[:cap(buf)]) - buf = buf[:n] - if n == 0 { - if err == nil { - continue - } - if err == io.EOF { - break - } - log.Fatal(err) - } - numChunks++ - numBytes += int64(len(buf)) - - fullPost = append(fullPost, buf...) - if err != nil && err != io.EOF { - log.Fatal(err) - } + if !writeascli.DataDirExists() { + writeascli.CreateDataDir() } - - return fullPost -} - -func handlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { - tor := isTor(c) - if c.Int("tor-port") != 0 { - torPort = c.Int("tor-port") - } - if tor { - Info(c, "Posting to hidden service...") - } else { - Info(c, "Posting...") - } - - return DoPost(c, fullPost, c.String("font"), false, tor, c.Bool("code")) } diff --git a/cmd/writeas/userconfig.go b/cmd/writeas/userconfig.go deleted file mode 100644 index 66a48e7..0000000 --- a/cmd/writeas/userconfig.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "path/filepath" - - "github.com/writeas/writeas-cli/fileutils" - writeas "go.code.as/writeas.v2" - ini "gopkg.in/ini.v1" -) - -const ( - userConfigFile = "config.ini" - userFile = "user.json" -) - -type ( - APIConfig struct { - } - - PostsConfig struct { - Directory string `ini:"directory"` - } - - UserConfig struct { - API APIConfig `ini:"api"` - Posts PostsConfig `ini:"posts"` - } -) - -func loadConfig() (*UserConfig, error) { - // TODO: load config to var shared across app - cfg, err := ini.LooseLoad(filepath.Join(userDataDir(), userConfigFile)) - if err != nil { - return nil, err - } - - // Parse INI file - uc := &UserConfig{} - err = cfg.MapTo(uc) - if err != nil { - return nil, err - } - return uc, nil -} - -func saveConfig(uc *UserConfig) error { - cfg := ini.Empty() - err := ini.ReflectFrom(cfg, uc) - if err != nil { - return err - } - - return cfg.SaveTo(filepath.Join(userDataDir(), userConfigFile)) -} - -func loadUser() (*writeas.AuthUser, error) { - fname := filepath.Join(userDataDir(), userFile) - userJSON, err := ioutil.ReadFile(fname) - if err != nil { - if !fileutils.Exists(fname) { - // Don't return a file-not-found error - return nil, nil - } - return nil, err - } - - // Parse JSON file - u := &writeas.AuthUser{} - err = json.Unmarshal(userJSON, u) - if err != nil { - return nil, err - } - return u, nil -} - -func saveUser(u *writeas.AuthUser) error { - // Marshal struct into pretty-printed JSON - userJSON, err := json.MarshalIndent(u, "", " ") - if err != nil { - return err - } - - // Save file - err = ioutil.WriteFile(filepath.Join(userDataDir(), userFile), userJSON, 0600) - if err != nil { - return err - } - return nil -} diff --git a/cmd/writeas/commands.go b/commands.go similarity index 87% rename from cmd/writeas/commands.go rename to commands.go index deabf83..d451654 100644 --- a/cmd/writeas/commands.go +++ b/commands.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "fmt" @@ -7,16 +7,17 @@ import ( "path/filepath" "github.com/howeyc/gopass" + "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" cli "gopkg.in/urfave/cli.v1" ) -func cmdPost(c *cli.Context) error { +func CmdPost(c *cli.Context) error { _, err := handlePost(readStdIn(), c) return err } -func cmdNew(c *cli.Context) error { +func CmdNew(c *cli.Context) error { fname, p := composeNewPost() if p == nil { // Assume composeNewPost already told us what the error was. Abort now. @@ -48,7 +49,7 @@ func cmdNew(c *cli.Context) error { return nil } -func cmdPublish(c *cli.Context) error { +func CmdPublish(c *cli.Context) error { filename := c.Args().Get(0) if filename == "" { return cli.NewExitError("usage: writeas publish ", 1) @@ -63,7 +64,7 @@ func cmdPublish(c *cli.Context) error { } // Save post to posts folder - cfg, err := loadConfig() + cfg, err := config.LoadConfig(userDataDir()) if cfg.Posts.Directory != "" { err = WritePost(cfg.Posts.Directory, p) if err != nil { @@ -73,14 +74,14 @@ func cmdPublish(c *cli.Context) error { return nil } -func cmdDelete(c *cli.Context) error { +func CmdDelete(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := loadUser() + u, _ := LoadUser(userDataDir()) if token == "" { // Search for the token locally token = tokenFromID(friendlyID) @@ -108,7 +109,7 @@ func cmdDelete(c *cli.Context) error { } // Delete local file, if necessary - cfg, err := loadConfig() + cfg, err := config.LoadConfig(userDataDir()) if cfg.Posts.Directory != "" { // TODO: handle deleting blog posts err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+postFileExt)) @@ -120,14 +121,14 @@ func cmdDelete(c *cli.Context) error { return nil } -func cmdUpdate(c *cli.Context) error { +func CmdUpdate(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := loadUser() + u, _ := LoadUser(userDataDir()) if token == "" { // Search for the token locally token = tokenFromID(friendlyID) @@ -155,7 +156,7 @@ func cmdUpdate(c *cli.Context) error { return DoUpdate(c, fullPost, friendlyID, token, c.String("font"), tor, c.Bool("code")) } -func cmdGet(c *cli.Context) error { +func CmdGet(c *cli.Context) error { friendlyID := c.Args().Get(0) if friendlyID == "" { return cli.NewExitError("usage: writeas get ", 1) @@ -176,7 +177,7 @@ func cmdGet(c *cli.Context) error { return DoFetch(friendlyID, userAgent(c), tor) } -func cmdAdd(c *cli.Context) error { +func CmdAdd(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" || token == "" { @@ -187,7 +188,7 @@ func cmdAdd(c *cli.Context) error { return err } -func cmdList(c *cli.Context) error { +func CmdList(c *cli.Context) error { urls := c.Bool("url") ids := c.Bool("id") @@ -199,9 +200,9 @@ func cmdList(c *cli.Context) error { fmt.Printf("%s ", p.ID) } if urls { - base := writeasBaseURL - if isDev() { - base = devBaseURL + base := WriteasBaseURL + if IsDev() { + base = DevBaseURL } ext := "" // Output URL in requested format @@ -215,9 +216,9 @@ func cmdList(c *cli.Context) error { return nil } -func cmdAuth(c *cli.Context) error { +func CmdAuth(c *cli.Context) error { // Check configuration - u, err := loadUser() + u, err := LoadUser(userDataDir()) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } @@ -249,6 +250,6 @@ func cmdAuth(c *cli.Context) error { return nil } -func cmdLogOut(c *cli.Context) error { +func CmdLogOut(c *cli.Context) error { return DoLogOut(c) } diff --git a/cmd/writeas/config.go b/config.go similarity index 69% rename from cmd/writeas/config.go rename to config.go index bf08f2c..36bc3c6 100644 --- a/cmd/writeas/config.go +++ b/config.go @@ -1,6 +1,6 @@ // +build !debug -package main +package writeascli const ( debug = false diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..8f60729 --- /dev/null +++ b/config/config.go @@ -0,0 +1,51 @@ +package config + +import ( + "path/filepath" + + ini "gopkg.in/ini.v1" +) + +const ( + UserConfigFile = "config.ini" +) + +type ( + APIConfig struct { + } + + PostsConfig struct { + Directory string `ini:"directory"` + } + + UserConfig struct { + API APIConfig `ini:"api"` + Posts PostsConfig `ini:"posts"` + } +) + +func LoadConfig(dataDir string) (*UserConfig, error) { + // TODO: load config to var shared across app + cfg, err := ini.LooseLoad(filepath.Join(dataDir, UserConfigFile)) + if err != nil { + return nil, err + } + + // Parse INI file + uc := &UserConfig{} + err = cfg.MapTo(uc) + if err != nil { + return nil, err + } + return uc, nil +} + +func SaveConfig(dataDir string, uc *UserConfig) error { + cfg := ini.Empty() + err := ini.ReflectFrom(cfg, uc) + if err != nil { + return err + } + + return cfg.SaveTo(filepath.Join(dataDir, UserConfigFile)) +} diff --git a/cmd/writeas/config_debug.go b/config_debug.go similarity index 68% rename from cmd/writeas/config_debug.go rename to config_debug.go index d51d841..eaa975a 100644 --- a/cmd/writeas/config_debug.go +++ b/config_debug.go @@ -1,6 +1,6 @@ // +build debug -package main +package writeascli const ( debug = true diff --git a/cmd/writeas/dev.go b/dev.go similarity index 61% rename from cmd/writeas/dev.go rename to dev.go index a290838..d029c3d 100644 --- a/cmd/writeas/dev.go +++ b/dev.go @@ -1,9 +1,9 @@ -package main +package writeascli import ( "os" ) -func isDev() bool { +func IsDev() bool { return os.Getenv("WRITEAS_DEV") == "1" } diff --git a/cmd/writeas/edit.go b/edit.go similarity index 75% rename from cmd/writeas/edit.go rename to edit.go index fdf72b7..349189f 100644 --- a/cmd/writeas/edit.go +++ b/edit.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "os" @@ -6,7 +6,7 @@ import ( var editors = []string{"WRITEAS_EDITOR", "EDITOR"} -func getConfiguredEditor() string { +func GetConfiguredEditor() string { for _, v := range editors { if e := os.Getenv(v); e != "" { return e diff --git a/cmd/writeas/errors.go b/errors.go similarity index 80% rename from cmd/writeas/errors.go rename to errors.go index 9acc555..a48a660 100644 --- a/cmd/writeas/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "errors" diff --git a/cmd/writeas/fonts.go b/fonts.go similarity index 85% rename from cmd/writeas/fonts.go rename to fonts.go index 28684e9..4910a7c 100644 --- a/cmd/writeas/fonts.go +++ b/fonts.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "fmt" @@ -30,7 +30,7 @@ var postFontMap = map[string]postFont{ func getFont(code bool, font string) string { if code { - if font != "" && font != defaultFont { + if font != "" && font != DefaultFont { fmt.Printf("A non-default font '%s' and --code flag given. 'code' type takes precedence.\n", font) } return "code" @@ -41,6 +41,6 @@ func getFont(code bool, font string) string { return string(f) } - fmt.Printf("Font '%s' invalid. Using default '%s'\n", font, defaultFont) - return string(defaultFont) + fmt.Printf("Font '%s' invalid. Using default '%s'\n", font, DefaultFont) + return string(DefaultFont) } diff --git a/go.mod b/go.mod index a416f43..ec850e6 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/microcosm-cc/bluemonday v1.0.1 // indirect github.com/mitchellh/go-homedir v1.0.0 + github.com/onsi/ginkgo v1.8.0 // indirect + github.com/onsi/gomega v1.5.0 // indirect github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect diff --git a/go.sum b/go.sum index 11512e2..9662d3c 100644 --- a/go.sum +++ b/go.sum @@ -4,16 +4,25 @@ github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVG github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -30,11 +39,23 @@ go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e h1:emU11ZqEW7s+6/Ty52t0 go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e/go.mod h1:wH0YOXh4B2fcSJ/ihy+qru0XfCdGb4CPKaO0qS2g47k= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.39.3 h1:+LGDwGPQXrK1zLmDY5GMdgX7uNvs4iS+9fIRAGaDBbg= gopkg.in/ini.v1 v1.39.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/cmd/writeas/logging.go b/logging.go similarity index 97% rename from cmd/writeas/logging.go rename to logging.go index 988209c..92ce475 100644 --- a/cmd/writeas/logging.go +++ b/logging.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "fmt" diff --git a/cmd/writeas/options.go b/options.go similarity index 69% rename from cmd/writeas/options.go rename to options.go index 1b0e065..42f4946 100644 --- a/cmd/writeas/options.go +++ b/options.go @@ -1,10 +1,21 @@ -package main +package writeascli import ( "github.com/cloudfoundry/jibber_jabber" cli "gopkg.in/urfave/cli.v1" ) +// Application constants. +const ( + Version = "1.99-dev" + defaultUserAgent = "writeas-cli v" + Version + // Defaults for posts on Write.as. + DefaultFont = PostFontMono + WriteasBaseURL = "https://write.as" + DevBaseURL = "https://development.write.as" + TorBaseURL = "http://writeas7pm7rcdqg.onion" +) + func userAgent(c *cli.Context) string { ua := c.String("user-agent") if ua == "" { diff --git a/cmd/writeas/posts.go b/posts.go similarity index 77% rename from cmd/writeas/posts.go rename to posts.go index 180693b..a1fc1d1 100644 --- a/cmd/writeas/posts.go +++ b/posts.go @@ -1,14 +1,18 @@ -package main +package writeascli import ( + "bufio" "fmt" + "io" "io/ioutil" + "log" "os" "path/filepath" "strings" "github.com/writeas/writeas-cli/fileutils" writeas "go.code.as/writeas.v2" + cli "gopkg.in/urfave/cli.v1" ) const ( @@ -26,11 +30,11 @@ func userDataDir() string { return filepath.Join(parentDataDir(), dataDirName) } -func dataDirExists() bool { +func DataDirExists() bool { return fileutils.Exists(userDataDir()) } -func createDataDir() { +func CreateDataDir() { err := os.Mkdir(userDataDir(), 0700) if err != nil { if debug { @@ -164,3 +168,46 @@ func WritePost(postsDir string, p *writeas.Post) error { } return ioutil.WriteFile(filepath.Join(postsDir, collDir, postFilename), []byte(txtFile), 0644) } + +func handlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { + tor := isTor(c) + if c.Int("tor-port") != 0 { + torPort = c.Int("tor-port") + } + if tor { + Info(c, "Posting to hidden service...") + } else { + Info(c, "Posting...") + } + + return DoPost(c, fullPost, c.String("font"), false, tor, c.Bool("code")) +} + +func readStdIn() []byte { + numBytes, numChunks := int64(0), int64(0) + r := bufio.NewReader(os.Stdin) + fullPost := []byte{} + buf := make([]byte, 0, 1024) + for { + n, err := r.Read(buf[:cap(buf)]) + buf = buf[:n] + if n == 0 { + if err == nil { + continue + } + if err == io.EOF { + break + } + log.Fatal(err) + } + numChunks++ + numBytes += int64(len(buf)) + + fullPost = append(fullPost, buf...) + if err != nil && err != io.EOF { + log.Fatal(err) + } + } + + return fullPost +} diff --git a/cmd/writeas/posts_nix.go b/posts_nix.go similarity index 93% rename from cmd/writeas/posts_nix.go rename to posts_nix.go index 52fbae5..9feb320 100644 --- a/cmd/writeas/posts_nix.go +++ b/posts_nix.go @@ -1,6 +1,6 @@ // +build !windows -package main +package writeascli import ( "fmt" @@ -24,7 +24,7 @@ func parentDataDir() string { } func editPostCmd(fname string) *exec.Cmd { - editor := getConfiguredEditor() + editor := GetConfiguredEditor() if editor == "" { // Fall back to default editor path, err := exec.LookPath("vim") diff --git a/cmd/writeas/posts_win.go b/posts_win.go similarity index 96% rename from cmd/writeas/posts_win.go rename to posts_win.go index c8b080a..caa737a 100644 --- a/cmd/writeas/posts_win.go +++ b/posts_win.go @@ -1,6 +1,6 @@ // +build windows -package main +package writeascli import ( "fmt" diff --git a/cmd/writeas/sync.go b/sync.go similarity index 91% rename from cmd/writeas/sync.go rename to sync.go index c14a8f5..c420625 100644 --- a/cmd/writeas/sync.go +++ b/sync.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( //"github.com/writeas/writeas-cli/sync" @@ -7,6 +7,7 @@ import ( "os" "path/filepath" + "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" cli "gopkg.in/urfave/cli.v1" ) @@ -16,8 +17,8 @@ const ( userFilename = "writeas_user" ) -func cmdPull(c *cli.Context) error { - cfg, err := loadConfig() +func CmdPull(c *cli.Context) error { + cfg, err := config.LoadConfig(userDataDir()) if err != nil { return err } @@ -78,10 +79,10 @@ func cmdPull(c *cli.Context) error { } // TODO: move UserConfig to its own package, and this to sync package -func syncSetUp(cfg *UserConfig) error { +func syncSetUp(cfg *config.UserConfig) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := loadUser() + u, err := LoadUser(userDataDir()) if err != nil { return err } @@ -117,7 +118,7 @@ func syncSetUp(cfg *UserConfig) error { // Save preference cfg.Posts.Directory = dir - err = saveConfig(cfg) + err = config.SaveConfig(userDataDir(), cfg) if err != nil { if debug { Errorln("Unable to save config: %s", err) diff --git a/cmd/writeas/tor.go b/tor.go similarity index 94% rename from cmd/writeas/tor.go rename to tor.go index d4dc585..c9d70e8 100644 --- a/cmd/writeas/tor.go +++ b/tor.go @@ -1,4 +1,4 @@ -package main +package writeascli import ( "fmt" diff --git a/user.go b/user.go new file mode 100644 index 0000000..b6f0ab1 --- /dev/null +++ b/user.go @@ -0,0 +1,47 @@ +package writeascli + +import ( + "encoding/json" + "io/ioutil" + "path/filepath" + + "github.com/writeas/writeas-cli/fileutils" + "go.code.as/writeas.v2" +) + +const UserFile = "user.json" + +func LoadUser(dataDir string) (*writeas.AuthUser, error) { + fname := filepath.Join(dataDir, UserFile) + userJSON, err := ioutil.ReadFile(fname) + if err != nil { + if !fileutils.Exists(fname) { + // Don't return a file-not-found error + return nil, nil + } + return nil, err + } + + // Parse JSON file + u := &writeas.AuthUser{} + err = json.Unmarshal(userJSON, u) + if err != nil { + return nil, err + } + return u, nil +} + +func SaveUser(dataDir string, u *writeas.AuthUser) error { + // Marshal struct into pretty-printed JSON + userJSON, err := json.MarshalIndent(u, "", " ") + if err != nil { + return err + } + + // Save file + err = ioutil.WriteFile(filepath.Join(dataDir, UserFile), userJSON, 0600) + if err != nil { + return err + } + return nil +} From f227faa5dc2e7c85b944bb18cb05d5bab0e29569 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 28 May 2019 10:44:50 -0700 Subject: [PATCH 059/181] Closes T592 T593 T597 splits code into packages this splits everything out into shared packages - user config and application config share a package for now - sync is part of the api package which includes the client, posts and tor logic - logging is it's own package - commands are in their own package --- api.go => api/api.go | 82 ++++++++++++------------ posts.go => api/posts.go | 79 +++++++++-------------- sync.go => api/sync.go | 34 +++++----- tor.go => api/tor.go | 6 +- cmd/writeas/main.go | 52 +++++++++------- commands.go => commands/commands.go | 97 ++++++++++++++--------------- config.go | 7 --- config/build.go | 7 +++ config/build_debug.go | 7 +++ config/config.go | 12 ++++ dev.go => config/dev.go | 2 +- config/directories.go | 20 ++++++ posts_nix.go => config/files_nix.go | 8 +-- posts_win.go => config/files_win.go | 8 +-- {cmd => config}/flags.go | 5 +- fonts.go => config/fonts.go | 4 +- options.go => config/options.go | 13 ++-- user.go => config/user.go | 2 +- config_debug.go | 7 --- edit.go | 16 ----- errors.go => log/errors.go | 3 +- logging.go => log/logging.go | 4 +- 22 files changed, 240 insertions(+), 235 deletions(-) rename api.go => api/api.go (67%) rename posts.go => api/posts.go (66%) rename sync.go => api/sync.go (75%) rename tor.go => api/tor.go (80%) rename commands.go => commands/commands.go (65%) delete mode 100644 config.go create mode 100644 config/build.go create mode 100644 config/build_debug.go rename dev.go => config/dev.go (81%) create mode 100644 config/directories.go rename posts_nix.go => config/files_nix.go (78%) rename posts_win.go => config/files_win.go (68%) rename {cmd => config}/flags.go (91%) rename fonts.go => config/fonts.go (93%) rename options.go => config/options.go (76%) rename user.go => config/user.go (97%) delete mode 100644 config_debug.go delete mode 100644 edit.go rename errors.go => log/errors.go (66%) rename logging.go => log/logging.go (93%) diff --git a/api.go b/api/api.go similarity index 67% rename from api.go rename to api/api.go index b3c59ef..14ac180 100644 --- a/api.go +++ b/api/api.go @@ -1,4 +1,4 @@ -package writeascli +package api import ( "fmt" @@ -6,7 +6,9 @@ import ( "github.com/atotto/clipboard" "github.com/writeas/web-core/posts" + "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" + "github.com/writeas/writeas-cli/log" writeas "go.code.as/writeas.v2" cli "gopkg.in/urfave/cli.v1" ) @@ -14,9 +16,9 @@ import ( func client(userAgent string, tor bool) *writeas.Client { var client *writeas.Client if tor { - client = writeas.NewTorClient(torPort) + client = writeas.NewTorClient(TorPort) } else { - if IsDev() { + if config.IsDev() { client = writeas.NewDevClient() } else { client = writeas.NewClient() @@ -27,20 +29,20 @@ func client(userAgent string, tor bool) *writeas.Client { return client } -func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { +func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { var client *writeas.Client - if isTor(c) { - client = writeas.NewTorClient(torPort) + if config.IsTor(c) { + client = writeas.NewTorClient(TorPort) } else { - if IsDev() { + if config.IsDev() { client = writeas.NewDevClient() } else { client = writeas.NewClient() } } - client.UserAgent = userAgent(c) + client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app - u, _ := LoadUser(userDataDir()) + u, _ := config.LoadUser(config.UserDataDir()) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -70,14 +72,14 @@ func DoFetch(friendlyID, ua string, tor bool) error { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) (*writeas.Post, error) { - cl, _ := newClient(c, false) + cl, _ := NewClient(c, false) pp := &writeas.PostParams{ - Font: getFont(code, font), - Collection: collection(c), + Font: config.GetFont(code, font), + Collection: config.Collection(c), } pp.Title, pp.Content = posts.ExtractTitle(string(post)) - if lang := language(c, true); lang != "" { + if lang := config.Language(c, true); lang != "" { pp.Language = &lang } p, err := cl.CreatePost(pp) @@ -90,11 +92,11 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( url = p.Collection.URL + p.Slug } else { if tor { - url = TorBaseURL - } else if IsDev() { - url = DevBaseURL + url = config.TorBaseURL + } else if config.IsDev() { + url = config.DevBaseURL } else { - url = WriteasBaseURL + url = config.WriteasBaseURL } url += "/" + p.ID // Output URL in requested format @@ -105,15 +107,15 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( if cl.Token() == "" { // Store post locally, since we're not authenticated - addPost(p.ID, p.Token) + AddPost(p.ID, p.Token) } // Copy URL to clipboard err = clipboard.WriteAll(string(url)) if err != nil { - Errorln("writeas: Didn't copy to clipboard: %s", err) + log.Errorln("writeas: Didn't copy to clipboard: %s", err) } else { - Info(c, "Copied to clipboard.") + log.Info(c, "Copied to clipboard.") } // Output URL @@ -124,49 +126,49 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - cl, _ := newClient(c, false) + cl, _ := NewClient(c, false) params := writeas.PostParams{} params.Title, params.Content = posts.ExtractTitle(string(post)) - if lang := language(c, false); lang != "" { + if lang := config.Language(c, false); lang != "" { params.Language = &lang } if code || font != "" { - params.Font = getFont(code, font) + params.Font = config.GetFont(code, font) } _, err := cl.UpdatePost(friendlyID, token, ¶ms) if err != nil { - if debug { - ErrorlnQuit("Problem updating: %v", err) + if config.Debug() { + log.ErrorlnQuit("Problem updating: %v", err) } return fmt.Errorf("Post doesn't exist, or bad edit token given.") } if tor { - Info(c, "Post updated via hidden service.") + log.Info(c, "Post updated via hidden service.") } else { - Info(c, "Post updated.") + log.Info(c, "Post updated.") } return nil } // DoDelete deletes the given post on Write.as. func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - cl, _ := newClient(c, false) + cl, _ := NewClient(c, false) err := cl.DeletePost(friendlyID, token) if err != nil { - if debug { - ErrorlnQuit("Problem deleting: %v", err) + if config.Debug() { + log.ErrorlnQuit("Problem deleting: %v", err) } return fmt.Errorf("Post doesn't exist, or bad edit token given.") } if tor { - Info(c, "Post deleted from hidden service.") + log.Info(c, "Post deleted from hidden service.") } else { - Info(c, "Post deleted.") + log.Info(c, "Post deleted.") } removePost(friendlyID) @@ -174,17 +176,17 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { } func DoLogIn(c *cli.Context, username, password string) error { - cl := client(userAgent(c), isTor(c)) + cl := client(config.UserAgent(c), config.IsTor(c)) u, err := cl.LogIn(username, password) if err != nil { - if debug { - ErrorlnQuit("Problem logging in: %v", err) + if config.Debug() { + log.ErrorlnQuit("Problem logging in: %v", err) } return err } - err = SaveUser(userDataDir(), u) + err = config.SaveUser(config.UserDataDir(), u) if err != nil { return err } @@ -193,21 +195,21 @@ func DoLogIn(c *cli.Context, username, password string) error { } func DoLogOut(c *cli.Context) error { - cl, err := newClient(c, true) + cl, err := NewClient(c, true) if err != nil { return err } err = cl.LogOut() if err != nil { - if debug { - ErrorlnQuit("Problem logging out: %v", err) + if config.Debug() { + log.ErrorlnQuit("Problem logging out: %v", err) } return err } // Delete local user data - err = fileutils.DeleteFile(filepath.Join(userDataDir(), UserFile)) + err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(), config.UserFile)) if err != nil { return err } diff --git a/posts.go b/api/posts.go similarity index 66% rename from posts.go rename to api/posts.go index a1fc1d1..3f005f2 100644 --- a/posts.go +++ b/api/posts.go @@ -1,16 +1,17 @@ -package writeascli +package api import ( "bufio" "fmt" "io" "io/ioutil" - "log" "os" "path/filepath" "strings" + "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" + "github.com/writeas/writeas-cli/log" writeas "go.code.as/writeas.v2" cli "gopkg.in/urfave/cli.v1" ) @@ -26,28 +27,8 @@ type Post struct { EditToken string } -func userDataDir() string { - return filepath.Join(parentDataDir(), dataDirName) -} - -func DataDirExists() bool { - return fileutils.Exists(userDataDir()) -} - -func CreateDataDir() { - err := os.Mkdir(userDataDir(), 0700) - if err != nil { - if debug { - panic(err) - } else { - Errorln("Error creating data directory: %s", err) - return - } - } -} - -func addPost(id, token string) error { - f, err := os.OpenFile(filepath.Join(userDataDir(), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) +func AddPost(id, token string) error { + f, err := os.OpenFile(filepath.Join(config.UserDataDir(), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("Error creating local posts list: %s", err) } @@ -62,8 +43,8 @@ func addPost(id, token string) error { return nil } -func tokenFromID(id string) string { - post := fileutils.FindLine(filepath.Join(userDataDir(), postsFile), id) +func TokenFromID(id string) string { + post := fileutils.FindLine(filepath.Join(config.UserDataDir(), postsFile), id) if post == "" { return "" } @@ -77,11 +58,11 @@ func tokenFromID(id string) string { } func removePost(id string) { - fileutils.RemoveLine(filepath.Join(userDataDir(), postsFile), id) + fileutils.RemoveLine(filepath.Join(config.UserDataDir(), postsFile), id) } -func getPosts() *[]Post { - lines := fileutils.ReadData(filepath.Join(userDataDir(), postsFile)) +func GetPosts() *[]Post { + lines := fileutils.ReadData(filepath.Join(config.UserDataDir(), postsFile)) posts := []Post{} @@ -99,33 +80,33 @@ func getPosts() *[]Post { return &posts } -func composeNewPost() (string, *[]byte) { +func ComposeNewPost() (string, *[]byte) { f, err := fileutils.TempFile(os.TempDir(), "WApost", "txt") if err != nil { - if debug { + if config.Debug() { panic(err) } else { - Errorln("Error creating temp file: %s", err) + log.Errorln("Error creating temp file: %s", err) return "", nil } } f.Close() - cmd := editPostCmd(f.Name()) + cmd := config.EditPostCmd(f.Name()) if cmd == nil { os.Remove(f.Name()) - fmt.Println(noEditorErr) + fmt.Println(config.NoEditorErr) return "", nil } cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr if err := cmd.Start(); err != nil { os.Remove(f.Name()) - if debug { + if config.Debug() { panic(err) } else { - Errorln("Error starting editor: %s", err) + log.Errorln("Error starting editor: %s", err) return "", nil } } @@ -133,20 +114,20 @@ func composeNewPost() (string, *[]byte) { // If something fails past this point, the temporary post file won't be // removed automatically. Calling function should handle this. if err := cmd.Wait(); err != nil { - if debug { + if config.Debug() { panic(err) } else { - Errorln("Editor finished with error: %s", err) + log.Errorln("Editor finished with error: %s", err) return "", nil } } post, err := ioutil.ReadFile(f.Name()) if err != nil { - if debug { + if config.Debug() { panic(err) } else { - Errorln("Error reading post: %s", err) + log.Errorln("Error reading post: %s", err) return "", nil } } @@ -160,7 +141,7 @@ func WritePost(postsDir string, p *writeas.Post) error { postFilename = p.Slug collDir = p.Collection.Alias } - postFilename += postFileExt + postFilename += PostFileExt txtFile := p.Content if p.Title != "" { @@ -169,21 +150,21 @@ func WritePost(postsDir string, p *writeas.Post) error { return ioutil.WriteFile(filepath.Join(postsDir, collDir, postFilename), []byte(txtFile), 0644) } -func handlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { - tor := isTor(c) +func HandlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { + tor := config.IsTor(c) if c.Int("tor-port") != 0 { - torPort = c.Int("tor-port") + TorPort = c.Int("tor-port") } if tor { - Info(c, "Posting to hidden service...") + log.Info(c, "Posting to hidden service...") } else { - Info(c, "Posting...") + log.Info(c, "Posting...") } return DoPost(c, fullPost, c.String("font"), false, tor, c.Bool("code")) } -func readStdIn() []byte { +func ReadStdIn() []byte { numBytes, numChunks := int64(0), int64(0) r := bufio.NewReader(os.Stdin) fullPost := []byte{} @@ -198,14 +179,14 @@ func readStdIn() []byte { if err == io.EOF { break } - log.Fatal(err) + log.ErrorlnQuit("Error reading from stdin: %v", err) } numChunks++ numBytes += int64(len(buf)) fullPost = append(fullPost, buf...) if err != nil && err != io.EOF { - log.Fatal(err) + log.ErrorlnQuit("Error appending to end of post: %v", err) } } diff --git a/sync.go b/api/sync.go similarity index 75% rename from sync.go rename to api/sync.go index c420625..22c1a01 100644 --- a/sync.go +++ b/api/sync.go @@ -1,4 +1,4 @@ -package writeascli +package api import ( //"github.com/writeas/writeas-cli/sync" @@ -9,16 +9,17 @@ import ( "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" + "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) const ( - postFileExt = ".txt" + PostFileExt = ".txt" userFilename = "writeas_user" ) func CmdPull(c *cli.Context) error { - cfg, err := config.LoadConfig(userDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir()) if err != nil { return err } @@ -28,7 +29,7 @@ func CmdPull(c *cli.Context) error { } // Fetch posts - cl, err := newClient(c, true) + cl, err := NewClient(c, true) if err != nil { return err } @@ -46,15 +47,15 @@ func CmdPull(c *cli.Context) error { // Create directory for collection collDir = p.Collection.Alias if !fileutils.Exists(filepath.Join(cfg.Posts.Directory, collDir)) { - Info(c, "Creating folder "+collDir) + log.Info(c, "Creating folder "+collDir) err = os.Mkdir(filepath.Join(cfg.Posts.Directory, collDir), 0755) if err != nil { - Errorln("Error creating blog directory %s: %s. Skipping post %s.", collDir, err, postFilename) + log.Errorln("Error creating blog directory %s: %s. Skipping post %s.", collDir, err, postFilename) continue } } } - postFilename += postFileExt + postFilename += PostFileExt // Write file txtFile := p.Content @@ -63,26 +64,25 @@ func CmdPull(c *cli.Context) error { } err = ioutil.WriteFile(filepath.Join(cfg.Posts.Directory, collDir, postFilename), []byte(txtFile), 0644) if err != nil { - Errorln("Error creating file %s: %s", postFilename, err) + log.Errorln("Error creating file %s: %s", postFilename, err) } - Info(c, "Saved post "+postFilename) + log.Info(c, "Saved post "+postFilename) // Update mtime and atime on files modTime := p.Updated.Local() err = os.Chtimes(filepath.Join(cfg.Posts.Directory, collDir, postFilename), modTime, modTime) if err != nil { - Errorln("Error setting time on %s: %s", postFilename, err) + log.Errorln("Error setting time on %s: %s", postFilename, err) } } return nil } -// TODO: move UserConfig to its own package, and this to sync package func syncSetUp(cfg *config.UserConfig) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := LoadUser(userDataDir()) + u, err := config.LoadUser(config.UserDataDir()) if err != nil { return err } @@ -106,8 +106,8 @@ func syncSetUp(cfg *config.UserConfig) error { if !fileutils.Exists(dir) { err = os.MkdirAll(dir, 0700) if err != nil { - if debug { - Errorln("Error creating data directory: %s", err) + if config.Debug() { + log.Errorln("Error creating data directory: %s", err) } return err } @@ -118,10 +118,10 @@ func syncSetUp(cfg *config.UserConfig) error { // Save preference cfg.Posts.Directory = dir - err = config.SaveConfig(userDataDir(), cfg) + err = config.SaveConfig(config.UserDataDir(), cfg) if err != nil { - if debug { - Errorln("Unable to save config: %s", err) + if config.Debug() { + log.Errorln("Unable to save config: %s", err) } return err } diff --git a/tor.go b/api/tor.go similarity index 80% rename from tor.go rename to api/tor.go index c9d70e8..dae5be5 100644 --- a/tor.go +++ b/api/tor.go @@ -1,4 +1,4 @@ -package writeascli +package api import ( "fmt" @@ -8,11 +8,11 @@ import ( ) var ( - torPort = 9150 + TorPort = 9150 ) func torClient() *http.Client { - dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", torPort)) + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", TorPort)) transport := &http.Transport{Dial: dialSocksProxy} return &http.Client{Transport: transport} } diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 82ab356..cff6657 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -3,8 +3,10 @@ package main import ( "os" - writeascli "github.com/writeas/writeas-cli" - cmd "github.com/writeas/writeas-cli/cmd" + "github.com/writeas/writeas-cli/api" + "github.com/writeas/writeas-cli/commands" + "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -19,7 +21,7 @@ func main() { // Run the app app := cli.NewApp() app.Name = "writeas" - app.Version = writeascli.Version + app.Version = config.Version app.Usage = "Publish text quickly" app.Authors = []cli.Author{ { @@ -27,14 +29,14 @@ func main() { Email: "hello@write.as", }, } - app.Action = writeascli.CmdPost - app.Flags = cmd.PostFlags + app.Action = commands.CmdPost + app.Flags = config.PostFlags app.Commands = []cli.Command{ { Name: "post", Usage: "Alias for default action: create post from stdin", - Action: writeascli.CmdPost, - Flags: cmd.PostFlags, + Action: commands.CmdPost, + Flags: config.PostFlags, Description: `Create a new post on Write.as from stdin. Use the --code flag to indicate that the post should use syntax @@ -60,19 +62,19 @@ func main() { If posting fails for any reason, 'writeas' will show you the temporary file location and how to pipe it to 'writeas' to retry.`, - Action: writeascli.CmdNew, - Flags: cmd.PostFlags, + Action: commands.CmdNew, + Flags: config.PostFlags, }, { Name: "publish", Usage: "Publish a file to Write.as", - Action: writeascli.CmdPublish, - Flags: cmd.PostFlags, + Action: commands.CmdPublish, + Flags: config.PostFlags, }, { Name: "delete", Usage: "Delete a post", - Action: writeascli.CmdDelete, + Action: commands.CmdDelete, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -92,7 +94,7 @@ func main() { { Name: "update", Usage: "Update (overwrite) a post", - Action: writeascli.CmdUpdate, + Action: commands.CmdUpdate, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -120,7 +122,7 @@ func main() { { Name: "get", Usage: "Read a raw post", - Action: writeascli.CmdGet, + Action: commands.CmdGet, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -145,12 +147,12 @@ func main() { This requires a post ID (from https://write.as/[ID]) and an Edit Token (exported from another Write.as client, such as the Android app). `, - Action: writeascli.CmdAdd, + Action: commands.CmdAdd, }, { Name: "list", Usage: "List local posts", - Action: writeascli.CmdList, + Action: commands.CmdList, Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", @@ -169,7 +171,7 @@ func main() { { Name: "fetch", Usage: "Fetch authenticated user's Write.as posts", - Action: writeascli.CmdPull, + Action: api.CmdPull, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -189,7 +191,7 @@ func main() { { Name: "auth", Usage: "Authenticate with Write.as", - Action: writeascli.CmdAuth, + Action: commands.CmdAuth, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -209,7 +211,7 @@ func main() { { Name: "logout", Usage: "Log out of Write.as", - Action: writeascli.CmdLogOut, + Action: commands.CmdLogOut, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -247,7 +249,15 @@ OPTIONS: func initialize() { // Ensure we have a data directory to use - if !writeascli.DataDirExists() { - writeascli.CreateDataDir() + if !config.DataDirExists() { + err := config.CreateDataDir() + if err != nil { + if config.Debug() { + panic(err) + } else { + log.Errorln("Error creating data directory: %s", err) + return + } + } } } diff --git a/commands.go b/commands/commands.go similarity index 65% rename from commands.go rename to commands/commands.go index d451654..fcdec40 100644 --- a/commands.go +++ b/commands/commands.go @@ -1,4 +1,4 @@ -package writeascli +package commands import ( "fmt" @@ -7,18 +7,20 @@ import ( "path/filepath" "github.com/howeyc/gopass" + "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" + "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) func CmdPost(c *cli.Context) error { - _, err := handlePost(readStdIn(), c) + _, err := api.HandlePost(api.ReadStdIn(), c) return err } func CmdNew(c *cli.Context) error { - fname, p := composeNewPost() + fname, p := api.ComposeNewPost() if p == nil { // Assume composeNewPost already told us what the error was. Abort now. os.Exit(1) @@ -31,13 +33,12 @@ func CmdNew(c *cli.Context) error { os.Remove(fname) } - InfolnQuit("Empty post. Bye!") + log.InfolnQuit("Empty post. Bye!") } - _, err := handlePost(*p, c) + _, err := api.HandlePost(*p, c) if err != nil { - Errorln("Error posting: %s", err) - Errorln(messageRetryCompose(fname)) + log.Errorln("Error posting: %s\n%s", err, config.MessageRetryCompose(fname)) return cli.NewExitError("", 1) } @@ -58,15 +59,15 @@ func CmdPublish(c *cli.Context) error { if err != nil { return err } - p, err := handlePost(content, c) + p, err := api.HandlePost(content, c) if err != nil { return err } // Save post to posts folder - cfg, err := config.LoadConfig(userDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir()) if cfg.Posts.Directory != "" { - err = WritePost(cfg.Posts.Directory, p) + err = api.WritePost(cfg.Posts.Directory, p) if err != nil { return err } @@ -81,38 +82,36 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := LoadUser(userDataDir()) + u, _ := config.LoadUser(config.UserDataDir()) if token == "" { // Search for the token locally - token = tokenFromID(friendlyID) + token = api.TokenFromID(friendlyID) if token == "" && u == nil { - Errorln("Couldn't find an edit token locally. Did you create this post here?") - ErrorlnQuit("If you have an edit token, use: writeas delete %s ", friendlyID) + log.Errorln("Couldn't find an edit token locally. Did you create this post here?") + log.ErrorlnQuit("If you have an edit token, use: writeas delete %s ", friendlyID) } } - tor := isTor(c) + tor := config.IsTor(c) if c.Int("tor-port") != 0 { - torPort = c.Int("tor-port") + api.TorPort = c.Int("tor-port") } if tor { - Info(c, "Deleting via hidden service...") - } else if isDev() { - Info(c, "Deleting via dev environment...") + log.Info(c, "Deleting via hidden service...") } else { - Info(c, "Deleting...") + log.Info(c, "Deleting...") } - err := DoDelete(c, friendlyID, token, tor) + err := api.DoDelete(c, friendlyID, token, tor) if err != nil { return err } // Delete local file, if necessary - cfg, err := config.LoadConfig(userDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir()) if cfg.Posts.Directory != "" { // TODO: handle deleting blog posts - err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+postFileExt)) + err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+api.PostFileExt)) if err != nil { return err } @@ -128,32 +127,30 @@ func CmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := LoadUser(userDataDir()) + u, _ := config.LoadUser(config.UserDataDir()) if token == "" { // Search for the token locally - token = tokenFromID(friendlyID) + token = api.TokenFromID(friendlyID) if token == "" && u == nil { - Errorln("Couldn't find an edit token locally. Did you create this post here?") - ErrorlnQuit("If you have an edit token, use: writeas update %s ", friendlyID) + log.Errorln("Couldn't find an edit token locally. Did you create this post here?") + log.ErrorlnQuit("If you have an edit token, use: writeas update %s ", friendlyID) } } // Read post body - fullPost := readStdIn() + fullPost := api.ReadStdIn() - tor := isTor(c) + tor := config.IsTor(c) if c.Int("tor-port") != 0 { - torPort = c.Int("tor-port") + api.TorPort = c.Int("tor-port") } if tor { - Info(c, "Updating via hidden service...") - } else if isDev() { - Info(c, "Updating via dev environment...") + log.Info(c, "Updating via hidden service...") } else { - Info(c, "Updating...") + log.Info(c, "Updating...") } - return DoUpdate(c, fullPost, friendlyID, token, c.String("font"), tor, c.Bool("code")) + return api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), tor, c.Bool("code")) } func CmdGet(c *cli.Context) error { @@ -162,19 +159,17 @@ func CmdGet(c *cli.Context) error { return cli.NewExitError("usage: writeas get ", 1) } - tor := isTor(c) + tor := config.IsTor(c) if c.Int("tor-port") != 0 { - torPort = c.Int("tor-port") + api.TorPort = c.Int("tor-port") } if tor { - Info(c, "Getting via hidden service...") - } else if isDev() { - Info(c, "Getting via dev environment...") + log.Info(c, "Getting via hidden service...") } else { - Info(c, "Getting...") + log.Info(c, "Getting...") } - return DoFetch(friendlyID, userAgent(c), tor) + return api.DoFetch(friendlyID, config.UserAgent(c), tor) } func CmdAdd(c *cli.Context) error { @@ -184,7 +179,7 @@ func CmdAdd(c *cli.Context) error { return cli.NewExitError("usage: writeas add ", 1) } - err := addPost(friendlyID, token) + err := api.AddPost(friendlyID, token) return err } @@ -192,17 +187,17 @@ func CmdList(c *cli.Context) error { urls := c.Bool("url") ids := c.Bool("id") - var p Post - posts := getPosts() + var p api.Post + posts := api.GetPosts() for i := range *posts { p = (*posts)[len(*posts)-1-i] if ids || !urls { fmt.Printf("%s ", p.ID) } if urls { - base := WriteasBaseURL - if IsDev() { - base = DevBaseURL + base := config.WriteasBaseURL + if config.IsDev() { + base = config.DevBaseURL } ext := "" // Output URL in requested format @@ -218,7 +213,7 @@ func CmdList(c *cli.Context) error { func CmdAuth(c *cli.Context) error { // Check configuration - u, err := LoadUser(userDataDir()) + u, err := config.LoadUser(config.UserDataDir()) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } @@ -242,7 +237,7 @@ func CmdAuth(c *cli.Context) error { if len(pass) == 0 { return cli.NewExitError("Please enter your password.", 1) } - err = DoLogIn(c, username, string(pass)) + err = api.DoLogIn(c, username, string(pass)) if err != nil { return cli.NewExitError(fmt.Sprintf("error logging in: %v", err), 1) } @@ -251,5 +246,5 @@ func CmdAuth(c *cli.Context) error { } func CmdLogOut(c *cli.Context) error { - return DoLogOut(c) + return api.DoLogOut(c) } diff --git a/config.go b/config.go deleted file mode 100644 index 36bc3c6..0000000 --- a/config.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build !debug - -package writeascli - -const ( - debug = false -) diff --git a/config/build.go b/config/build.go new file mode 100644 index 0000000..1ca1c10 --- /dev/null +++ b/config/build.go @@ -0,0 +1,7 @@ +// +build !debug + +package config + +func Debug() bool { + return false +} diff --git a/config/build_debug.go b/config/build_debug.go new file mode 100644 index 0000000..b08138d --- /dev/null +++ b/config/build_debug.go @@ -0,0 +1,7 @@ +// +build debug + +package config + +func Debug() bool { + return true +} diff --git a/config/config.go b/config/config.go index 8f60729..4c9c59d 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "os" "path/filepath" ini "gopkg.in/ini.v1" @@ -49,3 +50,14 @@ func SaveConfig(dataDir string, uc *UserConfig) error { return cfg.SaveTo(filepath.Join(dataDir, UserConfigFile)) } + +var editors = []string{"WRITEAS_EDITOR", "EDITOR"} + +func GetConfiguredEditor() string { + for _, v := range editors { + if e := os.Getenv(v); e != "" { + return e + } + } + return "" +} diff --git a/dev.go b/config/dev.go similarity index 81% rename from dev.go rename to config/dev.go index d029c3d..41dbf5a 100644 --- a/dev.go +++ b/config/dev.go @@ -1,4 +1,4 @@ -package writeascli +package config import ( "os" diff --git a/config/directories.go b/config/directories.go new file mode 100644 index 0000000..9031489 --- /dev/null +++ b/config/directories.go @@ -0,0 +1,20 @@ +package config + +import ( + "os" + "path/filepath" + + "github.com/writeas/writeas-cli/fileutils" +) + +func UserDataDir() string { + return filepath.Join(parentDataDir(), dataDirName) +} + +func DataDirExists() bool { + return fileutils.Exists(UserDataDir()) +} + +func CreateDataDir() error { + return os.Mkdir(UserDataDir(), 0700) +} diff --git a/posts_nix.go b/config/files_nix.go similarity index 78% rename from posts_nix.go rename to config/files_nix.go index 9feb320..8c669d3 100644 --- a/posts_nix.go +++ b/config/files_nix.go @@ -1,6 +1,6 @@ // +build !windows -package writeascli +package config import ( "fmt" @@ -11,7 +11,7 @@ import ( const ( dataDirName = ".writeas" - noEditorErr = "Couldn't find default editor. Try setting $EDITOR environment variable in ~/.profile" + NoEditorErr = "Couldn't find default editor. Try setting $EDITOR environment variable in ~/.profile" ) func parentDataDir() string { @@ -23,7 +23,7 @@ func parentDataDir() string { return dir } -func editPostCmd(fname string) *exec.Cmd { +func EditPostCmd(fname string) *exec.Cmd { editor := GetConfiguredEditor() if editor == "" { // Fall back to default editor @@ -39,6 +39,6 @@ func editPostCmd(fname string) *exec.Cmd { return exec.Command(editor, fname) } -func messageRetryCompose(fname string) string { +func MessageRetryCompose(fname string) string { return fmt.Sprintf("To retry this post, run:\n cat %s | writeas", fname) } diff --git a/posts_win.go b/config/files_win.go similarity index 68% rename from posts_win.go rename to config/files_win.go index caa737a..cf478f1 100644 --- a/posts_win.go +++ b/config/files_win.go @@ -1,6 +1,6 @@ // +build windows -package writeascli +package config import ( "fmt" @@ -10,18 +10,18 @@ import ( const ( dataDirName = "Write.as" - noEditorErr = "Error getting default editor. You shouldn't see this, so let us know you did: hello@write.as" + NoEditorErr = "Error getting default editor. You shouldn't see this, so let us know you did: hello@write.as" ) func parentDataDir() string { return os.Getenv("APPDATA") } -func editPostCmd(fname string) *exec.Cmd { +func EditPostCmd(fname string) *exec.Cmd { // NOTE this won't work if fname contains spaces. return exec.Command("cmd", "/C copy con "+fname) } -func messageRetryCompose(fname string) string { +func MessageRetryCompose(fname string) string { return fmt.Sprintf("To retry this post, run:\n type %s | writeas.exe", fname) } diff --git a/cmd/flags.go b/config/flags.go similarity index 91% rename from cmd/flags.go rename to config/flags.go index 062c4be..2c17e23 100644 --- a/cmd/flags.go +++ b/config/flags.go @@ -1,7 +1,6 @@ -package cmd +package config import ( - writeascli "github.com/writeas/writeas-cli" "gopkg.in/urfave/cli.v1" ) @@ -36,7 +35,7 @@ var PostFlags = []cli.Flag{ cli.StringFlag{ Name: "font", Usage: "Sets post font to given value", - Value: writeascli.DefaultFont, + Value: DefaultFont, }, cli.StringFlag{ Name: "lang", diff --git a/fonts.go b/config/fonts.go similarity index 93% rename from fonts.go rename to config/fonts.go index 4910a7c..22fc56b 100644 --- a/fonts.go +++ b/config/fonts.go @@ -1,4 +1,4 @@ -package writeascli +package config import ( "fmt" @@ -28,7 +28,7 @@ var postFontMap = map[string]postFont{ "code": PostFontCode, } -func getFont(code bool, font string) string { +func GetFont(code bool, font string) string { if code { if font != "" && font != DefaultFont { fmt.Printf("A non-default font '%s' and --code flag given. 'code' type takes precedence.\n", font) diff --git a/options.go b/config/options.go similarity index 76% rename from options.go rename to config/options.go index 42f4946..9b717bd 100644 --- a/options.go +++ b/config/options.go @@ -1,7 +1,8 @@ -package writeascli +package config import ( "github.com/cloudfoundry/jibber_jabber" + "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -16,7 +17,7 @@ const ( TorBaseURL = "http://writeas7pm7rcdqg.onion" ) -func userAgent(c *cli.Context) string { +func UserAgent(c *cli.Context) string { ua := c.String("user-agent") if ua == "" { return defaultUserAgent @@ -24,11 +25,11 @@ func userAgent(c *cli.Context) string { return ua + " (" + defaultUserAgent + ")" } -func isTor(c *cli.Context) bool { +func IsTor(c *cli.Context) bool { return c.Bool("tor") || c.Bool("t") } -func language(c *cli.Context, auto bool) string { +func Language(c *cli.Context, auto bool) string { if l := c.String("lang"); l != "" { return l } @@ -38,13 +39,13 @@ func language(c *cli.Context, auto bool) string { // Automatically detect language l, err := jibber_jabber.DetectLanguage() if err != nil { - Info(c, "Language detection failed: %s", err) + log.Info(c, "Language detection failed: %s", err) return "" } return l } -func collection(c *cli.Context) string { +func Collection(c *cli.Context) string { if coll := c.String("c"); coll != "" { return coll } diff --git a/user.go b/config/user.go similarity index 97% rename from user.go rename to config/user.go index b6f0ab1..d7039f1 100644 --- a/user.go +++ b/config/user.go @@ -1,4 +1,4 @@ -package writeascli +package config import ( "encoding/json" diff --git a/config_debug.go b/config_debug.go deleted file mode 100644 index eaa975a..0000000 --- a/config_debug.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build debug - -package writeascli - -const ( - debug = true -) diff --git a/edit.go b/edit.go deleted file mode 100644 index 349189f..0000000 --- a/edit.go +++ /dev/null @@ -1,16 +0,0 @@ -package writeascli - -import ( - "os" -) - -var editors = []string{"WRITEAS_EDITOR", "EDITOR"} - -func GetConfiguredEditor() string { - for _, v := range editors { - if e := os.Getenv(v); e != "" { - return e - } - } - return "" -} diff --git a/errors.go b/log/errors.go similarity index 66% rename from errors.go rename to log/errors.go index a48a660..8ce959f 100644 --- a/errors.go +++ b/log/errors.go @@ -1,9 +1,10 @@ -package writeascli +package log import ( "errors" ) +// TODO: this is never used? var ( ErrPostNotFound = errors.New("Post not found.") ) diff --git a/logging.go b/log/logging.go similarity index 93% rename from logging.go rename to log/logging.go index 92ce475..a514a7c 100644 --- a/logging.go +++ b/log/logging.go @@ -1,4 +1,4 @@ -package writeascli +package log import ( "fmt" @@ -21,7 +21,7 @@ func InfolnQuit(s string, p ...interface{}) { os.Exit(0) } -// Error +// Errorln logs the message to stderr func Errorln(s string, p ...interface{}) { fmt.Fprintf(os.Stderr, s+"\n", p...) } From a993f008462690e2ef2c3ed46831b2f525f0d6bf Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 28 May 2019 17:05:36 -0700 Subject: [PATCH 060/181] ref T594 allow configurable config directory this adds a configurable directory, currently within the users path for the configuration, user and post data to be saved. it is accessible down the stack of the application via the cli.Context, specificaly c.App.ExtraData which is a function that returns a map of [string]string under the key 'configDir". --- api/api.go | 12 ++++++------ api/posts.go | 16 ++++++++-------- api/sync.go | 10 +++++----- cmd/writeas/config_nix.go | 7 +++++++ cmd/writeas/config_win.go | 7 +++++++ cmd/writeas/main.go | 13 +++++++------ commands/commands.go | 18 +++++++++--------- config/directories.go | 10 +++++----- config/files_nix.go | 1 - config/files_win.go | 1 - 10 files changed, 54 insertions(+), 41 deletions(-) create mode 100644 cmd/writeas/config_nix.go create mode 100644 cmd/writeas/config_win.go diff --git a/api/api.go b/api/api.go index 14ac180..c6c9fee 100644 --- a/api/api.go +++ b/api/api.go @@ -42,7 +42,7 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { } client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app - u, _ := config.LoadUser(config.UserDataDir()) + u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -107,7 +107,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( if cl.Token() == "" { // Store post locally, since we're not authenticated - AddPost(p.ID, p.Token) + AddPost(c, p.ID, p.Token) } // Copy URL to clipboard @@ -170,7 +170,7 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { } else { log.Info(c, "Post deleted.") } - removePost(friendlyID) + removePost(c.App.ExtraInfo()["configDir"], friendlyID) return nil } @@ -186,11 +186,11 @@ func DoLogIn(c *cli.Context, username, password string) error { return err } - err = config.SaveUser(config.UserDataDir(), u) + err = config.SaveUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]), u) if err != nil { return err } - fmt.Printf("Logged in as %s.\n", u.User.Username) + log.Info(c, "Logged in as %s.\n", u.User.Username) return nil } @@ -209,7 +209,7 @@ func DoLogOut(c *cli.Context) error { } // Delete local user data - err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(), config.UserFile)) + err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), config.UserFile)) if err != nil { return err } diff --git a/api/posts.go b/api/posts.go index 3f005f2..68d0ba1 100644 --- a/api/posts.go +++ b/api/posts.go @@ -27,8 +27,8 @@ type Post struct { EditToken string } -func AddPost(id, token string) error { - f, err := os.OpenFile(filepath.Join(config.UserDataDir(), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) +func AddPost(c *cli.Context, id, token string) error { + f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("Error creating local posts list: %s", err) } @@ -43,8 +43,8 @@ func AddPost(id, token string) error { return nil } -func TokenFromID(id string) string { - post := fileutils.FindLine(filepath.Join(config.UserDataDir(), postsFile), id) +func TokenFromID(c *cli.Context, id string) string { + post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id) if post == "" { return "" } @@ -57,12 +57,12 @@ func TokenFromID(id string) string { return parts[1] } -func removePost(id string) { - fileutils.RemoveLine(filepath.Join(config.UserDataDir(), postsFile), id) +func removePost(path, id string) { + fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id) } -func GetPosts() *[]Post { - lines := fileutils.ReadData(filepath.Join(config.UserDataDir(), postsFile)) +func GetPosts(c *cli.Context) *[]Post { + lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile)) posts := []Post{} diff --git a/api/sync.go b/api/sync.go index 22c1a01..d687f24 100644 --- a/api/sync.go +++ b/api/sync.go @@ -19,13 +19,13 @@ const ( ) func CmdPull(c *cli.Context) error { - cfg, err := config.LoadConfig(config.UserDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if err != nil { return err } // Create posts directory if needed if cfg.Posts.Directory == "" { - syncSetUp(cfg) + syncSetUp(c.App.ExtraInfo()["configDir"], cfg) } // Fetch posts @@ -79,10 +79,10 @@ func CmdPull(c *cli.Context) error { return nil } -func syncSetUp(cfg *config.UserConfig) error { +func syncSetUp(path string, cfg *config.UserConfig) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := config.LoadUser(config.UserDataDir()) + u, err := config.LoadUser(config.UserDataDir(path)) if err != nil { return err } @@ -118,7 +118,7 @@ func syncSetUp(cfg *config.UserConfig) error { // Save preference cfg.Posts.Directory = dir - err = config.SaveConfig(config.UserDataDir(), cfg) + err = config.SaveConfig(config.UserDataDir(path), cfg) if err != nil { if config.Debug() { log.Errorln("Unable to save config: %s", err) diff --git a/cmd/writeas/config_nix.go b/cmd/writeas/config_nix.go new file mode 100644 index 0000000..6b3cb86 --- /dev/null +++ b/cmd/writeas/config_nix.go @@ -0,0 +1,7 @@ +// +build !windows + +package main + +var appInfo = map[string]string{ + "configDir": ".writeas", +} diff --git a/cmd/writeas/config_win.go b/cmd/writeas/config_win.go new file mode 100644 index 0000000..9a7eea1 --- /dev/null +++ b/cmd/writeas/config_win.go @@ -0,0 +1,7 @@ +// +build windows + +package main + +var appInfo = map[string]string{ + "configDir": "Write.as", +} diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index cff6657..838e4de 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -11,8 +11,7 @@ import ( ) func main() { - initialize() - + initialize(appInfo["configDir"]) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", Usage: "print the version", @@ -29,6 +28,9 @@ func main() { Email: "hello@write.as", }, } + app.ExtraInfo = func() map[string]string { + return appInfo + } app.Action = commands.CmdPost app.Flags = config.PostFlags app.Commands = []cli.Command{ @@ -243,14 +245,13 @@ OPTIONS: {{range .Flags}}{{.}} {{end}}{{ end }} ` - app.Run(os.Args) } -func initialize() { +func initialize(dataDirName string) { // Ensure we have a data directory to use - if !config.DataDirExists() { - err := config.CreateDataDir() + if !config.DataDirExists(dataDirName) { + err := config.CreateDataDir(dataDirName) if err != nil { if config.Debug() { panic(err) diff --git a/commands/commands.go b/commands/commands.go index fcdec40..dede994 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -65,7 +65,7 @@ func CmdPublish(c *cli.Context) error { } // Save post to posts folder - cfg, err := config.LoadConfig(config.UserDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if cfg.Posts.Directory != "" { err = api.WritePost(cfg.Posts.Directory, p) if err != nil { @@ -82,10 +82,10 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := config.LoadUser(config.UserDataDir()) + u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if token == "" { // Search for the token locally - token = api.TokenFromID(friendlyID) + token = api.TokenFromID(c, friendlyID) if token == "" && u == nil { log.Errorln("Couldn't find an edit token locally. Did you create this post here?") log.ErrorlnQuit("If you have an edit token, use: writeas delete %s ", friendlyID) @@ -108,7 +108,7 @@ func CmdDelete(c *cli.Context) error { } // Delete local file, if necessary - cfg, err := config.LoadConfig(config.UserDataDir()) + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if cfg.Posts.Directory != "" { // TODO: handle deleting blog posts err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+api.PostFileExt)) @@ -127,10 +127,10 @@ func CmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := config.LoadUser(config.UserDataDir()) + u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if token == "" { // Search for the token locally - token = api.TokenFromID(friendlyID) + token = api.TokenFromID(c, friendlyID) if token == "" && u == nil { log.Errorln("Couldn't find an edit token locally. Did you create this post here?") log.ErrorlnQuit("If you have an edit token, use: writeas update %s ", friendlyID) @@ -179,7 +179,7 @@ func CmdAdd(c *cli.Context) error { return cli.NewExitError("usage: writeas add ", 1) } - err := api.AddPost(friendlyID, token) + err := api.AddPost(c, friendlyID, token) return err } @@ -188,7 +188,7 @@ func CmdList(c *cli.Context) error { ids := c.Bool("id") var p api.Post - posts := api.GetPosts() + posts := api.GetPosts(c) for i := range *posts { p = (*posts)[len(*posts)-1-i] if ids || !urls { @@ -213,7 +213,7 @@ func CmdList(c *cli.Context) error { func CmdAuth(c *cli.Context) error { // Check configuration - u, err := config.LoadUser(config.UserDataDir()) + u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } diff --git a/config/directories.go b/config/directories.go index 9031489..8373835 100644 --- a/config/directories.go +++ b/config/directories.go @@ -7,14 +7,14 @@ import ( "github.com/writeas/writeas-cli/fileutils" ) -func UserDataDir() string { +func UserDataDir(dataDirName string) string { return filepath.Join(parentDataDir(), dataDirName) } -func DataDirExists() bool { - return fileutils.Exists(UserDataDir()) +func DataDirExists(dataDirName string) bool { + return fileutils.Exists(UserDataDir(dataDirName)) } -func CreateDataDir() error { - return os.Mkdir(UserDataDir(), 0700) +func CreateDataDir(dataDirName string) error { + return os.Mkdir(UserDataDir(dataDirName), 0700) } diff --git a/config/files_nix.go b/config/files_nix.go index 8c669d3..0c10f04 100644 --- a/config/files_nix.go +++ b/config/files_nix.go @@ -10,7 +10,6 @@ import ( ) const ( - dataDirName = ".writeas" NoEditorErr = "Couldn't find default editor. Try setting $EDITOR environment variable in ~/.profile" ) diff --git a/config/files_win.go b/config/files_win.go index cf478f1..026b803 100644 --- a/config/files_win.go +++ b/config/files_win.go @@ -9,7 +9,6 @@ import ( ) const ( - dataDirName = "Write.as" NoEditorErr = "Error getting default editor. You shouldn't see this, so let us know you did: hello@write.as" ) From bf389561d6286c46d96d6aa415bb9cffe39efde3 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 29 May 2019 13:55:44 -0700 Subject: [PATCH 061/181] rename cmd writeas list >> writeas posts --- cmd/writeas/main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 838e4de..15c12de 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -152,9 +152,10 @@ func main() { Action: commands.CmdAdd, }, { - Name: "list", - Usage: "List local posts", - Action: commands.CmdList, + Name: "posts", + Usage: "List posts", + Description: `List all local posts.`, + Action: commands.CmdList, Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", From 1fccf7ad62ca716198f8a91519e0dee731f6403e Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 30 May 2019 13:37:07 -0700 Subject: [PATCH 062/181] Resolves T604 Fetch users posts if authenticated This changes the 'posts' subcommand, previously 'list', to also list any remote posts for the user if currently authenticated. - adds DoFetchPosts to the api pkg which returns all the logged in users posts. - adds a RemotePost type to the api pkg which stores some of the return information from DoFetchPosts for use in printing to stdout - adds GetUserPosts to the api pkg which converts the incoming writeas.Posts into the smaller RemotePost type - adds getExcerpt which takes the incoming writeas.Post.Content and returns a trimmed down excerpt of the content. Max 2 lines of 80 chars and delimited by '...' - changes CmdList from pkg commands to CmdListPosts which now writes out any local and remote posts in simple formatted tables --- api/api.go | 16 ++++++++++++ api/posts.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ cmd/writeas/main.go | 10 ++++--- commands/commands.go | 59 +++++++++++++++++++++++++++++++---------- go.mod | 1 + go.sum | 2 ++ 6 files changed, 133 insertions(+), 17 deletions(-) diff --git a/api/api.go b/api/api.go index c6c9fee..e6acbd1 100644 --- a/api/api.go +++ b/api/api.go @@ -69,6 +69,22 @@ func DoFetch(friendlyID, ua string, tor bool) error { return nil } +// DoFetchPosts retrieves all remote posts for the +// authenticated user +func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { + cl, err := NewClient(c, true) + if err != nil { + return nil, err + } + + posts, err := cl.GetUserPosts() + if err != nil { + return nil, err + } + + return *posts, nil +} + // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) (*writeas.Post, error) { diff --git a/api/posts.go b/api/posts.go index 68d0ba1..d9a1314 100644 --- a/api/posts.go +++ b/api/posts.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" @@ -27,6 +28,18 @@ type Post struct { EditToken string } +// RemotePost holds addition information about published +// posts +type RemotePost struct { + Post + Title, + Excerpt, + Slug, + Collection, + EditToken string + Updated time.Time +} + func AddPost(c *cli.Context, id, token string) error { f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) if err != nil { @@ -80,6 +93,55 @@ func GetPosts(c *cli.Context) *[]Post { return &posts } +func GetUserPosts(c *cli.Context) ([]RemotePost, error) { + waposts, err := DoFetchPosts(c) + if err != nil { + return nil, err + } + + if len(waposts) == 0 { + return nil, nil + } + + posts := []RemotePost{} + for _, p := range waposts { + post := RemotePost{ + Post: Post{ + ID: p.ID, + EditToken: p.Token, + }, + Title: p.Title, + Excerpt: getExcerpt(p.Content), + Slug: p.Slug, + Updated: p.Updated, + } + if p.Collection != nil { + post.Collection = p.Collection.Alias + } + posts = append(posts, post) + } + + return posts, nil +} + +// getExcerpt takes in a content string and returns +// a concatenated version. limited to no more than +// two lines of 80 chars each. delimited by '...' +func getExcerpt(input string) string { + length := len(input) + + if length <= 80 { + return input + } + + if length <= 160 { + return input[:80] + "\n" + input[80:] + } + + excerpt := input[:157] + return excerpt[:80] + "\n" + excerpt[80:] + "..." +} + func ComposeNewPost() (string, *[]byte) { f, err := fileutils.TempFile(os.TempDir(), "WApost", "txt") if err != nil { diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 15c12de..e7cb833 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -152,10 +152,12 @@ func main() { Action: commands.CmdAdd, }, { - Name: "posts", - Usage: "List posts", - Description: `List all local posts.`, - Action: commands.CmdList, + Name: "posts", + Usage: "List posts", + Description: `List all of your posts. + + This will be only local posts when not currently authenticated. To see remote posts as well run 'writeas auth [username]' first, and authenticate with your password.`, + Action: commands.CmdListPosts, Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", diff --git a/commands/commands.go b/commands/commands.go index dede994..a4c4b7d 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "text/tabwriter" "github.com/howeyc/gopass" "github.com/writeas/writeas-cli/api" @@ -183,32 +184,64 @@ func CmdAdd(c *cli.Context) error { return err } -func CmdList(c *cli.Context) error { +func CmdListPosts(c *cli.Context) error { urls := c.Bool("url") ids := c.Bool("id") var p api.Post posts := api.GetPosts(c) + tw := tabwriter.NewWriter(os.Stderr, 10, 0, 2, ' ', tabwriter.TabIndent) + // TODO add no posts found text + if ids || !urls { + fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "ID", "Token") + } else { + fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "URL", "Token") + } for i := range *posts { p = (*posts)[len(*posts)-1-i] if ids || !urls { - fmt.Printf("%s ", p.ID) + fmt.Fprintf(tw, "local\t%s\t%s\t\n", p.ID, p.EditToken) + } else { + fmt.Fprintf(tw, "local\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) } - if urls { - base := config.WriteasBaseURL - if config.IsDev() { - base = config.DevBaseURL + } + u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if u != nil { + remotePosts, err := api.GetUserPosts(c) + if err != nil { + fmt.Println(err) + } + + if len(remotePosts) > 0 { + identifier := "URL" + if ids || !urls { + identifier = "ID" } - ext := "" - // Output URL in requested format - if c.Bool("md") { - ext = ".md" + fmt.Fprintf(tw, "\nLocation\t%s\t%s\t\n", identifier, "Title") + } + for _, p := range remotePosts { + identifier := getPostURL(c, p.ID) + if ids || !urls { + identifier = p.ID } - fmt.Printf("%s/%s%s ", base, p.ID, ext) + + fmt.Fprintf(tw, "remote\t%s\t%s\t\n", identifier, p.Title) } - fmt.Print("\n") } - return nil + return tw.Flush() +} + +func getPostURL(c *cli.Context, slug string) string { + base := config.WriteasBaseURL + if config.IsDev() { + base = config.DevBaseURL + } + ext := "" + // Output URL in requested format + if c.Bool("md") { + ext = ".md" + } + return fmt.Sprintf("%s/%s%s", base, slug, ext) } func CmdAuth(c *cli.Context) error { diff --git a/go.mod b/go.mod index ec850e6..ccd8a84 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/mitchellh/go-homedir v1.0.0 github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect + github.com/ryanuber/columnize v2.1.0+incompatible github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect diff --git a/go.sum b/go.sum index 9662d3c..52e9866 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= From afe2bc324e75cccbfd0aa4aefe3dfe48da49f5dd Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 30 May 2019 14:49:39 -0700 Subject: [PATCH 063/181] go mod tidy, remove previous dep for columns --- go.mod | 1 - go.sum | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ccd8a84..ec850e6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/mitchellh/go-homedir v1.0.0 github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect - github.com/ryanuber/columnize v2.1.0+incompatible github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect diff --git a/go.sum b/go.sum index 52e9866..11649ce 100644 --- a/go.sum +++ b/go.sum @@ -4,7 +4,9 @@ github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVG github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -23,8 +25,6 @@ github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -44,12 +44,14 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= From 6bb2d382084ae4d6e610b6b34b3ff55c42f97a91 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 30 May 2019 15:57:55 -0700 Subject: [PATCH 064/181] only show local posts header if there are any --- commands/commands.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index a4c4b7d..24de719 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -191,14 +191,16 @@ func CmdListPosts(c *cli.Context) error { var p api.Post posts := api.GetPosts(c) tw := tabwriter.NewWriter(os.Stderr, 10, 0, 2, ' ', tabwriter.TabIndent) - // TODO add no posts found text - if ids || !urls { + numPosts := len(*posts) + if ids || !urls && numPosts != 0 { fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "ID", "Token") - } else { + } else if numPosts != 0 { fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "URL", "Token") + } else { + fmt.Fprintf(tw, "No local posts found\n") } for i := range *posts { - p = (*posts)[len(*posts)-1-i] + p = (*posts)[numPosts-1-i] if ids || !urls { fmt.Fprintf(tw, "local\t%s\t%s\t\n", p.ID, p.EditToken) } else { From 82ecc7b388e2e29686ff7627971043571c57ab09 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Sat, 1 Jun 2019 12:42:39 -0700 Subject: [PATCH 065/181] fixes potential issue with multi-byte chars this moves string trimming into it's own helper which accounts for the possibility of multi-byte runes. includes tests for ascii and utf-8 language strings --- api/posts.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/api/posts.go b/api/posts.go index d9a1314..851dead 100644 --- a/api/posts.go +++ b/api/posts.go @@ -132,14 +132,42 @@ func getExcerpt(input string) string { if length <= 80 { return input + } else if length <= 160 { + ln1, idx := trimToLength(input, 80) + if idx == -1 { + idx = 80 + } + ln2, _ := trimToLength(input[idx:], 80) + return ln1 + "\n" + ln2 + } else { + excerpt := input[:157] + ln1, idx := trimToLength(excerpt, 80) + if idx == -1 { + idx = 80 + } + ln2, _ := trimToLength(excerpt[idx:], 80) + return ln1 + "\n" + ln2 + "..." } +} - if length <= 160 { - return input[:80] + "\n" + input[80:] +func trimToLength(in string, l int) (string, int) { + c := []rune(in) + spaceIdx := -1 + length := len(c) + if length <= l { + return in, spaceIdx } - excerpt := input[:157] - return excerpt[:80] + "\n" + excerpt[80:] + "..." + for i := l; i > 0; i-- { + if c[i] == ' ' { + spaceIdx = i + break + } + } + if spaceIdx > -1 { + c = c[:spaceIdx] + } + return string(c), spaceIdx } func ComposeNewPost() (string, *[]byte) { From 0ac5ec80ff47192386560c410a30e19acd96eb85 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Sat, 1 Jun 2019 13:46:54 -0700 Subject: [PATCH 066/181] finish getExcerpt and actualy add tests adds tests mentioned in previous commit for trimToLength and additonal tests for getExcerpt --- api/posts.go | 4 +- api/posts_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 api/posts_test.go diff --git a/api/posts.go b/api/posts.go index 851dead..3506d67 100644 --- a/api/posts.go +++ b/api/posts.go @@ -132,7 +132,7 @@ func getExcerpt(input string) string { if length <= 80 { return input - } else if length <= 160 { + } else if length < 160 { ln1, idx := trimToLength(input, 80) if idx == -1 { idx = 80 @@ -140,7 +140,7 @@ func getExcerpt(input string) string { ln2, _ := trimToLength(input[idx:], 80) return ln1 + "\n" + ln2 } else { - excerpt := input[:157] + excerpt := input[:158] ln1, idx := trimToLength(excerpt, 80) if idx == -1 { idx = 80 diff --git a/api/posts_test.go b/api/posts_test.go new file mode 100644 index 0000000..8630efe --- /dev/null +++ b/api/posts_test.go @@ -0,0 +1,103 @@ +package api + +import "testing" + +func TestTrimToLength(t *testing.T) { + tt := []struct { + Name string + Data string + Length int + ResultData string + ResultIDX int + }{ + { + "English, longer than trim length", + "This is a string, let's truncate it.", + 12, + "This is a", + 9, + }, { + "English, equal to length", + "Some other string.", + 18, + "Some other string.", + -1, + }, { + "English, shorter than trim length", + "I'm short!", + 20, + "I'm short!", + -1, + }, { + "Multi-byte, longer than trim length", + "這是一個較長的廣東話。 有許多特性可以確保足夠長的輸出。", + 14, + "這是一個較長的廣東話。", + 11, + }, { + "Multi-byte, equal to length", + "這是一個簡短的廣東話。", + 11, + "這是一個簡短的廣東話。", + -1, + }, { + "Multi-byte, shorter than trim length", + "我也很矮! 有空間。", + 20, + "我也很矮! 有空間。", + -1, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + out, idx := trimToLength(tc.Data, tc.Length) + if out != tc.ResultData { + t.Errorf("Incorrect output, expecting \"%s\" but got \"%s\"", tc.ResultData, out) + } + + if idx != tc.ResultIDX { + t.Errorf("Incorrect last index, expected \"%d\" but got \"%d\"", tc.ResultIDX, idx) + } + }) + } +} + +func TestGetExcerpt(t *testing.T) { + tt := []struct { + Name string + Data string + Result string + }{ + { + "Shorter than one line", + "This is much less than 80 chars", + "This is much less than 80 chars", + }, { + "Exact length, one line", + "This will be only 80 chars. Maybe all the way to column 88, that will do it. ---", + "This will be only 80 chars. Maybe all the way to column 88, that will do it. ---", + }, { + "Shorter than two lines", + "This will be more than one line but shorter than two. It should break at the 80th or less character. Let's check it out.", + "This will be more than one line but shorter than two. It should break at the\n 80th or less character. Let's check it out.", + }, { + "Exact length, two lines", + "This should be the exact length for two lines. There should ideally be no trailing periods to indicate further text. However trimToLength breaks on word bounds.", + "This should be the exact length for two lines. There should ideally be no\n trailing periods to indicate further text. However trimToLength breaks on word...", + }, { + "Longer than two lines", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque volutpat sagittis aliquet. Ut eu rutrum nisl. Proin molestie ante in dui vulputate dictum. Proin ac bibendum eros. Nulla porta congue tellus, sed vehicula sem bibendum eu. Donec vehicula erat viverra fermentum mattis. Integer volutpat.", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque volutpat\n sagittis aliquet. Ut eu rutrum nisl. Proin molestie ante in dui vulputate...", + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + out := getExcerpt(tc.Data) + if out != tc.Result { + t.Errorf("Output does not match:\nexpected \"%s\"\nbut got \"%s\"", tc.Result, out) + } + }) + } +} From dbe15329af30f2717bce7b917c7411d0c139e6ce Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 5 Jun 2019 11:05:37 -0700 Subject: [PATCH 067/181] return errors on delete command changes command for delete to return errors to the user also updates comment for DoDelete --- api/api.go | 2 +- commands/commands.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/api.go b/api/api.go index c6c9fee..3081e4c 100644 --- a/api/api.go +++ b/api/api.go @@ -153,7 +153,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, return nil } -// DoDelete deletes the given post on Write.as. +// DoDelete deletes the given post on Write.as, and removes any local references func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { cl, _ := NewClient(c, false) diff --git a/commands/commands.go b/commands/commands.go index dede994..068ac26 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -104,7 +104,7 @@ func CmdDelete(c *cli.Context) error { err := api.DoDelete(c, friendlyID, token, tor) if err != nil { - return err + log.ErrorlnQuit("Couldn't delete remote copy: %v", err) } // Delete local file, if necessary @@ -113,7 +113,7 @@ func CmdDelete(c *cli.Context) error { // TODO: handle deleting blog posts err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+api.PostFileExt)) if err != nil { - return err + log.ErrorlnQuit("Couldn't delete local copy: %v", err) } } From 07a55f4530a9e0acd21c33c22534846bf2c547cf Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 5 Jun 2019 13:37:16 -0700 Subject: [PATCH 068/181] use cli.NewExitError in CmdDelete --- commands/commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 068ac26..840120a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -104,7 +104,7 @@ func CmdDelete(c *cli.Context) error { err := api.DoDelete(c, friendlyID, token, tor) if err != nil { - log.ErrorlnQuit("Couldn't delete remote copy: %v", err) + return cli.NewExitError(fmt.Sprintf("Couldn't delete remote copy: %v", err), 1) } // Delete local file, if necessary @@ -113,7 +113,7 @@ func CmdDelete(c *cli.Context) error { // TODO: handle deleting blog posts err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+api.PostFileExt)) if err != nil { - log.ErrorlnQuit("Couldn't delete local copy: %v", err) + return cli.NewExitError(fmt.Sprintf("Couldn't delete local copy: %v", err), 1) } } From 97d682c73da2a6a5c81a732d549b4bcce62e3331 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 5 Jun 2019 15:32:11 -0700 Subject: [PATCH 069/181] use current application vocabulary hopefully making the output simple to understand at a glance --- api/posts.go | 2 ++ cmd/writeas/main.go | 10 ++++------ commands/commands.go | 17 ++++++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/api/posts.go b/api/posts.go index 3506d67..1220316 100644 --- a/api/posts.go +++ b/api/posts.go @@ -37,6 +37,7 @@ type RemotePost struct { Slug, Collection, EditToken string + Synced bool Updated time.Time } @@ -113,6 +114,7 @@ func GetUserPosts(c *cli.Context) ([]RemotePost, error) { Title: p.Title, Excerpt: getExcerpt(p.Content), Slug: p.Slug, + Synced: p.Slug != "", Updated: p.Updated, } if p.Collection != nil { diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index e7cb833..4121270 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -152,12 +152,10 @@ func main() { Action: commands.CmdAdd, }, { - Name: "posts", - Usage: "List posts", - Description: `List all of your posts. - - This will be only local posts when not currently authenticated. To see remote posts as well run 'writeas auth [username]' first, and authenticate with your password.`, - Action: commands.CmdListPosts, + Name: "posts", + Usage: "List all of your posts", + Description: "This will list only local posts when not currently authenticated. To list remote posts as well, first run: writeas auth .", + Action: commands.CmdListPosts, Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", diff --git a/commands/commands.go b/commands/commands.go index 24de719..dd06c72 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -193,18 +193,18 @@ func CmdListPosts(c *cli.Context) error { tw := tabwriter.NewWriter(os.Stderr, 10, 0, 2, ' ', tabwriter.TabIndent) numPosts := len(*posts) if ids || !urls && numPosts != 0 { - fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "ID", "Token") + fmt.Fprintf(tw, "Local\t%s\t%s\t\n", "ID", "Token") } else if numPosts != 0 { - fmt.Fprintf(tw, "Location\t%s\t%s\t\n", "URL", "Token") + fmt.Fprintf(tw, "Local\t%s\t%s\t\n", "URL", "Token") } else { fmt.Fprintf(tw, "No local posts found\n") } for i := range *posts { p = (*posts)[numPosts-1-i] if ids || !urls { - fmt.Fprintf(tw, "local\t%s\t%s\t\n", p.ID, p.EditToken) + fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", p.ID, p.EditToken) } else { - fmt.Fprintf(tw, "local\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) + fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) } } u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) @@ -219,15 +219,18 @@ func CmdListPosts(c *cli.Context) error { if ids || !urls { identifier = "ID" } - fmt.Fprintf(tw, "\nLocation\t%s\t%s\t\n", identifier, "Title") + fmt.Fprintf(tw, "\nAccount\t%s\t%s\t\n", identifier, "Title") } for _, p := range remotePosts { identifier := getPostURL(c, p.ID) if ids || !urls { identifier = p.ID } - - fmt.Fprintf(tw, "remote\t%s\t%s\t\n", identifier, p.Title) + synced := "unsynced" + if p.Synced { + synced = "synced" + } + fmt.Fprintf(tw, "%s\t%s\t%s\t\n", synced, identifier, p.Title) } } return tw.Flush() From 4ac17b62dd0eaa1f3753c2c0194753dfe2588610 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 07:30:13 -0700 Subject: [PATCH 070/181] use stdout in list posts tabwriter was using stderr from initial prototyping --- commands/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index dd06c72..ae02bc2 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -190,7 +190,7 @@ func CmdListPosts(c *cli.Context) error { var p api.Post posts := api.GetPosts(c) - tw := tabwriter.NewWriter(os.Stderr, 10, 0, 2, ' ', tabwriter.TabIndent) + tw := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', tabwriter.TabIndent) numPosts := len(*posts) if ids || !urls && numPosts != 0 { fmt.Fprintf(tw, "Local\t%s\t%s\t\n", "ID", "Token") From 30ecad172a26fb8da6ed82e303d9553abf244648 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 09:44:29 -0700 Subject: [PATCH 071/181] add collection listing adds subcommand colls lists collections for authenticated user supports -url flag for listing collection URLs --- api/api.go | 27 +++++++++++++++++++++++++++ api/collections.go | 9 +++++++++ cmd/writeas/main.go | 10 ++++++++++ commands/commands.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 api/collections.go diff --git a/api/api.go b/api/api.go index 16e6f2d..f21308a 100644 --- a/api/api.go +++ b/api/api.go @@ -140,6 +140,33 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( return p, nil } +// DoFetchCollections retrieves a list of the currently logged in users +// collections. +func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { + cl, err := NewClient(c, true) + if err != nil { + // + } + + colls, err := cl.GetUserCollections() + if err != nil { + // + } + + out := make([]RemoteColl, len(*colls)) + + for i, c := range *colls { + coll := RemoteColl{ + Alias: c.Alias, + Title: c.Title, + URL: c.URL, + } + out[i] = coll + } + + return out, nil +} + // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { cl, _ := NewClient(c, false) diff --git a/api/collections.go b/api/collections.go new file mode 100644 index 0000000..a7483b1 --- /dev/null +++ b/api/collections.go @@ -0,0 +1,9 @@ +package api + +// RemoteColl represents a collection of posts +// It is a reduced set of data from a go-writeas Collection +type RemoteColl struct { + Alias string + Title string + URL string +} diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 4121270..092e4f8 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -170,6 +170,16 @@ func main() { Usage: "Show list with URLs", }, }, + }, { + Name: "colls", + Usage: "List collections", + Action: commands.CmdCollections, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "url", + Usage: "Show list with URLs", + }, + }, }, { Name: "fetch", diff --git a/commands/commands.go b/commands/commands.go index 94a1388..96670e6 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -249,6 +249,36 @@ func getPostURL(c *cli.Context, slug string) string { return fmt.Sprintf("%s/%s%s", base, slug, ext) } +func CmdCollections(c *cli.Context) error { + u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + } + if u == nil { + return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth ", 1) + } + colls, err := api.DoFetchCollections(c) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Couldn't get collections for user %s: %v", u.User.Username, err), 1) + } + urls := c.Bool("url") + tw := tabwriter.NewWriter(os.Stdout, 8, 2, 0, ' ', tabwriter.TabIndent) + detail := "Title" + if urls { + detail = "URL" + } + fmt.Fprintf(tw, "%s\t%s\t\n", "Alias", detail) + for _, c := range colls { + dData := c.Title + if urls { + dData = c.URL + } + fmt.Fprintf(tw, "%s\t%s\t\n", c.Alias, dData) + } + tw.Flush() + return nil +} + func CmdAuth(c *cli.Context) error { // Check configuration u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) From e918e6e22ed7700ad918b59ab4ad55ad0c54f5d2 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 11:30:35 -0700 Subject: [PATCH 072/181] change colls cmd to blogs --- cmd/writeas/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 092e4f8..5657417 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -171,8 +171,8 @@ func main() { }, }, }, { - Name: "colls", - Usage: "List collections", + Name: "blogs", + Usage: "List blogs", Action: commands.CmdCollections, Flags: []cli.Flag{ cli.BoolFlag{ From e424bdddd11907de1e981930bff872c0b3703647 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 15:46:27 -0700 Subject: [PATCH 073/181] remove fetch sub cmd --- cmd/writeas/main.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 5657417..c429b33 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -3,7 +3,6 @@ package main import ( "os" - "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/log" @@ -181,26 +180,6 @@ func main() { }, }, }, - { - Name: "fetch", - Usage: "Fetch authenticated user's Write.as posts", - Action: api.CmdPull, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "tor, t", - Usage: "Authenticate via Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, - cli.BoolFlag{ - Name: "verbose, v", - Usage: "Make the operation more talkative", - }, - }, - }, { Name: "auth", Usage: "Authenticate with Write.as", From 8cc41b8e3503fbadb9c8a11c5d712d9c0cebebef Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 07:43:08 -0700 Subject: [PATCH 074/181] return proper errors in CmdCollections fix tabwriter padding --- api/api.go | 10 ++++++++-- commands/commands.go | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/api/api.go b/api/api.go index f21308a..b34ce99 100644 --- a/api/api.go +++ b/api/api.go @@ -145,12 +145,18 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { cl, err := NewClient(c, true) if err != nil { - // + if config.Debug() { + log.ErrorlnQuit("could not create new client: %v", err) + } + return nil, fmt.Errorf("Couldn't create new client") } colls, err := cl.GetUserCollections() if err != nil { - // + if config.Debug() { + log.ErrorlnQuit("failed fetching user collections: %v", err) + } + return nil, fmt.Errorf("Couldn't get user collections") } out := make([]RemoteColl, len(*colls)) diff --git a/commands/commands.go b/commands/commands.go index 96670e6..57c0315 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -262,7 +262,7 @@ func CmdCollections(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("Couldn't get collections for user %s: %v", u.User.Username, err), 1) } urls := c.Bool("url") - tw := tabwriter.NewWriter(os.Stdout, 8, 2, 0, ' ', tabwriter.TabIndent) + tw := tabwriter.NewWriter(os.Stdout, 8, 0, 2, ' ', tabwriter.TabIndent) detail := "Title" if urls { detail = "URL" From 7c104467d3888d7c52a7941d6b64ee526b37ebf3 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 11:24:05 -0700 Subject: [PATCH 075/181] update README and GUIDE for v2 --- GUIDE.md | 60 ++++++++++++++++++++++++++++++++++++++----------------- README.md | 32 +++++++++++++++++------------ 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 87a2948..caa942d 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -13,26 +13,31 @@ These are a few common uses for `writeas`. If you get stuck or want to know more ### Overview ``` -writeas [global options] command [command options] [arguments...] + writeas [global options] command [command options] [arguments...] COMMANDS: - post Alias for default action: create post from stdin - new Compose a new post from the command-line and publish - publish Publish a file to Write.as - delete Delete a post - update Update (overwrite) a post - get Read a raw post - add Add an existing post locally - list List local posts - auth Authenticate with Write.as - help, h Shows a list of commands or help for one command + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file to Write.as + delete Delete a post + update Update (overwrite) a post + get Read a raw post + add Add an existing post locally + posts List all of your posts + fetch Fetch authenticated user's Write.as posts + auth Authenticate with Write.as + logout Log out of Write.as + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: + -c value, -b value Optional blog to post to --tor, -t Perform action on Tor hidden service --tor-port value Use a different port to connect to Tor (default: 9150) --code Specifies this post is code + --md Returns post URL with Markdown enabled --verbose, -v Make the operation more talkative --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code --user-agent value Sets the User-Agent for API requests --help, -h show help --version, -V print the version @@ -44,7 +49,7 @@ By default, `writeas` creates a post with a `monospace` typeface that doesn't wo ```bash $ echo "Hello world!" | writeas -https://write.as/aaaaaaaaaaaa +https://write.as/aaaazzzzzzzza ``` This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting): @@ -58,19 +63,23 @@ Windows: `type writeas/cli.go | writeas.exe --code` This outputs any Write.as post with the given ID. ```bash -$ writeas get aaaaaaaaaaaa +$ writeas get aaaazzzzzzzza Hello world! ``` #### List all published posts -This lists all posts you've published from your device. +This lists all posts you've published from your device, as well as any published by the authenticated user. Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. ```bash -$ writeas list -aaaaaaaaaaaa +$ writeas posts +Local ID Token +unsynced aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j + +Account ID Title +synced mmmmmmmm33333333 This is a post ``` #### Delete a post @@ -78,7 +87,7 @@ aaaaaaaaaaaa This permanently deletes a post you own. ```bash -$ writeas delete aaaaaaaaaaaa +$ writeas delete aaaazzzzzzzza ``` #### Update a post @@ -86,7 +95,7 @@ $ writeas delete aaaaaaaaaaaa This completely overwrites an existing post you own. ```bash -$ echo "See you later!" | writeas update aaaaaaaaaaaa +$ echo "See you later!" | writeas update aaaazzzzzzzza ``` ### Composing posts @@ -108,3 +117,18 @@ Customize your post's appearance with the `--font` flag: Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans` If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md` + +### Fetch posts + +This pulls down local copies of the authenticated user's posts. + +You will be prompted for a storage location the first time, if you have not already configured one. + +```bash +$ writeas fetch +Posts directory? [/home/username/present/directory]: /home/username/posts +Created posts directory. +Saved config. +$ ls /home/username/posts +blog/ aaaazzzzzzzza.txt +``` \ No newline at end of file diff --git a/README.md b/README.md index ef43b03..1711deb 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ Command line interface for [Write.as](https://write.as). Works on Windows, macOS ## Features * Publish anonymously to Write.as +* Authenticate with a Write.as account * A stable, easy back-end for your GUI app or desktop-based workflow * Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/) * Locally keeps track of any posts you make -* Update and delete anonymous posts +* Update and delete posts, anonymous and authenticated * Fetch any post by ID * Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing @@ -53,26 +54,31 @@ go get -u github.com/writeas/writeas-cli/cmd/writeas See full usage documentation on our [User Guide](GUIDE.md). ``` -writeas [global options] command [command options] [arguments...] + writeas [global options] command [command options] [arguments...] COMMANDS: - post Alias for default action: create post from stdin - new Compose a new post from the command-line and publish - publish Publish a file to Write.as - delete Delete a post - update Update (overwrite) a post - get Read a raw post - add Add an existing post locally - list List local posts - auth Authenticate with Write.as - help, h Shows a list of commands or help for one command + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file to Write.as + delete Delete a post + update Update (overwrite) a post + get Read a raw post + add Add an existing post locally + posts List all of your posts + fetch Fetch authenticated user's Write.as posts + auth Authenticate with Write.as + logout Log out of Write.as + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: + -c value, -b value Optional blog to post to --tor, -t Perform action on Tor hidden service --tor-port value Use a different port to connect to Tor (default: 9150) --code Specifies this post is code + --md Returns post URL with Markdown enabled --verbose, -v Make the operation more talkative --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code --user-agent value Sets the User-Agent for API requests --help, -h show help --version, -V print the version @@ -86,7 +92,7 @@ We welcome any kind of contributions including documentation, organizational imp ### Getting Support -We're available on [several channels](https://write.as/contact), and prefer the #development channel [in Slack](http://slack.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. +We're available on [several channels](https://write.as/contact), and prefer our [discuss instance](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. ### Reporting Issues From 2d2b63d55d1e18110dac771b3c3fe547e09db369 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 07:45:41 -0700 Subject: [PATCH 076/181] remove mention of fetch cmd in docs --- GUIDE.md | 16 ---------------- README.md | 1 - 2 files changed, 17 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index caa942d..b24ecd0 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -24,7 +24,6 @@ COMMANDS: get Read a raw post add Add an existing post locally posts List all of your posts - fetch Fetch authenticated user's Write.as posts auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command @@ -117,18 +116,3 @@ Customize your post's appearance with the `--font` flag: Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans` If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md` - -### Fetch posts - -This pulls down local copies of the authenticated user's posts. - -You will be prompted for a storage location the first time, if you have not already configured one. - -```bash -$ writeas fetch -Posts directory? [/home/username/present/directory]: /home/username/posts -Created posts directory. -Saved config. -$ ls /home/username/posts -blog/ aaaazzzzzzzza.txt -``` \ No newline at end of file diff --git a/README.md b/README.md index 1711deb..cac390d 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ COMMANDS: get Read a raw post add Add an existing post locally posts List all of your posts - fetch Fetch authenticated user's Write.as posts auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command From b64adfdc08c3bcf57af1ae44eebb0764f061f12b Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 08:43:18 -0700 Subject: [PATCH 077/181] update CLI help output --- GUIDE.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/GUIDE.md b/GUIDE.md index b24ecd0..67667df 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -24,6 +24,7 @@ COMMANDS: get Read a raw post add Add an existing post locally posts List all of your posts + blogs List blogs auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command diff --git a/README.md b/README.md index cac390d..cee2294 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ COMMANDS: get Read a raw post add Add an existing post locally posts List all of your posts + blogs List blogs auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command From 29d829bae9855d42a5b1ed346080012dac16369a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 10 Jun 2019 12:26:25 -0400 Subject: [PATCH 078/181] Update forum link text and replace Slack badge with a link to the forum. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cee2294..1d3847a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ writeas-cli =========== -![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) +![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development) Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. @@ -92,7 +92,7 @@ We welcome any kind of contributions including documentation, organizational imp ### Getting Support -We're available on [several channels](https://write.as/contact), and prefer our [discuss instance](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. +We're available on [several channels](https://write.as/contact), and prefer our [forum](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. ### Reporting Issues From c371da1e5ee40562217445ce3cf5a143d968bcb4 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 15:01:34 -0700 Subject: [PATCH 079/181] add claim posts to writeas CLI this adds the ability to claim local posts under an authenticated account. removePost is now exported for use the claim command --- api/api.go | 2 +- api/posts.go | 20 +++++++++++++++++++- cmd/writeas/main.go | 5 +++++ commands/commands.go | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index b34ce99..1701941 100644 --- a/api/api.go +++ b/api/api.go @@ -219,7 +219,7 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { } else { log.Info(c, "Post deleted.") } - removePost(c.App.ExtraInfo()["configDir"], friendlyID) + RemovePost(c.App.ExtraInfo()["configDir"], friendlyID) return nil } diff --git a/api/posts.go b/api/posts.go index 1220316..c70da35 100644 --- a/api/posts.go +++ b/api/posts.go @@ -57,6 +57,24 @@ func AddPost(c *cli.Context, id, token string) error { return nil } +// ClaimPost adds a local post to the authenticated user's account and deletes +// the local reference +func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { + cl, err := NewClient(c, true) + if err != nil { + return nil, err + } + postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts)) + for i, post := range *localPosts { + postsToClaim[i] = writeas.OwnedPostParams{ + ID: post.ID, + Token: post.EditToken, + } + } + + return cl.ClaimPosts(&postsToClaim) +} + func TokenFromID(c *cli.Context, id string) string { post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id) if post == "" { @@ -71,7 +89,7 @@ func TokenFromID(c *cli.Context, id string) string { return parts[1] } -func removePost(path, id string) { +func RemovePost(path, id string) { fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id) } diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index c429b33..d612e0c 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -179,6 +179,11 @@ func main() { Usage: "Show list with URLs", }, }, + }, { + Name: "claim", + Usage: "Claim local unsynced posts", + Action: commands.CmdClaim, + Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", }, { Name: "auth", diff --git a/commands/commands.go b/commands/commands.go index 57c0315..5cb947c 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -279,6 +279,41 @@ func CmdCollections(c *cli.Context) error { return nil } +func CmdClaim(c *cli.Context) error { + u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + } + if u == nil { + return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth ", 1) + } + + localPosts := api.GetPosts(c) + if len(*localPosts) == 0 { + return nil + } + + results, err := api.ClaimPosts(c, localPosts) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to claim posts: %v", err), 1) + } + + for _, r := range *results { + fmt.Printf("Adding %s to user %s..", r.Post.ID, u.User.Username) + if r.ErrorMessage != "" { + fmt.Printf(" Failed\n") + if config.Debug() { + log.Errorln("Failed claiming post %s: %v", r.ID, r.ErrorMessage) + } + } else { + fmt.Printf(" OK\n") + // only delete local if successful + api.RemovePost(c.App.ExtraInfo()["configDir"], r.Post.ID) + } + } + return nil +} + func CmdAuth(c *cli.Context) error { // Check configuration u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) From 31575d41bd30599afdc9998f4bd6e396d52de1e9 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 15:23:42 -0700 Subject: [PATCH 080/181] move to version two of go-writeas this changes imports to use the github import path github.com/writeas/go-writeas/v2 --- api/api.go | 2 +- api/posts.go | 2 +- config/user.go | 2 +- go.mod | 5 ++--- go.sum | 8 ++++++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/api/api.go b/api/api.go index 1701941..d3fa3fc 100644 --- a/api/api.go +++ b/api/api.go @@ -5,11 +5,11 @@ import ( "path/filepath" "github.com/atotto/clipboard" + writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" - writeas "go.code.as/writeas.v2" cli "gopkg.in/urfave/cli.v1" ) diff --git a/api/posts.go b/api/posts.go index c70da35..379a2c6 100644 --- a/api/posts.go +++ b/api/posts.go @@ -10,10 +10,10 @@ import ( "strings" "time" + writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" - writeas "go.code.as/writeas.v2" cli "gopkg.in/urfave/cli.v1" ) diff --git a/config/user.go b/config/user.go index d7039f1..21dd8f9 100644 --- a/config/user.go +++ b/config/user.go @@ -5,8 +5,8 @@ import ( "io/ioutil" "path/filepath" + writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/writeas-cli/fileutils" - "go.code.as/writeas.v2" ) const UserFile = "user.json" diff --git a/go.mod b/go.mod index ec850e6..346f6ef 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/writeas/writeas-cli require ( - code.as/core/socks v0.0.0-20180906144846-5be269b4e664 + code.as/core/socks v1.0.0 github.com/atotto/clipboard v0.1.1 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect @@ -14,10 +14,9 @@ require ( github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - github.com/writeas/impart v0.0.0-20180808220913-fef51864677b // indirect + github.com/writeas/go-writeas/v2 v2.0.0 github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 - go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect diff --git a/go.sum b/go.sum index 11649ce..d135529 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ code.as/core/socks v0.0.0-20180906144846-5be269b4e664 h1:zWSFbwkYSuZ2PjvHqYDE/dhd9CCcsbSvUIRx8hIed3I= code.as/core/socks v0.0.0-20180906144846-5be269b4e664/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= +code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= +code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= github.com/atotto/clipboard v0.1.1 h1:WSoEbAS70E5gw8FbiqFlp69MGsB6dUb4l+0AGGLiVGw= github.com/atotto/clipboard v0.1.1/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= @@ -31,14 +33,16 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/writeas/go-writeas/v2 v2.0.0 h1:KjDI5bQSAIH0IzkKW3uGoY98I1A4DrBsSqBklgyOvHw= +github.com/writeas/go-writeas/v2 v2.0.0/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= +github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= +github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f h1:yyFguE0EopK8e7I7/AB1JWM925OFOI1uFhTM/SwXAnQ= github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 h1:mKD4DMZuiZWrn1k/f+1wLmBu9SYMrydy9om+eeo9kjA= github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE= -go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e h1:emU11ZqEW7s+6/Ty52t0lQ9c3Mg+c97YSwswUeSpsG8= -go.code.as/writeas.v2 v0.0.0-20181216235156-68cbee8f4a5e/go.mod h1:wH0YOXh4B2fcSJ/ihy+qru0XfCdGb4CPKaO0qS2g47k= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From 4b42c2160b7247f2aea50ca591591f116a7206db Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 11 Jun 2019 10:55:42 -0400 Subject: [PATCH 081/181] Fix nil pointer error in CmdClaim Ref T194 --- commands/commands.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 5cb947c..e601a95 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -299,16 +299,21 @@ func CmdClaim(c *cli.Context) error { } for _, r := range *results { - fmt.Printf("Adding %s to user %s..", r.Post.ID, u.User.Username) + id := r.ID + if id == "" { + // No top-level ID, so the claim was successful + id = r.Post.ID + } + fmt.Printf("Adding %s to user %s..", id, u.User.Username) if r.ErrorMessage != "" { fmt.Printf(" Failed\n") if config.Debug() { - log.Errorln("Failed claiming post %s: %v", r.ID, r.ErrorMessage) + log.Errorln("Failed claiming post %s: %v", id, r.ErrorMessage) } } else { fmt.Printf(" OK\n") // only delete local if successful - api.RemovePost(c.App.ExtraInfo()["configDir"], r.Post.ID) + api.RemovePost(c.App.ExtraInfo()["configDir"], id) } } return nil From ed5e498c40445bc4bc8e0077da60e6000253fe51 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 11 Jun 2019 11:22:41 -0400 Subject: [PATCH 082/181] Tweak post claim logging - Presents a summary before making the request when verbose (-v) enabled - Successful claims only shown with verbose (-v) enabled - Failed claims now always mention cause of failure - A summary of successes / failures shows after logging any failures, when verbose (-v) enabled Ref T194 --- cmd/writeas/main.go | 6 ++++++ commands/commands.go | 14 ++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index d612e0c..c113d19 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -184,6 +184,12 @@ func main() { Usage: "Claim local unsynced posts", Action: commands.CmdClaim, Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, }, { Name: "auth", diff --git a/commands/commands.go b/commands/commands.go index e601a95..e594620 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -293,29 +293,31 @@ func CmdClaim(c *cli.Context) error { return nil } + log.Info(c, "Claiming %d post(s) for %s...", len(*localPosts), u.User.Username) results, err := api.ClaimPosts(c, localPosts) if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to claim posts: %v", err), 1) } + var okCount, errCount int for _, r := range *results { id := r.ID if id == "" { // No top-level ID, so the claim was successful id = r.Post.ID } - fmt.Printf("Adding %s to user %s..", id, u.User.Username) + status := fmt.Sprintf("Post %s...", id) if r.ErrorMessage != "" { - fmt.Printf(" Failed\n") - if config.Debug() { - log.Errorln("Failed claiming post %s: %v", id, r.ErrorMessage) - } + log.Errorln("%serror: %v", status, r.ErrorMessage) + errCount++ } else { - fmt.Printf(" OK\n") + log.Info(c, "%sOK", status) + okCount++ // only delete local if successful api.RemovePost(c.App.ExtraInfo()["configDir"], id) } } + log.Info(c, "%d claimed, %d failed", okCount, errCount) return nil } From a294c6e4cded1c1109471f99be7afc6f0755aa55 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 11 Jun 2019 12:14:22 -0400 Subject: [PATCH 083/181] Bump version to 2.0 --- config/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/options.go b/config/options.go index 9b717bd..1abf9ee 100644 --- a/config/options.go +++ b/config/options.go @@ -8,7 +8,7 @@ import ( // Application constants. const ( - Version = "1.99-dev" + Version = "2.0" defaultUserAgent = "writeas-cli v" + Version // Defaults for posts on Write.as. DefaultFont = PostFontMono From 36477afe2a6005085274c445ab686b629b25bb58 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 13 Jun 2019 14:12:17 -0700 Subject: [PATCH 084/181] remove local post save and delete until the fetch command is included again --- commands/commands.go | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index e594620..0a4ba6a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -4,13 +4,11 @@ import ( "fmt" "io/ioutil" "os" - "path/filepath" "text/tabwriter" "github.com/howeyc/gopass" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/config" - "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -60,20 +58,10 @@ func CmdPublish(c *cli.Context) error { if err != nil { return err } - p, err := api.HandlePost(content, c) - if err != nil { - return err - } + _, err = api.HandlePost(content, c) - // Save post to posts folder - cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) - if cfg.Posts.Directory != "" { - err = api.WritePost(cfg.Posts.Directory, p) - if err != nil { - return err - } - } - return nil + // TODO: write local file if directory is set + return err } func CmdDelete(c *cli.Context) error { @@ -108,16 +96,7 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("Couldn't delete remote copy: %v", err), 1) } - // Delete local file, if necessary - cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) - if cfg.Posts.Directory != "" { - // TODO: handle deleting blog posts - err = fileutils.DeleteFile(filepath.Join(cfg.Posts.Directory, friendlyID+api.PostFileExt)) - if err != nil { - return cli.NewExitError(fmt.Sprintf("Couldn't delete local copy: %v", err), 1) - } - } - + // TODO: Delete local file, if necessary return nil } From e5554b745ce0c4af4aff9f3cdd3e065c986b9db8 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 13 Jun 2019 14:29:59 -0700 Subject: [PATCH 085/181] update documentation for v2 - remove line about authentication not being supported - add claim to list of commands in both README and GUIDE - add auth, blogs and claim example usage in GUIDE --- GUIDE.md | 28 ++++++++++++++++++++++++++-- README.md | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 67667df..864ae45 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -4,8 +4,6 @@ The Write.as Command-Line Interface (CLI) is a cross-platform tool for publishin Write.as is a text-publishing service that protects your privacy. There's no sign up required to publish, but if you do sign up, you can access posts across devices and compile collections of them in what most people would call a "blog". -**Note** accounts are not supported in CLI v1.0. They'll be available in [v2.0](https://github.com/writeas/writeas-cli/milestone/4). - ## Uses These are a few common uses for `writeas`. If you get stuck or want to know more, run `writeas [command] --help`. If you still have questions, [ask us](https://write.as/contact). @@ -24,6 +22,7 @@ COMMANDS: get Read a raw post add Add an existing post locally posts List all of your posts + claim Claim local unsynced posts blogs List blogs auth Authenticate with Write.as logout Log out of Write.as @@ -67,6 +66,24 @@ $ writeas get aaaazzzzzzzza Hello world! ``` +#### Authenticate + +This will authenticate with write.as and store the user access token locally, until you explicitly logout. +```bash +$ writeas auth username +Password: ************ +``` + +#### List all blogs + +This will output a list of the authenticated user's blogs. +```bash +$ writeas blogs +Alias Title +user An Example Blog +dev My Dev Log +``` + #### List all published posts This lists all posts you've published from your device, as well as any published by the authenticated user. @@ -98,6 +115,13 @@ This completely overwrites an existing post you own. $ echo "See you later!" | writeas update aaaazzzzzzzza ``` +#### Claim a post + +This moves an unsynced local post to a draft on your account. You will need to authenticate first. +```bash +$ writeas claim aaaazzzzzzzza +``` + ### Composing posts If you simply have a penchant for never leaving your keyboard, `writeas` is great for composing new posts from the command-line. Just use the `new` subcommand. diff --git a/README.md b/README.md index 4e281d4..9cb55f6 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ COMMANDS: add Add an existing post locally posts List all of your posts blogs List blogs + claim Claim local unsynced posts auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command From 1dcbdb8e01a522a4313c7ce9aefeadc78dc52f31 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 18 Jun 2019 14:46:12 -0400 Subject: [PATCH 086/181] Add note about unstable `master` branch --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9cb55f6..5104d89 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ writeas-cli Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. +**NOTE: the `master` branch is currently unstable while we prepare the v2.0 release! You should install via official release channel, or build from the `v1.2` tag.** + ## Features * Publish anonymously to Write.as From d21caacbf50ea3d12d522b695569e21b457a96a1 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 18 Jun 2019 14:46:35 -0400 Subject: [PATCH 087/181] Link to desktop app in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5104d89..65841f4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Command line interface for [Write.as](https://write.as). Works on Windows, macOS * Publish anonymously to Write.as * Authenticate with a Write.as account -* A stable, easy back-end for your GUI app or desktop-based workflow +* A stable, easy back-end for your [GUI app](https://write.as/apps/desktop) or desktop-based workflow * Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/) * Locally keeps track of any posts you make * Update and delete posts, anonymous and authenticated From 4ed622b6dfe875f81413d4e50ad7ebeb9b1149d0 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 18 Jun 2019 14:46:20 -0700 Subject: [PATCH 088/181] revert posts listing style to v1.2 this changes the posts listing output back to v1.2 style also removes remote posts listing for now. posts with edit tokens are behind `-v` flag. --- GUIDE.md | 16 ++++++++------ cmd/writeas/main.go | 6 +++++- commands/commands.go | 50 +++++++------------------------------------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 864ae45..422cb00 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -84,19 +84,23 @@ user An Example Blog dev My Dev Log ``` -#### List all published posts +#### List posts -This lists all posts you've published from your device, as well as any published by the authenticated user. +This lists all posts you've published from your device Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. +To see post IDs with their Edit Tokens pass the `--v` flag. + ```bash $ writeas posts -Local ID Token -unsynced aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j +aaaazzzzzzzza + +$ writeas posts -url +https://write.as/aaaazzzzzzzza -Account ID Title -synced mmmmmmmm33333333 This is a post +$ writeas posts -v +aaaazzzzzzzza | dhuieoj23894jhf984hdfs9834hdf84j ``` #### Delete a post diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index c113d19..06d8af5 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -153,7 +153,7 @@ func main() { { Name: "posts", Usage: "List all of your posts", - Description: "This will list only local posts when not currently authenticated. To list remote posts as well, first run: writeas auth .", + Description: "This will list only local posts.", Action: commands.CmdListPosts, Flags: []cli.Flag{ cli.BoolFlag{ @@ -168,6 +168,10 @@ func main() { Name: "url", Usage: "Show list with URLs", }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Show verbose post listing, including Edit Tokens", + }, }, }, { Name: "blogs", diff --git a/commands/commands.go b/commands/commands.go index 0a4ba6a..e44b419 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -166,53 +166,19 @@ func CmdAdd(c *cli.Context) error { func CmdListPosts(c *cli.Context) error { urls := c.Bool("url") ids := c.Bool("id") + details := c.Bool("v") - var p api.Post posts := api.GetPosts(c) - tw := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', tabwriter.TabIndent) - numPosts := len(*posts) - if ids || !urls && numPosts != 0 { - fmt.Fprintf(tw, "Local\t%s\t%s\t\n", "ID", "Token") - } else if numPosts != 0 { - fmt.Fprintf(tw, "Local\t%s\t%s\t\n", "URL", "Token") - } else { - fmt.Fprintf(tw, "No local posts found\n") - } - for i := range *posts { - p = (*posts)[numPosts-1-i] - if ids || !urls { - fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", p.ID, p.EditToken) + for _, p := range *posts { + if details { + fmt.Printf("%s | %s\n", p.ID, p.EditToken) + } else if ids || !urls { + fmt.Printf("%s\n", p.ID) } else { - fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) + fmt.Printf("%s\n", getPostURL(c, p.ID)) } } - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) - if u != nil { - remotePosts, err := api.GetUserPosts(c) - if err != nil { - fmt.Println(err) - } - - if len(remotePosts) > 0 { - identifier := "URL" - if ids || !urls { - identifier = "ID" - } - fmt.Fprintf(tw, "\nAccount\t%s\t%s\t\n", identifier, "Title") - } - for _, p := range remotePosts { - identifier := getPostURL(c, p.ID) - if ids || !urls { - identifier = p.ID - } - synced := "unsynced" - if p.Synced { - synced = "synced" - } - fmt.Fprintf(tw, "%s\t%s\t%s\t\n", synced, identifier, p.Title) - } - } - return tw.Flush() + return nil } func getPostURL(c *cli.Context, slug string) string { From ece87e913398d8c7f423db37337fd37984238834 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 13:19:44 -0700 Subject: [PATCH 089/181] add new wf binary for community instances this creates a second binary, wf, which will have additonal functionality for hosts other than write.as. also moves global flags into their own slice in new file adds `host, H` global flag inlcudes gitignore for binary --- cmd/wf/.gitignore | 1 + cmd/wf/config_nix.go | 7 ++ cmd/wf/config_win.go | 7 ++ cmd/wf/flags.go | 30 +++++ cmd/wf/main.go | 266 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 311 insertions(+) create mode 100644 cmd/wf/.gitignore create mode 100644 cmd/wf/config_nix.go create mode 100644 cmd/wf/config_win.go create mode 100644 cmd/wf/flags.go create mode 100644 cmd/wf/main.go diff --git a/cmd/wf/.gitignore b/cmd/wf/.gitignore new file mode 100644 index 0000000..89b7a51 --- /dev/null +++ b/cmd/wf/.gitignore @@ -0,0 +1 @@ +wf \ No newline at end of file diff --git a/cmd/wf/config_nix.go b/cmd/wf/config_nix.go new file mode 100644 index 0000000..4abd334 --- /dev/null +++ b/cmd/wf/config_nix.go @@ -0,0 +1,7 @@ +// +build !windows + +package main + +var appInfo = map[string]string{ + "configDir": ".writefreely", +} diff --git a/cmd/wf/config_win.go b/cmd/wf/config_win.go new file mode 100644 index 0000000..e44b45b --- /dev/null +++ b/cmd/wf/config_win.go @@ -0,0 +1,7 @@ +// +build windows + +package main + +var appInfo = map[string]string{ + "configDir": "WriteFreely", +} diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go new file mode 100644 index 0000000..03ed17c --- /dev/null +++ b/cmd/wf/flags.go @@ -0,0 +1,30 @@ +package main + +import ( + "gopkg.in/urfave/cli.v1" +) + +var globalFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Perform action on Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + cli.StringFlag{ + Name: "user-agent", + Usage: "Sets the User-Agent for API requests", + Value: "", + }, + cli.StringFlag{ + Name: "host, H", + Usage: "Operate against a custom hostname", + }, +} diff --git a/cmd/wf/main.go b/cmd/wf/main.go new file mode 100644 index 0000000..8b78771 --- /dev/null +++ b/cmd/wf/main.go @@ -0,0 +1,266 @@ +package main + +import ( + "os" + + "github.com/writeas/writeas-cli/api" + "github.com/writeas/writeas-cli/commands" + "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/log" + cli "gopkg.in/urfave/cli.v1" +) + +func main() { + initialize(appInfo["configDir"]) + cli.VersionFlag = cli.BoolFlag{ + Name: "version, V", + Usage: "print the version", + } + + // Run the app + app := cli.NewApp() + app.Name = "wf" + app.Version = config.Version + app.Usage = "Publish text quickly" + // TODO: who is the author? the contributors? link to GH? + app.Authors = []cli.Author{ + { + Name: "Write.as", + Email: "hello@write.as", + }, + } + app.ExtraInfo = func() map[string]string { + return appInfo + } + app.Action = commands.CmdPost + app.Flags = globalFlags + app.Commands = []cli.Command{ + { + Name: "post", + Usage: "Alias for default action: create post from stdin", + Action: commands.CmdPost, + Flags: config.PostFlags, + Description: `Create a new post on Write.as from stdin. + + Use the --code flag to indicate that the post should use syntax + highlighting. Or use the --font [value] argument to set the post's + appearance, where [value] is mono, monospace (default), wrap (monospace + font with word wrapping), serif, or sans.`, + }, + { + Name: "new", + Usage: "Compose a new post from the command-line and publish", + Description: `An alternative to piping data to the program. + + On Windows, this will use 'copy con' to start reading what you input from the + prompt. Press F6 or Ctrl-Z then Enter to end input. + On *nix, this will use the best available text editor, starting with the + value set to the WRITEAS_EDITOR or EDITOR environment variable, or vim, or + finally nano. + + Use the --code flag to indicate that the post should use syntax + highlighting. Or use the --font [value] argument to set the post's + appearance, where [value] is mono, monospace (default), wrap (monospace + font with word wrapping), serif, or sans. + + If posting fails for any reason, 'writeas' will show you the temporary file + location and how to pipe it to 'writeas' to retry.`, + Action: commands.CmdNew, + Flags: config.PostFlags, + }, + { + Name: "publish", + Usage: "Publish a file to Write.as", + Action: commands.CmdPublish, + Flags: config.PostFlags, + }, + { + Name: "delete", + Usage: "Delete a post", + Action: commands.CmdDelete, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Delete via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + { + Name: "update", + Usage: "Update (overwrite) a post", + Action: commands.CmdUpdate, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Update via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "code", + Usage: "Specifies this post is code", + }, + cli.StringFlag{ + Name: "font", + Usage: "Sets post font to given value", + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + { + Name: "get", + Usage: "Read a raw post", + Action: commands.CmdGet, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Get from Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + { + Name: "add", + Usage: "Add an existing post locally", + Description: `A way to add an existing post to your local store for easy editing later. + + This requires a post ID (from https://write.as/[ID]) and an Edit Token + (exported from another Write.as client, such as the Android app). +`, + Action: commands.CmdAdd, + }, + { + Name: "posts", + Usage: "List all of your posts", + Description: "This will list only local posts when not currently authenticated. To list remote posts as well, first run: writeas auth .", + Action: commands.CmdListPosts, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "id", + Usage: "Show list with post IDs (default)", + }, + cli.BoolFlag{ + Name: "md", + Usage: "Use with --url to return URLs with Markdown enabled", + }, + cli.BoolFlag{ + Name: "url", + Usage: "Show list with URLs", + }, + }, + }, + { + Name: "fetch", + Usage: "Fetch authenticated user's Write.as posts", + Action: api.CmdPull, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + { + Name: "auth", + Usage: "Authenticate with Write.as", + Action: commands.CmdAuth, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + { + Name: "logout", + Usage: "Log out of Write.as", + Action: commands.CmdLogOut, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, + }, + } + + cli.CommandHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + writeas {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .Flags}} + +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{ end }} +` + app.Run(os.Args) +} + +func initialize(dataDirName string) { + // Ensure we have a data directory to use + if !config.DataDirExists(dataDirName) { + err := config.CreateDataDir(dataDirName) + if err != nil { + if config.Debug() { + panic(err) + } else { + log.Errorln("Error creating data directory: %s", err) + return + } + } + } +} From 834bc2a440553ba6c2a02b3fbdbacb0395221d2b Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 14:27:29 -0700 Subject: [PATCH 090/181] add helper to parse host based config dir --- config/options.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/options.go b/config/options.go index 1abf9ee..57f08cd 100644 --- a/config/options.go +++ b/config/options.go @@ -1,6 +1,8 @@ package config import ( + "net/url" + "github.com/cloudfoundry/jibber_jabber" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" @@ -54,3 +56,16 @@ func Collection(c *cli.Context) string { } return "" } + +// HostDirectory returns the sub directory string for the host flag if set +func HostDirectory(c *cli.Context) (string, error) { + if host := c.GlobalString("host"); host != "" { + u, err := url.Parse(host) + if err != nil { + return "", err // TODO + } + return u.Hostname(), nil + } + + return "", nil +} From 35292d25f547c82faac28bc08e13499616406c08 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 14:27:55 -0700 Subject: [PATCH 091/181] remove unused global flags --- cmd/wf/flags.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go index 03ed17c..dac0803 100644 --- a/cmd/wf/flags.go +++ b/cmd/wf/flags.go @@ -14,15 +14,6 @@ var globalFlags = []cli.Flag{ Usage: "Use a different port to connect to Tor", Value: 9150, }, - cli.BoolFlag{ - Name: "verbose, v", - Usage: "Make the operation more talkative", - }, - cli.StringFlag{ - Name: "user-agent", - Usage: "Sets the User-Agent for API requests", - Value: "", - }, cli.StringFlag{ Name: "host, H", Usage: "Operate against a custom hostname", From 30ecf6cc76de5b29cab1f6f1e54e5ac7181c2455 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 14:31:18 -0700 Subject: [PATCH 092/181] use global flags in writeas cli --- cmd/writeas/flags.go | 21 +++++++++++++++++++++ cmd/writeas/main.go | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 cmd/writeas/flags.go diff --git a/cmd/writeas/flags.go b/cmd/writeas/flags.go new file mode 100644 index 0000000..dac0803 --- /dev/null +++ b/cmd/writeas/flags.go @@ -0,0 +1,21 @@ +package main + +import ( + "gopkg.in/urfave/cli.v1" +) + +var globalFlags = []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Perform action on Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, + cli.StringFlag{ + Name: "host, H", + Usage: "Operate against a custom hostname", + }, +} diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index c113d19..7bdcc6e 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -31,7 +31,7 @@ func main() { return appInfo } app.Action = commands.CmdPost - app.Flags = config.PostFlags + app.Flags = globalFlags app.Commands = []cli.Command{ { Name: "post", From d6c4b2c392ad3922afee0bbaf7bc2ca44c70475a Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 15:17:30 -0700 Subject: [PATCH 093/181] store/load user with host specific sub folder only if host flag supplied for writefreely binary, writeas unaffected --- api/api.go | 4 ++-- api/sync.go | 8 ++++---- cmd/wf/main.go | 18 +----------------- cmd/writeas/main.go | 18 +----------------- commands/commands.go | 8 ++++---- config/directories.go | 26 ++++++++++++++++++++++---- config/user.go | 27 +++++++++++++++++++++++---- 7 files changed, 57 insertions(+), 52 deletions(-) diff --git a/api/api.go b/api/api.go index d3fa3fc..032d69f 100644 --- a/api/api.go +++ b/api/api.go @@ -42,7 +42,7 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { } client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, _ := config.LoadUser(c) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -235,7 +235,7 @@ func DoLogIn(c *cli.Context, username, password string) error { return err } - err = config.SaveUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]), u) + err = config.SaveUser(c, u) if err != nil { return err } diff --git a/api/sync.go b/api/sync.go index d687f24..070b5e0 100644 --- a/api/sync.go +++ b/api/sync.go @@ -25,7 +25,7 @@ func CmdPull(c *cli.Context) error { } // Create posts directory if needed if cfg.Posts.Directory == "" { - syncSetUp(c.App.ExtraInfo()["configDir"], cfg) + syncSetUp(c, cfg) } // Fetch posts @@ -79,10 +79,10 @@ func CmdPull(c *cli.Context) error { return nil } -func syncSetUp(path string, cfg *config.UserConfig) error { +func syncSetUp(c *cli.Context, cfg *config.UserConfig) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := config.LoadUser(config.UserDataDir(path)) + u, err := config.LoadUser(c) if err != nil { return err } @@ -118,7 +118,7 @@ func syncSetUp(path string, cfg *config.UserConfig) error { // Save preference cfg.Posts.Directory = dir - err = config.SaveConfig(config.UserDataDir(path), cfg) + err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) if err != nil { if config.Debug() { log.Errorln("Unable to save config: %s", err) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 8b78771..4dbbf7e 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -6,12 +6,11 @@ import ( "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" - "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) func main() { - initialize(appInfo["configDir"]) + config.DirMustExist(appInfo["configDir"]) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", Usage: "print the version", @@ -249,18 +248,3 @@ OPTIONS: ` app.Run(os.Args) } - -func initialize(dataDirName string) { - // Ensure we have a data directory to use - if !config.DataDirExists(dataDirName) { - err := config.CreateDataDir(dataDirName) - if err != nil { - if config.Debug() { - panic(err) - } else { - log.Errorln("Error creating data directory: %s", err) - return - } - } - } -} diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 7bdcc6e..111d1e5 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -5,12 +5,11 @@ import ( "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" - "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) func main() { - initialize(appInfo["configDir"]) + config.DirMustExist(appInfo["configDir"]) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", Usage: "print the version", @@ -248,18 +247,3 @@ OPTIONS: ` app.Run(os.Args) } - -func initialize(dataDirName string) { - // Ensure we have a data directory to use - if !config.DataDirExists(dataDirName) { - err := config.CreateDataDir(dataDirName) - if err != nil { - if config.Debug() { - panic(err) - } else { - log.Errorln("Error creating data directory: %s", err) - return - } - } - } -} diff --git a/commands/commands.go b/commands/commands.go index 0a4ba6a..7e51455 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -71,7 +71,7 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, _ := config.LoadUser(c) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -107,7 +107,7 @@ func CmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, _ := config.LoadUser(c) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -186,7 +186,7 @@ func CmdListPosts(c *cli.Context) error { fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) } } - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, _ := config.LoadUser(c) if u != nil { remotePosts, err := api.GetUserPosts(c) if err != nil { @@ -302,7 +302,7 @@ func CmdClaim(c *cli.Context) error { func CmdAuth(c *cli.Context) error { // Check configuration - u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, err := config.LoadUser(c) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } diff --git a/config/directories.go b/config/directories.go index 8373835..a24cc31 100644 --- a/config/directories.go +++ b/config/directories.go @@ -5,16 +5,34 @@ import ( "path/filepath" "github.com/writeas/writeas-cli/fileutils" + "github.com/writeas/writeas-cli/log" ) func UserDataDir(dataDirName string) string { return filepath.Join(parentDataDir(), dataDirName) } -func DataDirExists(dataDirName string) bool { - return fileutils.Exists(UserDataDir(dataDirName)) +func dataDirExists(dataDirName string) bool { + return fileutils.Exists(dataDirName) } -func CreateDataDir(dataDirName string) error { - return os.Mkdir(UserDataDir(dataDirName), 0700) +func createDataDir(dataDirName string) error { + return os.Mkdir(dataDirName, 0700) +} + +// DirMustExist checks for a directory, creates it if not found and either +// panics or logs and error depending on the status of Debug +func DirMustExist(dataDirName string) { + // Ensure we have a data directory to use + if !dataDirExists(dataDirName) { + err := createDataDir(dataDirName) + if err != nil { + if Debug() { + panic(err) + } else { + log.Errorln("Error creating data directory: %s", err) + return + } + } + } } diff --git a/config/user.go b/config/user.go index 21dd8f9..6722452 100644 --- a/config/user.go +++ b/config/user.go @@ -7,12 +7,17 @@ import ( writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/writeas-cli/fileutils" + "gopkg.in/urfave/cli.v1" ) const UserFile = "user.json" -func LoadUser(dataDir string) (*writeas.AuthUser, error) { - fname := filepath.Join(dataDir, UserFile) +func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { + dir, err := userHostDir(c) + if err != nil { + return nil, err + } + fname := filepath.Join(dir, UserFile) userJSON, err := ioutil.ReadFile(fname) if err != nil { if !fileutils.Exists(fname) { @@ -31,17 +36,31 @@ func LoadUser(dataDir string) (*writeas.AuthUser, error) { return u, nil } -func SaveUser(dataDir string, u *writeas.AuthUser) error { +func SaveUser(c *cli.Context, u *writeas.AuthUser) error { // Marshal struct into pretty-printed JSON userJSON, err := json.MarshalIndent(u, "", " ") if err != nil { return err } + dir, err := userHostDir(c) + if err != nil { + return err + } + DirMustExist(dir) // Save file - err = ioutil.WriteFile(filepath.Join(dataDir, UserFile), userJSON, 0600) + err = ioutil.WriteFile(filepath.Join(dir, UserFile), userJSON, 0600) if err != nil { return err } return nil } + +func userHostDir(c *cli.Context) (string, error) { + dataDir := UserDataDir(c.App.ExtraInfo()["configDir"]) + hostDir, err := HostDirectory(c) + if err != nil { + return "", err + } + return filepath.Join(dataDir, hostDir), nil +} From 79aae3105274948d799938b716099193e5c2d836 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 7 Jun 2019 15:30:27 -0700 Subject: [PATCH 094/181] delete host specific user when doing logout --- api/api.go | 11 ++--------- config/directories.go | 2 ++ config/user.go | 11 +++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/api/api.go b/api/api.go index 032d69f..f6cd7c2 100644 --- a/api/api.go +++ b/api/api.go @@ -2,13 +2,11 @@ package api import ( "fmt" - "path/filepath" "github.com/atotto/clipboard" writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/config" - "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -257,11 +255,6 @@ func DoLogOut(c *cli.Context) error { return err } - // Delete local user data - err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), config.UserFile)) - if err != nil { - return err - } - - return nil + // delete local user file + return config.DeleteUser(c) } diff --git a/config/directories.go b/config/directories.go index a24cc31..e2a68f3 100644 --- a/config/directories.go +++ b/config/directories.go @@ -8,6 +8,8 @@ import ( "github.com/writeas/writeas-cli/log" ) +// UserDataDir returns a platform specific directory under the user's home +// directory func UserDataDir(dataDirName string) string { return filepath.Join(parentDataDir(), dataDirName) } diff --git a/config/user.go b/config/user.go index 6722452..52dd972 100644 --- a/config/user.go +++ b/config/user.go @@ -36,6 +36,15 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { return u, nil } +func DeleteUser(c *cli.Context) error { + dir, err := userHostDir(c) + if err != nil { + return err + } + + return fileutils.DeleteFile(filepath.Join(dir, UserFile)) +} + func SaveUser(c *cli.Context, u *writeas.AuthUser) error { // Marshal struct into pretty-printed JSON userJSON, err := json.MarshalIndent(u, "", " ") @@ -56,6 +65,8 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { return nil } +// userHostDir returns the path to the user data directory with the host based +// subpath if the host flag is set func userHostDir(c *cli.Context) (string, error) { dataDir := UserDataDir(c.App.ExtraInfo()["configDir"]) hostDir, err := HostDirectory(c) From be060d7ed1c3286cef05f3815e687f5ecf3180e2 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 11:12:14 -0700 Subject: [PATCH 095/181] fix bug with creating empty dir in working dir some changes resulted in the data directory initialization creating the .writeas/.writefreely directory in the current working directory. --- cmd/wf/main.go | 2 +- cmd/writeas/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 4dbbf7e..a942e5c 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - config.DirMustExist(appInfo["configDir"]) + config.DirMustExist(config.UserDataDir(appInfo["configDir"])) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", Usage: "print the version", diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 111d1e5..10750b4 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - config.DirMustExist(appInfo["configDir"]) + config.DirMustExist(config.UserDataDir(appInfo["configDir"])) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", Usage: "print the version", From 33578e83e95680b9369a05cb1c19a1c95e9e347b Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 10 Jun 2019 11:21:58 -0700 Subject: [PATCH 096/181] start multi user authentication - adds new user/u flag to wf - load and save user file based on username - removed host flag from writeas - adds hidden global flag for user to writeas to maintain compatibility --- api/api.go | 4 ++-- api/sync.go | 2 +- cmd/wf/flags.go | 4 ++++ cmd/writeas/flags.go | 5 +++-- commands/commands.go | 10 +++++----- config/user.go | 12 +++++------- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/api/api.go b/api/api.go index f6cd7c2..50553bb 100644 --- a/api/api.go +++ b/api/api.go @@ -40,7 +40,7 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { } client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app - u, _ := config.LoadUser(c) + u, _ := config.LoadUser(c, c.GlobalString("user")) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -256,5 +256,5 @@ func DoLogOut(c *cli.Context) error { } // delete local user file - return config.DeleteUser(c) + return config.DeleteUser(c, c.GlobalString("user")) } diff --git a/api/sync.go b/api/sync.go index 070b5e0..3736729 100644 --- a/api/sync.go +++ b/api/sync.go @@ -82,7 +82,7 @@ func CmdPull(c *cli.Context) error { func syncSetUp(c *cli.Context, cfg *config.UserConfig) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := config.LoadUser(c) + u, err := config.LoadUser(c, c.GlobalString("user")) if err != nil { return err } diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go index dac0803..854b1f8 100644 --- a/cmd/wf/flags.go +++ b/cmd/wf/flags.go @@ -18,4 +18,8 @@ var globalFlags = []cli.Flag{ Name: "host, H", Usage: "Operate against a custom hostname", }, + cli.StringFlag{ + Name: "user, u", + Usage: "Use authenticated user, other than default", + }, } diff --git a/cmd/writeas/flags.go b/cmd/writeas/flags.go index dac0803..f19f830 100644 --- a/cmd/writeas/flags.go +++ b/cmd/writeas/flags.go @@ -15,7 +15,8 @@ var globalFlags = []cli.Flag{ Value: 9150, }, cli.StringFlag{ - Name: "host, H", - Usage: "Operate against a custom hostname", + Name: "user, u", + Hidden: true, + Value: "user", }, } diff --git a/commands/commands.go b/commands/commands.go index 7e51455..f67e673 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -71,7 +71,7 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := config.LoadUser(c) + u, _ := config.LoadUser(c, c.GlobalString("user")) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -107,7 +107,7 @@ func CmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := config.LoadUser(c) + u, _ := config.LoadUser(c, c.GlobalString("user")) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -186,7 +186,7 @@ func CmdListPosts(c *cli.Context) error { fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) } } - u, _ := config.LoadUser(c) + u, _ := config.LoadUser(c, c.GlobalString("user")) if u != nil { remotePosts, err := api.GetUserPosts(c) if err != nil { @@ -229,7 +229,7 @@ func getPostURL(c *cli.Context, slug string) string { } func CmdCollections(c *cli.Context) error { - u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, err := config.LoadUser(c, c.GlobalString("user")) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } @@ -302,7 +302,7 @@ func CmdClaim(c *cli.Context) error { func CmdAuth(c *cli.Context) error { // Check configuration - u, err := config.LoadUser(c) + u, err := config.LoadUser(c, c.GlobalString("user")) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } diff --git a/config/user.go b/config/user.go index 52dd972..89cd9f7 100644 --- a/config/user.go +++ b/config/user.go @@ -10,14 +10,12 @@ import ( "gopkg.in/urfave/cli.v1" ) -const UserFile = "user.json" - -func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { +func LoadUser(c *cli.Context, username string) (*writeas.AuthUser, error) { dir, err := userHostDir(c) if err != nil { return nil, err } - fname := filepath.Join(dir, UserFile) + fname := filepath.Join(dir, username+".json") userJSON, err := ioutil.ReadFile(fname) if err != nil { if !fileutils.Exists(fname) { @@ -36,13 +34,13 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { return u, nil } -func DeleteUser(c *cli.Context) error { +func DeleteUser(c *cli.Context, username string) error { dir, err := userHostDir(c) if err != nil { return err } - return fileutils.DeleteFile(filepath.Join(dir, UserFile)) + return fileutils.DeleteFile(filepath.Join(dir, username+".json")) } func SaveUser(c *cli.Context, u *writeas.AuthUser) error { @@ -58,7 +56,7 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { } DirMustExist(dir) // Save file - err = ioutil.WriteFile(filepath.Join(dir, UserFile), userJSON, 0600) + err = ioutil.WriteFile(filepath.Join(dir, u.User.Username+".json"), userJSON, 0600) if err != nil { return err } From 15f71e3714e36293895f73751323f84f0a54d309 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 10:57:27 -0700 Subject: [PATCH 097/181] multiple user authentication - now allows authentication with more than one user per host, stored as username.json inside the [host] directory. - supports a default user and host in config.ini - global flags will override the default --- api/api.go | 4 ++-- api/sync.go | 4 ++-- commands/commands.go | 11 ++++++----- config/config.go | 29 ++++++++++++++++++++--------- config/options.go | 20 +++++++++++++++----- config/user.go | 26 ++++++++++++++++++++++++-- 6 files changed, 69 insertions(+), 25 deletions(-) diff --git a/api/api.go b/api/api.go index 50553bb..f6cd7c2 100644 --- a/api/api.go +++ b/api/api.go @@ -40,7 +40,7 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { } client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app - u, _ := config.LoadUser(c, c.GlobalString("user")) + u, _ := config.LoadUser(c) if u != nil { client.SetToken(u.AccessToken) } else if authRequired { @@ -256,5 +256,5 @@ func DoLogOut(c *cli.Context) error { } // delete local user file - return config.DeleteUser(c, c.GlobalString("user")) + return config.DeleteUser(c) } diff --git a/api/sync.go b/api/sync.go index 3736729..dec2794 100644 --- a/api/sync.go +++ b/api/sync.go @@ -79,10 +79,10 @@ func CmdPull(c *cli.Context) error { return nil } -func syncSetUp(c *cli.Context, cfg *config.UserConfig) error { +func syncSetUp(c *cli.Context, cfg *config.Config) error { // Get user information and fail early (before we make the user do // anything), if we're going to - u, err := config.LoadUser(c, c.GlobalString("user")) + u, err := config.LoadUser(c) if err != nil { return err } diff --git a/commands/commands.go b/commands/commands.go index f67e673..de193fc 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -71,7 +71,7 @@ func CmdDelete(c *cli.Context) error { return cli.NewExitError("usage: writeas delete []", 1) } - u, _ := config.LoadUser(c, c.GlobalString("user")) + u, _ := config.LoadUser(c) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -107,7 +107,7 @@ func CmdUpdate(c *cli.Context) error { return cli.NewExitError("usage: writeas update []", 1) } - u, _ := config.LoadUser(c, c.GlobalString("user")) + u, _ := config.LoadUser(c) if token == "" { // Search for the token locally token = api.TokenFromID(c, friendlyID) @@ -186,7 +186,7 @@ func CmdListPosts(c *cli.Context) error { fmt.Fprintf(tw, "unsynced\t%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) } } - u, _ := config.LoadUser(c, c.GlobalString("user")) + u, _ := config.LoadUser(c) if u != nil { remotePosts, err := api.GetUserPosts(c) if err != nil { @@ -229,7 +229,7 @@ func getPostURL(c *cli.Context, slug string) string { } func CmdCollections(c *cli.Context) error { - u, err := config.LoadUser(c, c.GlobalString("user")) + u, err := config.LoadUser(c) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } @@ -302,7 +302,7 @@ func CmdClaim(c *cli.Context) error { func CmdAuth(c *cli.Context) error { // Check configuration - u, err := config.LoadUser(c, c.GlobalString("user")) + u, err := config.LoadUser(c) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } @@ -311,6 +311,7 @@ func CmdAuth(c *cli.Context) error { } // Validate arguments and get password + // TODO: after global config, check for default user username := c.Args().Get(0) if username == "" { return cli.NewExitError("usage: writeas auth ", 1) diff --git a/config/config.go b/config/config.go index 4c9c59d..6ded22b 100644 --- a/config/config.go +++ b/config/config.go @@ -8,32 +8,43 @@ import ( ) const ( - UserConfigFile = "config.ini" + // ConfigFile is the full filename for application configuration files + ConfigFile = "config.ini" ) type ( + // APIConfig is not currently used APIConfig struct { } + // PostsConfig stores the directory for the user post cache PostsConfig struct { Directory string `ini:"directory"` } - UserConfig struct { - API APIConfig `ini:"api"` - Posts PostsConfig `ini:"posts"` + // DefaultConfig stores the default host and user to authenticate with + DefaultConfig struct { + Host string `ini:"host"` + User string `ini:"user"` + } + + // Config represents the entire base configuration + Config struct { + API APIConfig `ini:"api"` + Default DefaultConfig `ini:"default"` + Posts PostsConfig `ini:"posts"` } ) -func LoadConfig(dataDir string) (*UserConfig, error) { +func LoadConfig(dataDir string) (*Config, error) { // TODO: load config to var shared across app - cfg, err := ini.LooseLoad(filepath.Join(dataDir, UserConfigFile)) + cfg, err := ini.LooseLoad(filepath.Join(dataDir, ConfigFile)) if err != nil { return nil, err } // Parse INI file - uc := &UserConfig{} + uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err @@ -41,14 +52,14 @@ func LoadConfig(dataDir string) (*UserConfig, error) { return uc, nil } -func SaveConfig(dataDir string, uc *UserConfig) error { +func SaveConfig(dataDir string, uc *Config) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } - return cfg.SaveTo(filepath.Join(dataDir, UserConfigFile)) + return cfg.SaveTo(filepath.Join(dataDir, ConfigFile)) } var editors = []string{"WRITEAS_EDITOR", "EDITOR"} diff --git a/config/options.go b/config/options.go index 57f08cd..78d3335 100644 --- a/config/options.go +++ b/config/options.go @@ -57,15 +57,25 @@ func Collection(c *cli.Context) string { return "" } -// HostDirectory returns the sub directory string for the host flag if set +// HostDirectory returns the sub directory string for the host. Order of +// precedence is a host flag if any, then the configured default, if any func HostDirectory(c *cli.Context) (string, error) { - if host := c.GlobalString("host"); host != "" { - u, err := url.Parse(host) + cfg, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return "", err + } + // flag takes precedence over defaults + if hostFlag := c.GlobalString("host"); hostFlag != "" { + u, err := url.Parse(hostFlag) if err != nil { - return "", err // TODO + return "", err } return u.Hostname(), nil } - return "", nil + u, err := url.Parse(cfg.Default.Host) + if err != nil { + return "", err + } + return u.Hostname(), nil } diff --git a/config/user.go b/config/user.go index 89cd9f7..1ba9b15 100644 --- a/config/user.go +++ b/config/user.go @@ -10,11 +10,15 @@ import ( "gopkg.in/urfave/cli.v1" ) -func LoadUser(c *cli.Context, username string) (*writeas.AuthUser, error) { +func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { dir, err := userHostDir(c) if err != nil { return nil, err } + username, err := currentUser(c) + if err != nil { + return nil, err + } fname := filepath.Join(dir, username+".json") userJSON, err := ioutil.ReadFile(fname) if err != nil { @@ -34,12 +38,17 @@ func LoadUser(c *cli.Context, username string) (*writeas.AuthUser, error) { return u, nil } -func DeleteUser(c *cli.Context, username string) error { +func DeleteUser(c *cli.Context) error { dir, err := userHostDir(c) if err != nil { return err } + username, err := currentUser(c) + if err != nil { + return err + } + return fileutils.DeleteFile(filepath.Join(dir, username+".json")) } @@ -73,3 +82,16 @@ func userHostDir(c *cli.Context) (string, error) { } return filepath.Join(dataDir, hostDir), nil } + +func currentUser(c *cli.Context) (string, error) { + cfg, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return "", err + } + + if c.GlobalString("user") != "" { + return c.GlobalString("user"), nil + } + + return cfg.Default.User, nil +} From 20919fbe0ddebc43fa805c2e9a9d2f37c26d3371 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 12:14:32 -0700 Subject: [PATCH 098/181] support any writefreely instance full support, auth and actions working by use of flags or defaults maintains backwards compatibility with write.as --- api/api.go | 66 +++++++++++++++++++++++++++++++------------- api/tor.go | 1 + commands/commands.go | 2 +- config/user.go | 9 +++++- 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/api/api.go b/api/api.go index f6cd7c2..4540182 100644 --- a/api/api.go +++ b/api/api.go @@ -11,33 +11,55 @@ import ( cli "gopkg.in/urfave/cli.v1" ) -func client(userAgent string, tor bool) *writeas.Client { +func client(c *cli.Context, userAgent string, tor bool) (*writeas.Client, error) { var client *writeas.Client - if tor { - client = writeas.NewTorClient(TorPort) + var clientConfig writeas.Config + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return nil, fmt.Errorf("Failed to load configuration file: %v", err) + } + if c.GlobalString("host") != "" { + clientConfig.URL = c.GlobalString("host") + "/api" + } else if cfg.Default.Host != "" { + clientConfig.URL = cfg.Default.Host + "/api" + } else if config.IsDev() { + clientConfig.URL = config.DevBaseURL + "/api" } else { - if config.IsDev() { - client = writeas.NewDevClient() - } else { - client = writeas.NewClient() - } + clientConfig.URL = config.WriteasBaseURL + "/api" + } + if tor { + clientConfig.URL = config.TorBaseURL + clientConfig.TorPort = TorPort } + + client = writeas.NewClientWith(clientConfig) client.UserAgent = userAgent - return client + return client, nil } func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { var client *writeas.Client - if config.IsTor(c) { - client = writeas.NewTorClient(TorPort) + var clientConfig writeas.Config + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return nil, fmt.Errorf("Failed to load configuration file: %v", err) + } + if c.GlobalString("host") != "" { + clientConfig.URL = c.GlobalString("host") + "/api" + } else if cfg.Default.Host != "" { + clientConfig.URL = cfg.Default.Host + "/api" + } else if config.IsDev() { + clientConfig.URL = config.DevBaseURL + "/api" } else { - if config.IsDev() { - client = writeas.NewDevClient() - } else { - client = writeas.NewClient() - } + clientConfig.URL = config.WriteasBaseURL + "/api" + } + if config.IsTor(c) { + clientConfig.URL = config.TorBaseURL + clientConfig.TorPort = TorPort } + + client = writeas.NewClientWith(clientConfig) client.UserAgent = config.UserAgent(c) // TODO: load user into var shared across the app u, _ := config.LoadUser(c) @@ -52,8 +74,11 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. -func DoFetch(friendlyID, ua string, tor bool) error { - cl := client(ua, tor) +func DoFetch(c *cli.Context, friendlyID, ua string, tor bool) error { + cl, err := client(c, ua, tor) + if err != nil { + return err + } p, err := cl.GetPost(friendlyID) if err != nil { @@ -223,7 +248,10 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { } func DoLogIn(c *cli.Context, username, password string) error { - cl := client(config.UserAgent(c), config.IsTor(c)) + cl, err := client(c, config.UserAgent(c), config.IsTor(c)) + if err != nil { + return err + } u, err := cl.LogIn(username, password) if err != nil { diff --git a/api/tor.go b/api/tor.go index dae5be5..d181b7f 100644 --- a/api/tor.go +++ b/api/tor.go @@ -11,6 +11,7 @@ var ( TorPort = 9150 ) +// TODO: never used? func torClient() *http.Client { dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", TorPort)) transport := &http.Transport{Dial: dialSocksProxy} diff --git a/commands/commands.go b/commands/commands.go index de193fc..a14bd69 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -149,7 +149,7 @@ func CmdGet(c *cli.Context) error { log.Info(c, "Getting...") } - return api.DoFetch(friendlyID, config.UserAgent(c), tor) + return api.DoFetch(c, friendlyID, config.UserAgent(c), tor) } func CmdAdd(c *cli.Context) error { diff --git a/config/user.go b/config/user.go index 1ba9b15..359fcb4 100644 --- a/config/user.go +++ b/config/user.go @@ -65,7 +65,14 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { } DirMustExist(dir) // Save file - err = ioutil.WriteFile(filepath.Join(dir, u.User.Username+".json"), userJSON, 0600) + username, err := currentUser(c) + if err != nil { + return err + } + if username != "user" { + username = u.User.Username + } + err = ioutil.WriteFile(filepath.Join(dir, username+".json"), userJSON, 0600) if err != nil { return err } From 5855fd31813c223dd322b04df9945b2c4f5ffa11 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 14:27:57 -0700 Subject: [PATCH 099/181] wf: remove fetch and add blogs cmd this get's the wf binary up to date with the writeas functionality --- cmd/wf/main.go | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index a942e5c..87e74dc 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -3,7 +3,6 @@ package main import ( "os" - "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" cli "gopkg.in/urfave/cli.v1" @@ -170,28 +169,17 @@ func main() { Usage: "Show list with URLs", }, }, - }, - { - Name: "fetch", - Usage: "Fetch authenticated user's Write.as posts", - Action: api.CmdPull, + }, { + Name: "blogs", + Usage: "List blogs", + Action: commands.CmdCollections, Flags: []cli.Flag{ cli.BoolFlag{ - Name: "tor, t", - Usage: "Authenticate via Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, - cli.BoolFlag{ - Name: "verbose, v", - Usage: "Make the operation more talkative", + Name: "url", + Usage: "Show list with URLs", }, }, - }, - { + }, { Name: "auth", Usage: "Authenticate with Write.as", Action: commands.CmdAuth, From d10b3ee85a15974e9581e53da57e80e1d7aa4e71 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 14:28:50 -0700 Subject: [PATCH 100/181] store user.json in user sub folder only on wf, writeas still stores in root user config directory --- config/user.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/config/user.go b/config/user.go index 359fcb4..d47a211 100644 --- a/config/user.go +++ b/config/user.go @@ -19,7 +19,10 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { if err != nil { return nil, err } - fname := filepath.Join(dir, username+".json") + if username == "user" { + username = "" + } + fname := filepath.Join(dir, username, "user.json") userJSON, err := ioutil.ReadFile(fname) if err != nil { if !fileutils.Exists(fname) { @@ -49,7 +52,11 @@ func DeleteUser(c *cli.Context) error { return err } - return fileutils.DeleteFile(filepath.Join(dir, username+".json")) + if username == "user" { + username = "" + } + + return fileutils.DeleteFile(filepath.Join(dir, username, "user.json")) } func SaveUser(c *cli.Context, u *writeas.AuthUser) error { @@ -63,16 +70,16 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { if err != nil { return err } - DirMustExist(dir) // Save file username, err := currentUser(c) if err != nil { return err } if username != "user" { - username = u.User.Username + dir = filepath.Join(dir, u.User.Username) } - err = ioutil.WriteFile(filepath.Join(dir, username+".json"), userJSON, 0600) + DirMustExist(dir) + err = ioutil.WriteFile(filepath.Join(dir, "user.json"), userJSON, 0600) if err != nil { return err } @@ -91,7 +98,11 @@ func userHostDir(c *cli.Context) (string, error) { } func currentUser(c *cli.Context) (string, error) { - cfg, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) + hostDir, err := userHostDir(c) + if err != nil { + return "", err + } + cfg, err := LoadConfig(hostDir) if err != nil { return "", err } From ea3e57a2a782f58cb38a5b79d0c71206f915d66f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 15:16:50 -0700 Subject: [PATCH 101/181] bugfix: default user should work on other domains there was a bug where a default user at the config directory root was not being used for calls not including a flag or host level config.ini --- config/user.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/user.go b/config/user.go index d47a211..af9c710 100644 --- a/config/user.go +++ b/config/user.go @@ -106,6 +106,12 @@ func currentUser(c *cli.Context) (string, error) { if err != nil { return "", err } + if cfg.Default.User == "" { + cfg, err = LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return "", err + } + } if c.GlobalString("user") != "" { return c.GlobalString("user"), nil From 572044043a6c569f21cd81c9e2a2417e549ae0a6 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 15:41:58 -0700 Subject: [PATCH 102/181] include new claim cmd in wf also update CmdClaim for new LoadUser signiture --- cmd/wf/main.go | 11 +++++++++++ commands/commands.go | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 87e74dc..3ede8a6 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -179,6 +179,17 @@ func main() { Usage: "Show list with URLs", }, }, + }, { + Name: "claim", + Usage: "Claim local unsynced posts", + Action: commands.CmdClaim, + Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, }, { Name: "auth", Usage: "Authenticate with Write.as", diff --git a/commands/commands.go b/commands/commands.go index a14bd69..2c2b2da 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -259,7 +259,7 @@ func CmdCollections(c *cli.Context) error { } func CmdClaim(c *cli.Context) error { - u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, err := config.LoadUser(c) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } From 9266f6622107f77a9e592e2b2b5111af0c70ff70 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 16:26:32 -0700 Subject: [PATCH 103/181] revert global flag change I had changed the global flags variable to only be those that are global this broke some compatibility with piping from stdout into the binary. also: - binary specific configuration details have moved into the same map but in main.go. only the configDir is OS dependent. - a new key in the map is for the version so each binary can have their own --- cmd/wf/config_nix.go | 4 +--- cmd/wf/config_win.go | 4 +--- cmd/wf/flags.go | 11 +---------- cmd/wf/main.go | 8 ++++++-- cmd/writeas/config_nix.go | 4 +--- cmd/writeas/config_win.go | 4 +--- cmd/writeas/flags.go | 11 +---------- cmd/writeas/main.go | 8 ++++++-- config/options.go | 7 +++---- 9 files changed, 21 insertions(+), 40 deletions(-) diff --git a/cmd/wf/config_nix.go b/cmd/wf/config_nix.go index 4abd334..ddc1bff 100644 --- a/cmd/wf/config_nix.go +++ b/cmd/wf/config_nix.go @@ -2,6 +2,4 @@ package main -var appInfo = map[string]string{ - "configDir": ".writefreely", -} +const configDir = ".writefreely" diff --git a/cmd/wf/config_win.go b/cmd/wf/config_win.go index e44b45b..1673fa1 100644 --- a/cmd/wf/config_win.go +++ b/cmd/wf/config_win.go @@ -2,6 +2,4 @@ package main -var appInfo = map[string]string{ - "configDir": "WriteFreely", -} +const configDir = "WriteFreely" diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go index 854b1f8..5245bb7 100644 --- a/cmd/wf/flags.go +++ b/cmd/wf/flags.go @@ -4,16 +4,7 @@ import ( "gopkg.in/urfave/cli.v1" ) -var globalFlags = []cli.Flag{ - cli.BoolFlag{ - Name: "tor, t", - Usage: "Perform action on Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, +var flags = []cli.Flag{ cli.StringFlag{ Name: "host, H", Usage: "Operate against a custom hostname", diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 3ede8a6..31fc450 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -9,6 +9,10 @@ import ( ) func main() { + appInfo := map[string]string{ + "configDir": configDir, + "version": "1.0", + } config.DirMustExist(config.UserDataDir(appInfo["configDir"])) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", @@ -18,7 +22,7 @@ func main() { // Run the app app := cli.NewApp() app.Name = "wf" - app.Version = config.Version + app.Version = appInfo["version"] app.Usage = "Publish text quickly" // TODO: who is the author? the contributors? link to GH? app.Authors = []cli.Author{ @@ -31,7 +35,7 @@ func main() { return appInfo } app.Action = commands.CmdPost - app.Flags = globalFlags + app.Flags = append(config.PostFlags, flags...) app.Commands = []cli.Command{ { Name: "post", diff --git a/cmd/writeas/config_nix.go b/cmd/writeas/config_nix.go index 6b3cb86..6c0ed02 100644 --- a/cmd/writeas/config_nix.go +++ b/cmd/writeas/config_nix.go @@ -2,6 +2,4 @@ package main -var appInfo = map[string]string{ - "configDir": ".writeas", -} +const configDir = ".writeas" diff --git a/cmd/writeas/config_win.go b/cmd/writeas/config_win.go index 9a7eea1..43d2bca 100644 --- a/cmd/writeas/config_win.go +++ b/cmd/writeas/config_win.go @@ -2,6 +2,4 @@ package main -var appInfo = map[string]string{ - "configDir": "Write.as", -} +const configDir = "Write.as" diff --git a/cmd/writeas/flags.go b/cmd/writeas/flags.go index f19f830..5fdaf8a 100644 --- a/cmd/writeas/flags.go +++ b/cmd/writeas/flags.go @@ -4,16 +4,7 @@ import ( "gopkg.in/urfave/cli.v1" ) -var globalFlags = []cli.Flag{ - cli.BoolFlag{ - Name: "tor, t", - Usage: "Perform action on Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, +var flags = []cli.Flag{ cli.StringFlag{ Name: "user, u", Hidden: true, diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 10750b4..d94dfca 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -9,6 +9,10 @@ import ( ) func main() { + appInfo := map[string]string{ + "configDir": configDir, + "version": "2.0", + } config.DirMustExist(config.UserDataDir(appInfo["configDir"])) cli.VersionFlag = cli.BoolFlag{ Name: "version, V", @@ -18,7 +22,7 @@ func main() { // Run the app app := cli.NewApp() app.Name = "writeas" - app.Version = config.Version + app.Version = appInfo["version"] app.Usage = "Publish text quickly" app.Authors = []cli.Author{ { @@ -30,7 +34,7 @@ func main() { return appInfo } app.Action = commands.CmdPost - app.Flags = globalFlags + app.Flags = append(config.PostFlags, flags...) app.Commands = []cli.Command{ { Name: "post", diff --git a/config/options.go b/config/options.go index 78d3335..5bd0a18 100644 --- a/config/options.go +++ b/config/options.go @@ -10,8 +10,7 @@ import ( // Application constants. const ( - Version = "2.0" - defaultUserAgent = "writeas-cli v" + Version + defaultUserAgent = "writeas-cli v" // Defaults for posts on Write.as. DefaultFont = PostFontMono WriteasBaseURL = "https://write.as" @@ -22,9 +21,9 @@ const ( func UserAgent(c *cli.Context) string { ua := c.String("user-agent") if ua == "" { - return defaultUserAgent + return defaultUserAgent + c.App.ExtraInfo()["version"] } - return ua + " (" + defaultUserAgent + ")" + return ua + " (" + defaultUserAgent + c.App.ExtraInfo()["version"] + ")" } func IsTor(c *cli.Context) bool { From d7e477a22a56e4ef848ba748920fe1b1e4a0701a Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 16:32:48 -0700 Subject: [PATCH 104/181] include new claim cmd and host/user flag in docs --- GUIDE.md | 27 ++++++++++++++++----------- README.md | 26 +++++++++++++++----------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 864ae45..24f5e26 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -24,24 +24,29 @@ COMMANDS: posts List all of your posts claim Claim local unsynced posts blogs List blogs + claim Claim local unsynced posts auth Authenticate with Write.as logout Log out of Write.as help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - -c value, -b value Optional blog to post to - --tor, -t Perform action on Tor hidden service - --tor-port value Use a different port to connect to Tor (default: 9150) - --code Specifies this post is code - --md Returns post URL with Markdown enabled - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --lang value Sets post language to given ISO 639-1 language code - --user-agent value Sets the User-Agent for API requests - --help, -h show help - --version, -V print the version + -c value, -b value Optional blog to post to + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --md Returns post URL with Markdown enabled + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --host value, -H value Operate against a custom hostname + --user value, -u value Use authenticated user, other than default + --help, -h show help + --version, -V print the version ``` +> Note: the host and user flags are only available in `wf` the community edition + #### Share something By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard: diff --git a/README.md b/README.md index 65841f4..e648af0 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,23 @@ COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - -c value, -b value Optional blog to post to - --tor, -t Perform action on Tor hidden service - --tor-port value Use a different port to connect to Tor (default: 9150) - --code Specifies this post is code - --md Returns post URL with Markdown enabled - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --lang value Sets post language to given ISO 639-1 language code - --user-agent value Sets the User-Agent for API requests - --help, -h show help - --version, -V print the version + -c value, -b value Optional blog to post to + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --md Returns post URL with Markdown enabled + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --host value, -H value Operate against a custom hostname + --user value, -u value Use authenticated user, other than default + --help, -h show help + --version, -V print the version ``` +> Note: the host and user flags are only available in `wf` the community edition + ## Contributing to the CLI For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). From 844ce0708ecf0a2ad674ab913898fc4d5b9ba0fb Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 11 Jun 2019 17:55:17 -0700 Subject: [PATCH 105/181] local posts should use host dir also CmdPost should return an exit error if there is one --- api/api.go | 14 +++++++++++--- api/posts.go | 18 +++++++++++++----- commands/commands.go | 7 +++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/api/api.go b/api/api.go index 4540182..f4999b1 100644 --- a/api/api.go +++ b/api/api.go @@ -126,14 +126,22 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( return nil, fmt.Errorf("Unable to post: %v", err) } + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return nil, fmt.Errorf("Couldn't check for config file: %v", err) + } var url string if p.Collection != nil { url = p.Collection.URL + p.Slug } else { - if tor { - url = config.TorBaseURL + if c.GlobalString("host") != "" { + url = c.GlobalString("host") + } else if cfg.Default.Host != "" { + url = cfg.Default.Host } else if config.IsDev() { url = config.DevBaseURL + } else if tor { + url = config.TorBaseURL } else { url = config.WriteasBaseURL } @@ -242,7 +250,7 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { } else { log.Info(c, "Post deleted.") } - RemovePost(c.App.ExtraInfo()["configDir"], friendlyID) + RemovePost(c, friendlyID) return nil } diff --git a/api/posts.go b/api/posts.go index 379a2c6..cef808f 100644 --- a/api/posts.go +++ b/api/posts.go @@ -42,7 +42,11 @@ type RemotePost struct { } func AddPost(c *cli.Context, id, token string) error { - f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) + hostDir, err := config.HostDirectory(c) + if err != nil { + return fmt.Errorf("Error checking for host directory: %v", err) + } + f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("Error creating local posts list: %s", err) } @@ -76,7 +80,8 @@ func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, } func TokenFromID(c *cli.Context, id string) string { - post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id) + hostDir, _ := config.HostDirectory(c) + post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), id) if post == "" { return "" } @@ -89,12 +94,15 @@ func TokenFromID(c *cli.Context, id string) string { return parts[1] } -func RemovePost(path, id string) { - fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id) +func RemovePost(c *cli.Context, id string) { + hostDir, _ := config.HostDirectory(c) + fullPath := filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile) + fileutils.RemoveLine(fullPath, id) } func GetPosts(c *cli.Context) *[]Post { - lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile)) + hostDir, _ := config.HostDirectory(c) + lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile)) posts := []Post{} diff --git a/commands/commands.go b/commands/commands.go index 2c2b2da..dff931b 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -15,7 +15,10 @@ import ( func CmdPost(c *cli.Context) error { _, err := api.HandlePost(api.ReadStdIn(), c) - return err + if err != nil { + cli.NewExitError(fmt.Sprintf("Could not post: %v", err), 1) + } + return nil } func CmdNew(c *cli.Context) error { @@ -293,7 +296,7 @@ func CmdClaim(c *cli.Context) error { log.Info(c, "%sOK", status) okCount++ // only delete local if successful - api.RemovePost(c.App.ExtraInfo()["configDir"], id) + api.RemovePost(c, id) } } log.Info(c, "%d claimed, %d failed", okCount, errCount) From 2412d21fcecc751453bfce163d063b326d0f38d1 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 12 Jun 2019 19:58:02 -0700 Subject: [PATCH 106/181] use v2.0.1 go-writeas for inclusion of collection in post parameters --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 346f6ef..ed00dac 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - github.com/writeas/go-writeas/v2 v2.0.0 + github.com/writeas/go-writeas/v2 v2.0.1 github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect diff --git a/go.sum b/go.sum index d135529..72edf90 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpke github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/writeas/go-writeas/v2 v2.0.0 h1:KjDI5bQSAIH0IzkKW3uGoY98I1A4DrBsSqBklgyOvHw= github.com/writeas/go-writeas/v2 v2.0.0/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= +github.com/writeas/go-writeas/v2 v2.0.1 h1:2ptcSFARmmiQh6/3Bj3dAMAvSPgntf+k2IJwV9CxpSU= +github.com/writeas/go-writeas/v2 v2.0.1/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= From 3f8b0d0c6e03db25fa8c438d64d8dea4f8f6794e Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 12 Jun 2019 20:06:42 -0700 Subject: [PATCH 107/181] default to user collection new posts, when no flag is specified for blog or collection, will now default to the user collection. that is the username. --- config/options.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/options.go b/config/options.go index 5bd0a18..aed4c9a 100644 --- a/config/options.go +++ b/config/options.go @@ -53,6 +53,10 @@ func Collection(c *cli.Context) string { if coll := c.String("b"); coll != "" { return coll } + u, _ := LoadUser(c) + if u != nil { + return u.User.Username + } return "" } From c7bff5e86b1e3db4f1e5920f1d7ba2042db16cf4 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 13 Jun 2019 10:17:24 -0700 Subject: [PATCH 108/181] revert to go-writeas v2.0.0 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ed00dac..346f6ef 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - github.com/writeas/go-writeas/v2 v2.0.1 + github.com/writeas/go-writeas/v2 v2.0.0 github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect From 73501ae46e0b77041611358c857b21eb83c35d7a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Jun 2019 09:31:53 -0400 Subject: [PATCH 109/181] Keep new post listing style under -v flag --- GUIDE.md | 3 ++- commands/commands.go | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 422cb00..5ed6e63 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -100,7 +100,8 @@ $ writeas posts -url https://write.as/aaaazzzzzzzza $ writeas posts -v -aaaazzzzzzzza | dhuieoj23894jhf984hdfs9834hdf84j +ID Token +aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j ``` #### Delete a post diff --git a/commands/commands.go b/commands/commands.go index e44b419..8627411 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -169,10 +169,31 @@ func CmdListPosts(c *cli.Context) error { details := c.Bool("v") posts := api.GetPosts(c) + + if details { + var p api.Post + tw := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', tabwriter.TabIndent) + numPosts := len(*posts) + if ids || !urls && numPosts != 0 { + fmt.Fprintf(tw, "%s\t%s\t\n", "ID", "Token") + } else if numPosts != 0 { + fmt.Fprintf(tw, "%s\t%s\t\n", "URL", "Token") + } else { + fmt.Fprintf(tw, "No local posts found\n") + } + for i := range *posts { + p = (*posts)[numPosts-1-i] + if ids || !urls { + fmt.Fprintf(tw, "%s\t%s\t\n", p.ID, p.EditToken) + } else { + fmt.Fprintf(tw, "%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken) + } + } + return tw.Flush() + } + for _, p := range *posts { - if details { - fmt.Printf("%s | %s\n", p.ID, p.EditToken) - } else if ids || !urls { + if ids || !urls { fmt.Printf("%s\n", p.ID) } else { fmt.Printf("%s\n", getPostURL(c, p.ID)) From d257ff0070b24ce18dd2c50e89ac29c181b82841 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 20 Jun 2019 13:46:59 -0700 Subject: [PATCH 110/181] add tor options to claim and blogs cmds both commands make network calls but do not accept flags for tor or the tor port --- cmd/writeas/main.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index 06d8af5..b9ec4ba 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -178,6 +178,15 @@ func main() { Usage: "List blogs", Action: commands.CmdCollections, Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, cli.BoolFlag{ Name: "url", Usage: "Show list with URLs", @@ -189,6 +198,15 @@ func main() { Action: commands.CmdClaim, Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, cli.BoolFlag{ Name: "verbose, v", Usage: "Make the operation more talkative", From 27e615035e0382e9b3a2ac4a516f5c7f9530bf19 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 20 Jun 2019 15:10:11 -0700 Subject: [PATCH 111/181] support tor everywhere this changes all api calls to respect the flags for tor and tor-port - config now has TorPort method to return flag value if set, or default - api now only has newClient, this creates a tor client when flag is present. does not need to be exported anymore - no methods take a tor argument as no longer needed - all commands now share the same behaviour logging tor messages - api.torClient was removed as not used anywhere - all calls to api.newClient now check for and return the error - api.HandlePost was removed as redundant of api.DoPost --- api/api.go | 84 +++++++++++++++++++------------------------- api/posts.go | 16 +-------- api/sync.go | 4 +-- api/tor.go | 18 ---------- commands/commands.go | 68 ++++++++++++++++++++++++----------- config/options.go | 8 +++++ 6 files changed, 94 insertions(+), 104 deletions(-) delete mode 100644 api/tor.go diff --git a/api/api.go b/api/api.go index d3fa3fc..b59debe 100644 --- a/api/api.go +++ b/api/api.go @@ -13,26 +13,10 @@ import ( cli "gopkg.in/urfave/cli.v1" ) -func client(userAgent string, tor bool) *writeas.Client { - var client *writeas.Client - if tor { - client = writeas.NewTorClient(TorPort) - } else { - if config.IsDev() { - client = writeas.NewDevClient() - } else { - client = writeas.NewClient() - } - } - client.UserAgent = userAgent - - return client -} - -func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { +func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { var client *writeas.Client if config.IsTor(c) { - client = writeas.NewTorClient(TorPort) + client = writeas.NewTorClient(config.TorPort(c)) } else { if config.IsDev() { client = writeas.NewDevClient() @@ -54,8 +38,11 @@ func NewClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. -func DoFetch(friendlyID, ua string, tor bool) error { - cl := client(ua, tor) +func DoFetch(c *cli.Context, friendlyID string) error { + cl, err := newClient(c, false) + if err != nil { + return err + } p, err := cl.GetPost(friendlyID) if err != nil { @@ -72,9 +59,9 @@ func DoFetch(friendlyID, ua string, tor bool) error { // DoFetchPosts retrieves all remote posts for the // authenticated user func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { - cl, err := NewClient(c, true) + cl, err := newClient(c, true) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to create client: %v", err) } posts, err := cl.GetUserPosts() @@ -87,8 +74,11 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. -func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) (*writeas.Post, error) { - cl, _ := NewClient(c, false) +func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { + cl, err := newClient(c, false) + if err != nil { + return nil, fmt.Errorf("Unable to create client: %v", err) + } pp := &writeas.PostParams{ Font: config.GetFont(code, font), @@ -107,7 +97,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( if p.Collection != nil { url = p.Collection.URL + p.Slug } else { - if tor { + if config.IsTor(c) { url = config.TorBaseURL } else if config.IsDev() { url = config.DevBaseURL @@ -143,10 +133,10 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, tor, code bool) ( // DoFetchCollections retrieves a list of the currently logged in users // collections. func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { - cl, err := NewClient(c, true) + cl, err := newClient(c, true) if err != nil { if config.Debug() { - log.ErrorlnQuit("could not create new client: %v", err) + log.ErrorlnQuit("could not create client: %v", err) } return nil, fmt.Errorf("Couldn't create new client") } @@ -156,7 +146,7 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { if config.Debug() { log.ErrorlnQuit("failed fetching user collections: %v", err) } - return nil, fmt.Errorf("Couldn't get user collections") + return nil, fmt.Errorf("Couldn't get user blogs") } out := make([]RemoteColl, len(*colls)) @@ -174,8 +164,11 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { } // DoUpdate updates the given post on Write.as. -func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, code bool) error { - cl, _ := NewClient(c, false) +func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { + cl, err := newClient(c, false) + if err != nil { + return fmt.Errorf("Unable to create client: %v", err) + } params := writeas.PostParams{} params.Title, params.Content = posts.ExtractTitle(string(post)) @@ -186,27 +179,24 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, tor, params.Font = config.GetFont(code, font) } - _, err := cl.UpdatePost(friendlyID, token, ¶ms) + _, err = cl.UpdatePost(friendlyID, token, ¶ms) if err != nil { if config.Debug() { log.ErrorlnQuit("Problem updating: %v", err) } return fmt.Errorf("Post doesn't exist, or bad edit token given.") } - - if tor { - log.Info(c, "Post updated via hidden service.") - } else { - log.Info(c, "Post updated.") - } return nil } // DoDelete deletes the given post on Write.as, and removes any local references -func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { - cl, _ := NewClient(c, false) +func DoDelete(c *cli.Context, friendlyID, token string) error { + cl, err := newClient(c, false) + if err != nil { + return fmt.Errorf("Unable to create client: %v", err) + } - err := cl.DeletePost(friendlyID, token) + err = cl.DeletePost(friendlyID, token) if err != nil { if config.Debug() { log.ErrorlnQuit("Problem deleting: %v", err) @@ -214,18 +204,16 @@ func DoDelete(c *cli.Context, friendlyID, token string, tor bool) error { return fmt.Errorf("Post doesn't exist, or bad edit token given.") } - if tor { - log.Info(c, "Post deleted from hidden service.") - } else { - log.Info(c, "Post deleted.") - } RemovePost(c.App.ExtraInfo()["configDir"], friendlyID) return nil } func DoLogIn(c *cli.Context, username, password string) error { - cl := client(config.UserAgent(c), config.IsTor(c)) + cl, err := newClient(c, false) + if err != nil { + return fmt.Errorf("Unable to create client: %v", err) + } u, err := cl.LogIn(username, password) if err != nil { @@ -244,9 +232,9 @@ func DoLogIn(c *cli.Context, username, password string) error { } func DoLogOut(c *cli.Context) error { - cl, err := NewClient(c, true) + cl, err := newClient(c, true) if err != nil { - return err + return fmt.Errorf("Unable to create client: %v", err) } err = cl.LogOut() diff --git a/api/posts.go b/api/posts.go index 379a2c6..6ab1647 100644 --- a/api/posts.go +++ b/api/posts.go @@ -60,7 +60,7 @@ func AddPost(c *cli.Context, id, token string) error { // ClaimPost adds a local post to the authenticated user's account and deletes // the local reference func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { - cl, err := NewClient(c, true) + cl, err := newClient(c, true) if err != nil { return nil, err } @@ -260,20 +260,6 @@ func WritePost(postsDir string, p *writeas.Post) error { return ioutil.WriteFile(filepath.Join(postsDir, collDir, postFilename), []byte(txtFile), 0644) } -func HandlePost(fullPost []byte, c *cli.Context) (*writeas.Post, error) { - tor := config.IsTor(c) - if c.Int("tor-port") != 0 { - TorPort = c.Int("tor-port") - } - if tor { - log.Info(c, "Posting to hidden service...") - } else { - log.Info(c, "Posting...") - } - - return DoPost(c, fullPost, c.String("font"), false, tor, c.Bool("code")) -} - func ReadStdIn() []byte { numBytes, numChunks := int64(0), int64(0) r := bufio.NewReader(os.Stdin) diff --git a/api/sync.go b/api/sync.go index d687f24..b093be9 100644 --- a/api/sync.go +++ b/api/sync.go @@ -28,12 +28,12 @@ func CmdPull(c *cli.Context) error { syncSetUp(c.App.ExtraInfo()["configDir"], cfg) } - // Fetch posts - cl, err := NewClient(c, true) + cl, err := newClient(c, true) if err != nil { return err } + // Fetch posts posts, err := cl.GetUserPosts() if err != nil { return err diff --git a/api/tor.go b/api/tor.go deleted file mode 100644 index dae5be5..0000000 --- a/api/tor.go +++ /dev/null @@ -1,18 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - - "code.as/core/socks" -) - -var ( - TorPort = 9150 -) - -func torClient() *http.Client { - dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", TorPort)) - transport := &http.Transport{Dial: dialSocksProxy} - return &http.Client{Transport: transport} -} diff --git a/commands/commands.go b/commands/commands.go index 8627411..3f06507 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -14,7 +14,13 @@ import ( ) func CmdPost(c *cli.Context) error { - _, err := api.HandlePost(api.ReadStdIn(), c) + _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code")) + if config.IsTor(c) { + log.Info(c, "Posted to hidden service...") + } else { + log.Info(c, "Posted...") + } + return err } @@ -35,7 +41,13 @@ func CmdNew(c *cli.Context) error { log.InfolnQuit("Empty post. Bye!") } - _, err := api.HandlePost(*p, c) + if config.IsTor(c) { + log.Info(c, "Posting to hidden service...") + } else { + log.Info(c, "Posting...") + } + + _, err := api.DoPost(c, *p, c.String("font"), false, c.Bool("code")) if err != nil { log.Errorln("Error posting: %s\n%s", err, config.MessageRetryCompose(fname)) return cli.NewExitError("", 1) @@ -58,7 +70,13 @@ func CmdPublish(c *cli.Context) error { if err != nil { return err } - _, err = api.HandlePost(content, c) + + if config.IsTor(c) { + log.Info(c, "Publishing to hidden service...") + } else { + log.Info(c, "Publishing...") + } + _, err = api.DoPost(c, content, c.String("font"), false, c.Bool("code")) // TODO: write local file if directory is set return err @@ -81,17 +99,13 @@ func CmdDelete(c *cli.Context) error { } } - tor := config.IsTor(c) - if c.Int("tor-port") != 0 { - api.TorPort = c.Int("tor-port") - } - if tor { + if config.IsTor(c) { log.Info(c, "Deleting via hidden service...") } else { log.Info(c, "Deleting...") } - err := api.DoDelete(c, friendlyID, token, tor) + err := api.DoDelete(c, friendlyID, token) if err != nil { return cli.NewExitError(fmt.Sprintf("Couldn't delete remote copy: %v", err), 1) } @@ -120,17 +134,13 @@ func CmdUpdate(c *cli.Context) error { // Read post body fullPost := api.ReadStdIn() - tor := config.IsTor(c) - if c.Int("tor-port") != 0 { - api.TorPort = c.Int("tor-port") - } - if tor { + if config.IsTor(c) { log.Info(c, "Updating via hidden service...") } else { log.Info(c, "Updating...") } - return api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), tor, c.Bool("code")) + return api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), c.Bool("code")) } func CmdGet(c *cli.Context) error { @@ -139,17 +149,13 @@ func CmdGet(c *cli.Context) error { return cli.NewExitError("usage: writeas get ", 1) } - tor := config.IsTor(c) - if c.Int("tor-port") != 0 { - api.TorPort = c.Int("tor-port") - } - if tor { + if config.IsTor(c) { log.Info(c, "Getting via hidden service...") } else { log.Info(c, "Getting...") } - return api.DoFetch(friendlyID, config.UserAgent(c), tor) + return api.DoFetch(c, friendlyID) } func CmdAdd(c *cli.Context) error { @@ -223,6 +229,11 @@ func CmdCollections(c *cli.Context) error { if u == nil { return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth ", 1) } + if config.IsTor(c) { + log.Info(c, "Getting blogs via hidden service...") + } else { + log.Info(c, "Getting blogs...") + } colls, err := api.DoFetchCollections(c) if err != nil { return cli.NewExitError(fmt.Sprintf("Couldn't get collections for user %s: %v", u.User.Username, err), 1) @@ -260,6 +271,10 @@ func CmdClaim(c *cli.Context) error { } log.Info(c, "Claiming %d post(s) for %s...", len(*localPosts), u.User.Username) + if config.IsTor(c) { + log.Info(c, "...via hidden service...") + } + results, err := api.ClaimPosts(c, localPosts) if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to claim posts: %v", err), 1) @@ -313,6 +328,12 @@ func CmdAuth(c *cli.Context) error { if len(pass) == 0 { return cli.NewExitError("Please enter your password.", 1) } + + if config.IsTor(c) { + log.Info(c, "Logging in to hidden service...") + } else { + log.Info(c, "Logging in...") + } err = api.DoLogIn(c, username, string(pass)) if err != nil { return cli.NewExitError(fmt.Sprintf("error logging in: %v", err), 1) @@ -322,5 +343,10 @@ func CmdAuth(c *cli.Context) error { } func CmdLogOut(c *cli.Context) error { + if config.IsTor(c) { + log.Info(c, "Logging out of hidden service...") + } else { + log.Info(c, "Logging out...") + } return api.DoLogOut(c) } diff --git a/config/options.go b/config/options.go index 1abf9ee..c3bffb5 100644 --- a/config/options.go +++ b/config/options.go @@ -15,6 +15,7 @@ const ( WriteasBaseURL = "https://write.as" DevBaseURL = "https://development.write.as" TorBaseURL = "http://writeas7pm7rcdqg.onion" + torPort = 9150 ) func UserAgent(c *cli.Context) string { @@ -29,6 +30,13 @@ func IsTor(c *cli.Context) bool { return c.Bool("tor") || c.Bool("t") } +func TorPort(c *cli.Context) int { + if c.IsSet("tor-port") && c.Int("tor-port") != 0 { + return c.Int("tor-port") + } + return torPort +} + func Language(c *cli.Context, auto bool) string { if l := c.String("lang"); l != "" { return l From f46c5717a737c01c9498df4361ac06476d1904d2 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Jun 2019 22:32:15 -0400 Subject: [PATCH 112/181] Make Tor publishing statuses consistent This uses "[action] via hidden service" instead of "[action] to hidden service". It also consistently uses "publishing" instead of "posting". --- commands/commands.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 3f06507..251495a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -16,9 +16,9 @@ import ( func CmdPost(c *cli.Context) error { _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code")) if config.IsTor(c) { - log.Info(c, "Posted to hidden service...") + log.Info(c, "Publishing via hidden service...") } else { - log.Info(c, "Posted...") + log.Info(c, "Publishing...") } return err @@ -42,9 +42,9 @@ func CmdNew(c *cli.Context) error { } if config.IsTor(c) { - log.Info(c, "Posting to hidden service...") + log.Info(c, "Publishing via hidden service...") } else { - log.Info(c, "Posting...") + log.Info(c, "Publishing...") } _, err := api.DoPost(c, *p, c.String("font"), false, c.Bool("code")) @@ -72,7 +72,7 @@ func CmdPublish(c *cli.Context) error { } if config.IsTor(c) { - log.Info(c, "Publishing to hidden service...") + log.Info(c, "Publishing via hidden service...") } else { log.Info(c, "Publishing...") } @@ -270,9 +270,10 @@ func CmdClaim(c *cli.Context) error { return nil } - log.Info(c, "Claiming %d post(s) for %s...", len(*localPosts), u.User.Username) if config.IsTor(c) { - log.Info(c, "...via hidden service...") + log.Info(c, "Claiming %d post(s) for %s via hidden service...", len(*localPosts), u.User.Username) + } else { + log.Info(c, "Claiming %d post(s) for %s...", len(*localPosts), u.User.Username) } results, err := api.ClaimPosts(c, localPosts) @@ -330,7 +331,7 @@ func CmdAuth(c *cli.Context) error { } if config.IsTor(c) { - log.Info(c, "Logging in to hidden service...") + log.Info(c, "Logging in via hidden service...") } else { log.Info(c, "Logging in...") } @@ -344,7 +345,7 @@ func CmdAuth(c *cli.Context) error { func CmdLogOut(c *cli.Context) error { if config.IsTor(c) { - log.Info(c, "Logging out of hidden service...") + log.Info(c, "Logging out via hidden service...") } else { log.Info(c, "Logging out...") } From ccec921bd72089c9651f89aec648e063f648b34a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Jun 2019 22:35:43 -0400 Subject: [PATCH 113/181] Show "publishing..." statuses before publishing instead of after. --- commands/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index 251495a..3fb6f49 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -14,13 +14,13 @@ import ( ) func CmdPost(c *cli.Context) error { - _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code")) if config.IsTor(c) { log.Info(c, "Publishing via hidden service...") } else { log.Info(c, "Publishing...") } + _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code")) return err } From a4f122aa552f424a12fd6da98ced820434d6af39 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 20 Jun 2019 22:55:30 -0400 Subject: [PATCH 114/181] Display errors with various commands --- commands/commands.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 3fb6f49..5450763 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -21,7 +21,10 @@ func CmdPost(c *cli.Context) error { } _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code")) - return err + if err != nil { + return cli.NewExitError(err.Error(), 1) + } + return nil } func CmdNew(c *cli.Context) error { @@ -77,9 +80,12 @@ func CmdPublish(c *cli.Context) error { log.Info(c, "Publishing...") } _, err = api.DoPost(c, content, c.String("font"), false, c.Bool("code")) + if err != nil { + return cli.NewExitError(err.Error(), 1) + } // TODO: write local file if directory is set - return err + return nil } func CmdDelete(c *cli.Context) error { @@ -139,8 +145,11 @@ func CmdUpdate(c *cli.Context) error { } else { log.Info(c, "Updating...") } - - return api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), c.Bool("code")) + err := api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), c.Bool("code")) + if err != nil { + return cli.NewExitError(fmt.Sprintf("%v", err), 1) + } + return nil } func CmdGet(c *cli.Context) error { @@ -155,7 +164,11 @@ func CmdGet(c *cli.Context) error { log.Info(c, "Getting...") } - return api.DoFetch(c, friendlyID) + err := api.DoFetch(c, friendlyID) + if err != nil { + return cli.NewExitError(fmt.Sprintf("%v", err), 1) + } + return nil } func CmdAdd(c *cli.Context) error { @@ -166,7 +179,10 @@ func CmdAdd(c *cli.Context) error { } err := api.AddPost(c, friendlyID, token) - return err + if err != nil { + return cli.NewExitError(fmt.Sprintf("%v", err), 1) + } + return nil } func CmdListPosts(c *cli.Context) error { @@ -349,5 +365,9 @@ func CmdLogOut(c *cli.Context) error { } else { log.Info(c, "Logging out...") } - return api.DoLogOut(c) + err := api.DoLogOut(c) + if err != nil { + return cli.NewExitError(fmt.Sprintf("error logging out: %v", err), 1) + } + return nil } From 83ad65c27d8c04e3218f788cc19d4a0dc088198c Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 24 Jun 2019 20:37:07 -0700 Subject: [PATCH 115/181] add config.TorURL select tor URL based on provided host flag, default host in config or default write.as onion address. in that order of precedence. --- api/api.go | 2 +- config/options.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index eaffaca..b8bda5a 100644 --- a/api/api.go +++ b/api/api.go @@ -28,7 +28,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { clientConfig.URL = config.WriteasBaseURL + "/api" } if config.IsTor(c) { - clientConfig.URL = config.TorBaseURL + clientConfig.URL = config.TorURL(c) clientConfig.TorPort = config.TorPort(c) } diff --git a/config/options.go b/config/options.go index fc7813f..047c5c4 100644 --- a/config/options.go +++ b/config/options.go @@ -2,6 +2,7 @@ package config import ( "net/url" + "strings" "github.com/cloudfoundry/jibber_jabber" "github.com/writeas/writeas-cli/log" @@ -38,6 +39,18 @@ func TorPort(c *cli.Context) int { return torPort } +func TorURL(c *cli.Context) string { + flagHost := c.String("host") + if flagHost != "" && strings.HasSuffix(flagHost, "onion") { + return flagHost + } + cfg, _ := LoadConfig(c.App.ExtraInfo()["configDir"]) + if cfg != nil && cfg.Default.Host != "" && strings.HasSuffix(cfg.Default.Host, "onion") { + return cfg.Default.Host + } + return TorBaseURL +} + func Language(c *cli.Context, auto bool) string { if l := c.String("lang"); l != "" { return l From a18df8880f79981c630f34843a822a6f231e4200 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 24 Jun 2019 20:38:41 -0700 Subject: [PATCH 116/181] update wf binary flags to include tor all network calls should respect tor when set --- cmd/wf/main.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 31fc450..e731bf5 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -157,7 +157,7 @@ func main() { { Name: "posts", Usage: "List all of your posts", - Description: "This will list only local posts when not currently authenticated. To list remote posts as well, first run: writeas auth .", + Description: "This will list only local posts.", Action: commands.CmdListPosts, Flags: []cli.Flag{ cli.BoolFlag{ @@ -172,12 +172,25 @@ func main() { Name: "url", Usage: "Show list with URLs", }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Show verbose post listing, including Edit Tokens", + }, }, }, { Name: "blogs", Usage: "List blogs", Action: commands.CmdCollections, Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, cli.BoolFlag{ Name: "url", Usage: "Show list with URLs", @@ -189,6 +202,15 @@ func main() { Action: commands.CmdClaim, Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "tor, t", + Usage: "Authenticate via Tor hidden service", + }, + cli.IntFlag{ + Name: "tor-port", + Usage: "Use a different port to connect to Tor", + Value: 9150, + }, cli.BoolFlag{ Name: "verbose, v", Usage: "Make the operation more talkative", From ce3f8b5945540e75fb371af810428a3b3bf7a9ff Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 24 Jun 2019 20:47:12 -0700 Subject: [PATCH 117/181] update note about user and host flags previously was calling the new `wf` binary the community edition, now just called `writefreely` --- GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDE.md b/GUIDE.md index 848e060..fabd68d 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -45,7 +45,7 @@ GLOBAL OPTIONS: --version, -V print the version ``` -> Note: the host and user flags are only available in `wf` the community edition +> Note: the host and user flags are only available in `writefreely`. #### Share something From 0490e07093eba4b0351995e88dfcdabe8e2c68a5 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 13:40:57 -0400 Subject: [PATCH 118/181] Leave out "Unable to create client" in error msg This bit of extra text ends up creating very long error messages, and only to say that the user isn't logged in, so this commit removes it. --- api/api.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/api.go b/api/api.go index b59debe..baa138b 100644 --- a/api/api.go +++ b/api/api.go @@ -61,7 +61,7 @@ func DoFetch(c *cli.Context, friendlyID string) error { func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { cl, err := newClient(c, true) if err != nil { - return nil, fmt.Errorf("Unable to create client: %v", err) + return nil, fmt.Errorf("%v", err) } posts, err := cl.GetUserPosts() @@ -77,7 +77,7 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { cl, err := newClient(c, false) if err != nil { - return nil, fmt.Errorf("Unable to create client: %v", err) + return nil, fmt.Errorf("%v", err) } pp := &writeas.PostParams{ @@ -167,7 +167,7 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { cl, err := newClient(c, false) if err != nil { - return fmt.Errorf("Unable to create client: %v", err) + return fmt.Errorf("%v", err) } params := writeas.PostParams{} @@ -193,7 +193,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code func DoDelete(c *cli.Context, friendlyID, token string) error { cl, err := newClient(c, false) if err != nil { - return fmt.Errorf("Unable to create client: %v", err) + return fmt.Errorf("%v", err) } err = cl.DeletePost(friendlyID, token) @@ -212,7 +212,7 @@ func DoDelete(c *cli.Context, friendlyID, token string) error { func DoLogIn(c *cli.Context, username, password string) error { cl, err := newClient(c, false) if err != nil { - return fmt.Errorf("Unable to create client: %v", err) + return fmt.Errorf("%v", err) } u, err := cl.LogIn(username, password) @@ -234,7 +234,7 @@ func DoLogIn(c *cli.Context, username, password string) error { func DoLogOut(c *cli.Context) error { cl, err := newClient(c, true) if err != nil { - return fmt.Errorf("Unable to create client: %v", err) + return fmt.Errorf("%v", err) } err = cl.LogOut() From 08c7201580f108b36bcaf0f7b6e4f29b31bbb5af Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 14:17:55 -0400 Subject: [PATCH 119/181] Tweak "couldn't delete" prefix text Now we use "post" instead of "remote copy" --- commands/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index 5450763..bc9ae42 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -113,7 +113,7 @@ func CmdDelete(c *cli.Context) error { err := api.DoDelete(c, friendlyID, token) if err != nil { - return cli.NewExitError(fmt.Sprintf("Couldn't delete remote copy: %v", err), 1) + return cli.NewExitError(fmt.Sprintf("Couldn't delete post: %v", err), 1) } // TODO: Delete local file, if necessary From ae30827389675e40011ab24054befb935642f480 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 15:02:15 -0400 Subject: [PATCH 120/181] Include anonymous posts in `posts` when auth'd This lists the anonymous posts owned by the currently-authenticated user in addition to any local posts, with titles marking both sections. Ref T604 --- api/posts.go | 5 ++++- cmd/writeas/main.go | 4 ++++ commands/commands.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/api/posts.go b/api/posts.go index 6ab1647..77f82a1 100644 --- a/api/posts.go +++ b/api/posts.go @@ -112,7 +112,7 @@ func GetPosts(c *cli.Context) *[]Post { return &posts } -func GetUserPosts(c *cli.Context) ([]RemotePost, error) { +func GetUserPosts(c *cli.Context, draftsOnly bool) ([]RemotePost, error) { waposts, err := DoFetchPosts(c) if err != nil { return nil, err @@ -124,6 +124,9 @@ func GetUserPosts(c *cli.Context) ([]RemotePost, error) { posts := []RemotePost{} for _, p := range waposts { + if draftsOnly && p.Collection != nil { + continue + } post := RemotePost{ Post: Post{ ID: p.ID, diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index b9ec4ba..eec4500 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -164,6 +164,10 @@ func main() { Name: "md", Usage: "Use with --url to return URLs with Markdown enabled", }, + cli.BoolFlag{ + Name: "tor, t", + Usage: "Get posts via Tor hidden service, if authenticated", + }, cli.BoolFlag{ Name: "url", Usage: "Show list with URLs", diff --git a/commands/commands.go b/commands/commands.go index bc9ae42..a275e91 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -192,6 +192,42 @@ func CmdListPosts(c *cli.Context) error { posts := api.GetPosts(c) + u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if u != nil { + if config.IsTor(c) { + log.Info(c, "Getting posts via hidden service...") + } else { + log.Info(c, "Getting posts...") + } + remotePosts, err := api.GetUserPosts(c, true) + if err != nil { + return cli.NewExitError(fmt.Sprintf("error getting posts: %v", err), 1) + } + + if len(remotePosts) > 0 { + fmt.Println("Anonymous Posts") + if details { + identifier := "URL" + if ids || !urls { + identifier = "ID" + } + fmt.Println(identifier) + } + } + for _, p := range remotePosts { + identifier := getPostURL(c, p.ID) + if ids || !urls { + identifier = p.ID + } + + fmt.Println(identifier) + } + + if len(*posts) > 0 { + fmt.Printf("\nUnclaimed Posts\n") + } + } + if details { var p api.Post tw := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', tabwriter.TabIndent) From 4614de5ad98bdbca41a0ca12a716b0e50292c722 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 15:12:52 -0400 Subject: [PATCH 121/181] Update GUIDE to reflect `posts` command changes --- GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDE.md b/GUIDE.md index 5ed6e63..72d51b7 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -86,7 +86,7 @@ dev My Dev Log #### List posts -This lists all posts you've published from your device +This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts. Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. From 56c402f3038d82559edb457d52248c651bf9d736 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 15:27:14 -0400 Subject: [PATCH 122/181] Use go-writeas v2.0.2 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 346f6ef..90d8f91 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - github.com/writeas/go-writeas/v2 v2.0.0 + github.com/writeas/go-writeas/v2 v2.0.2 github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect diff --git a/go.sum b/go.sum index d135529..86825a8 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/writeas/go-writeas/v2 v2.0.0 h1:KjDI5bQSAIH0IzkKW3uGoY98I1A4DrBsSqBklgyOvHw= -github.com/writeas/go-writeas/v2 v2.0.0/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= +github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk= +github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI= github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8= github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= From e5296c338778673134deaeb532b601955bd5193e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 15:34:44 -0400 Subject: [PATCH 123/181] Update README for v2.0 --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65841f4..fc8f93c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ writeas-cli Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. -**NOTE: the `master` branch is currently unstable while we prepare the v2.0 release! You should install via official release channel, or build from the `v1.2` tag.** - ## Features * Publish anonymously to Write.as @@ -26,10 +24,10 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. **Windows**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_386.zip) executable and put it somewhere in your `%PATH%`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. **Debian-based Linux**
```bash @@ -39,7 +37,7 @@ sudo apt-get update && sudo apt-get install writeas-cli ``` **Linux (other)**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. ### Go get it ```bash From 94f847b8b9ae046cdba1cffb4ec10f46d9a23b5a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 26 Jun 2019 15:42:54 -0400 Subject: [PATCH 124/181] Fix macOS download link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc8f93c..149ea10 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Get the latest version for your operating system as a standalone executable. Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. **Debian-based Linux**
```bash From 33c18cbe5d206f6cb044ce1722dc9ab4e6ab94a2 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 1 Jul 2019 16:16:18 -0400 Subject: [PATCH 125/181] Change mentions of Write.as in wf cmd to WriteFreely Particularly, in command descriptions. --- cmd/wf/main.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index e731bf5..7bb709d 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -23,7 +23,7 @@ func main() { app := cli.NewApp() app.Name = "wf" app.Version = appInfo["version"] - app.Usage = "Publish text quickly" + app.Usage = "Publish to any WriteFreely instance from the command-line." // TODO: who is the author? the contributors? link to GH? app.Authors = []cli.Author{ { @@ -42,7 +42,7 @@ func main() { Usage: "Alias for default action: create post from stdin", Action: commands.CmdPost, Flags: config.PostFlags, - Description: `Create a new post on Write.as from stdin. + Description: `Create a new post on WriteFreely from stdin. Use the --code flag to indicate that the post should use syntax highlighting. Or use the --font [value] argument to set the post's @@ -65,14 +65,14 @@ func main() { appearance, where [value] is mono, monospace (default), wrap (monospace font with word wrapping), serif, or sans. - If posting fails for any reason, 'writeas' will show you the temporary file - location and how to pipe it to 'writeas' to retry.`, + If posting fails for any reason, 'wf' will show you the temporary file + location and how to pipe it to 'wf' to retry.`, Action: commands.CmdNew, Flags: config.PostFlags, }, { Name: "publish", - Usage: "Publish a file to Write.as", + Usage: "Publish a file", Action: commands.CmdPublish, Flags: config.PostFlags, }, @@ -149,8 +149,8 @@ func main() { Usage: "Add an existing post locally", Description: `A way to add an existing post to your local store for easy editing later. - This requires a post ID (from https://write.as/[ID]) and an Edit Token - (exported from another Write.as client, such as the Android app). + This requires a post ID (from e.g. https://write.as/[ID]) and an Edit Token + (exported from another WriteFreely client, such as the Android app). `, Action: commands.CmdAdd, }, @@ -200,7 +200,7 @@ func main() { Name: "claim", Usage: "Claim local unsynced posts", Action: commands.CmdClaim, - Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: writeas posts.", + Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.", Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -218,7 +218,7 @@ func main() { }, }, { Name: "auth", - Usage: "Authenticate with Write.as", + Usage: "Authenticate with a WriteFreely instance", Action: commands.CmdAuth, Flags: []cli.Flag{ cli.BoolFlag{ @@ -238,7 +238,7 @@ func main() { }, { Name: "logout", - Usage: "Log out of Write.as", + Usage: "Log out of a WriteFreely instance", Action: commands.CmdLogOut, Flags: []cli.Flag{ cli.BoolFlag{ @@ -262,7 +262,7 @@ func main() { {{.Name}} - {{.Usage}} USAGE: - writeas {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} + wf {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} DESCRIPTION: {{.Description}}{{end}}{{if .Flags}} From 8129d8440d88b4d27ab43df7615ba1d037eed8bc Mon Sep 17 00:00:00 2001 From: Rob j Loranger Date: Wed, 3 Jul 2019 18:59:32 +0000 Subject: [PATCH 126/181] CmdListPosts: update use of LoadUser config.LoadUser now only takes a *cli.Context --- commands/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index fcac3e2..bd6a493 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -192,7 +192,7 @@ func CmdListPosts(c *cli.Context) error { posts := api.GetPosts(c) - u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + u, _ := config.LoadUser(c) if u != nil { if config.IsTor(c) { log.Info(c, "Getting posts via hidden service...") From 5212fa12c8dc6f35ae133d2ee3b80f0caf82cd7b Mon Sep 17 00:00:00 2001 From: Rob j Loranger Date: Wed, 3 Jul 2019 19:01:19 +0000 Subject: [PATCH 127/181] fix LoadUser directory does not exist config.LoadUser was not ensuring the directory exists before trying to load any already authenticated user, fixed using config.DirMustExist --- config/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/user.go b/config/user.go index af9c710..04aa6be 100644 --- a/config/user.go +++ b/config/user.go @@ -15,6 +15,7 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { if err != nil { return nil, err } + DirMustExist(dir) username, err := currentUser(c) if err != nil { return nil, err From a53cbb16b5c5afcd162a921b5ad8fafb159ac660 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 21 Jul 2019 10:26:57 -0400 Subject: [PATCH 128/181] Show correct executable name in user messages --- api/api.go | 5 +++-- commands/commands.go | 23 ++++++++++++----------- config/files_nix.go | 3 ++- config/files_win.go | 4 +++- executable/executable.go | 13 +++++++++++++ 5 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 executable/executable.go diff --git a/api/api.go b/api/api.go index b309993..31e90e7 100644 --- a/api/api.go +++ b/api/api.go @@ -7,6 +7,7 @@ import ( writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/web-core/posts" "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/executable" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -39,7 +40,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { if u != nil { client.SetToken(u.AccessToken) } else if authRequired { - return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth ") + return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") } return client, nil @@ -136,7 +137,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ // Copy URL to clipboard err = clipboard.WriteAll(string(url)) if err != nil { - log.Errorln("writeas: Didn't copy to clipboard: %s", err) + log.Errorln(executable.Name()+": Didn't copy to clipboard: %s", err) } else { log.Info(c, "Copied to clipboard.") } diff --git a/commands/commands.go b/commands/commands.go index bd6a493..41b0662 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -9,6 +9,7 @@ import ( "github.com/howeyc/gopass" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/executable" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -67,7 +68,7 @@ func CmdNew(c *cli.Context) error { func CmdPublish(c *cli.Context) error { filename := c.Args().Get(0) if filename == "" { - return cli.NewExitError("usage: writeas publish ", 1) + return cli.NewExitError("usage: "+executable.Name()+" publish ", 1) } content, err := ioutil.ReadFile(filename) if err != nil { @@ -92,7 +93,7 @@ func CmdDelete(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" { - return cli.NewExitError("usage: writeas delete []", 1) + return cli.NewExitError("usage: "+executable.Name()+" delete []", 1) } u, _ := config.LoadUser(c) @@ -101,7 +102,7 @@ func CmdDelete(c *cli.Context) error { token = api.TokenFromID(c, friendlyID) if token == "" && u == nil { log.Errorln("Couldn't find an edit token locally. Did you create this post here?") - log.ErrorlnQuit("If you have an edit token, use: writeas delete %s ", friendlyID) + log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" delete %s ", friendlyID) } } @@ -124,7 +125,7 @@ func CmdUpdate(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" { - return cli.NewExitError("usage: writeas update []", 1) + return cli.NewExitError("usage: "+executable.Name()+" update []", 1) } u, _ := config.LoadUser(c) @@ -133,7 +134,7 @@ func CmdUpdate(c *cli.Context) error { token = api.TokenFromID(c, friendlyID) if token == "" && u == nil { log.Errorln("Couldn't find an edit token locally. Did you create this post here?") - log.ErrorlnQuit("If you have an edit token, use: writeas update %s ", friendlyID) + log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" update %s ", friendlyID) } } @@ -155,7 +156,7 @@ func CmdUpdate(c *cli.Context) error { func CmdGet(c *cli.Context) error { friendlyID := c.Args().Get(0) if friendlyID == "" { - return cli.NewExitError("usage: writeas get ", 1) + return cli.NewExitError("usage: "+executable.Name()+" get ", 1) } if config.IsTor(c) { @@ -175,7 +176,7 @@ func CmdAdd(c *cli.Context) error { friendlyID := c.Args().Get(0) token := c.Args().Get(1) if friendlyID == "" || token == "" { - return cli.NewExitError("usage: writeas add ", 1) + return cli.NewExitError("usage: "+executable.Name()+" add ", 1) } err := api.AddPost(c, friendlyID, token) @@ -279,7 +280,7 @@ func CmdCollections(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } if u == nil { - return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth ", 1) + return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: "+executable.Name()+" auth ", 1) } if config.IsTor(c) { log.Info(c, "Getting blogs via hidden service...") @@ -314,7 +315,7 @@ func CmdClaim(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } if u == nil { - return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth ", 1) + return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: "+executable.Name()+" auth ", 1) } localPosts := api.GetPosts(c) @@ -362,14 +363,14 @@ func CmdAuth(c *cli.Context) error { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } if u != nil && u.AccessToken != "" { - return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1) + return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: "+executable.Name()+" logout", 1) } // Validate arguments and get password // TODO: after global config, check for default user username := c.Args().Get(0) if username == "" { - return cli.NewExitError("usage: writeas auth ", 1) + return cli.NewExitError("usage: "+executable.Name()+" auth ", 1) } fmt.Print("Password: ") diff --git a/config/files_nix.go b/config/files_nix.go index 0c10f04..13fb338 100644 --- a/config/files_nix.go +++ b/config/files_nix.go @@ -7,6 +7,7 @@ import ( "os/exec" homedir "github.com/mitchellh/go-homedir" + "github.com/writeas/writeas-cli/executable" ) const ( @@ -39,5 +40,5 @@ func EditPostCmd(fname string) *exec.Cmd { } func MessageRetryCompose(fname string) string { - return fmt.Sprintf("To retry this post, run:\n cat %s | writeas", fname) + return fmt.Sprintf("To retry this post, run:\n cat %s | %s", fname, executable.Name()) } diff --git a/config/files_win.go b/config/files_win.go index 026b803..db2f459 100644 --- a/config/files_win.go +++ b/config/files_win.go @@ -6,6 +6,8 @@ import ( "fmt" "os" "os/exec" + + "github.com/writeas/writeas-cli/executable" ) const ( @@ -22,5 +24,5 @@ func EditPostCmd(fname string) *exec.Cmd { } func MessageRetryCompose(fname string) string { - return fmt.Sprintf("To retry this post, run:\n type %s | writeas.exe", fname) + return fmt.Sprintf("To retry this post, run:\n type %s | %s", fname, executable.Name()) } diff --git a/executable/executable.go b/executable/executable.go new file mode 100644 index 0000000..697f419 --- /dev/null +++ b/executable/executable.go @@ -0,0 +1,13 @@ +// Package executable holds utility functions that assist both CLI executables, +// writeas and wf. +package executable + +import ( + "os" + "path" +) + +func Name() string { + n := os.Args[0] + return path.Base(n) +} From 1b8655f06ad65da5f6c1810fc5e5c761f9c49e83 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 13:19:12 -0400 Subject: [PATCH 129/181] Accept --host without scheme This automatically prepends https:// to any given --host value. It also adds an --insecure flag, which will instead prepend http:// The goal with this is to save some typing and encourage operations over HTTPS. Ref T595 --- api/api.go | 17 +++++++++++++++-- config/flags.go | 4 ++++ config/options.go | 6 +----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 31e90e7..b60d9c1 100644 --- a/api/api.go +++ b/api/api.go @@ -12,6 +12,19 @@ import ( cli "gopkg.in/urfave/cli.v1" ) +func HostURL(c *cli.Context) string { + host := c.GlobalString("host") + if host == "" { + return "" + } + insecure := c.Bool("insecure") + scheme := "https://" + if insecure { + scheme = "http://" + } + return scheme + host +} + func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { var client *writeas.Client var clientConfig writeas.Config @@ -19,8 +32,8 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { if err != nil { return nil, fmt.Errorf("Failed to load configuration file: %v", err) } - if c.GlobalString("host") != "" { - clientConfig.URL = c.GlobalString("host") + "/api" + if host := HostURL(c); host != "" { + clientConfig.URL = host + "/api" } else if cfg.Default.Host != "" { clientConfig.URL = cfg.Default.Host + "/api" } else if config.IsDev() { diff --git a/config/flags.go b/config/flags.go index 2c17e23..195eb8d 100644 --- a/config/flags.go +++ b/config/flags.go @@ -11,6 +11,10 @@ var PostFlags = []cli.Flag{ Usage: "Optional blog to post to", Value: "", }, + cli.BoolFlag{ + Name: "insecure", + Usage: "Send request insecurely.", + }, cli.BoolFlag{ Name: "tor, t", Usage: "Perform action on Tor hidden service", diff --git a/config/options.go b/config/options.go index 047c5c4..adbed78 100644 --- a/config/options.go +++ b/config/options.go @@ -90,11 +90,7 @@ func HostDirectory(c *cli.Context) (string, error) { } // flag takes precedence over defaults if hostFlag := c.GlobalString("host"); hostFlag != "" { - u, err := url.Parse(hostFlag) - if err != nil { - return "", err - } - return u.Hostname(), nil + return hostFlag, nil } u, err := url.Parse(cfg.Default.Host) From 4e4ccbe2e85ac21ccd9e3a9e65b58638841c425a Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 13:24:09 -0400 Subject: [PATCH 130/181] Don't default host to Write.as in wf-cli We want to keep wf-cli service-agnostic and to make sure there's no confusing overlap between this client and writeas-cli. Ref T586 --- api/api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index b60d9c1..8fac589 100644 --- a/api/api.go +++ b/api/api.go @@ -38,8 +38,10 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { clientConfig.URL = cfg.Default.Host + "/api" } else if config.IsDev() { clientConfig.URL = config.DevBaseURL + "/api" - } else { + } else if c.App.Name == "writeas" { clientConfig.URL = config.WriteasBaseURL + "/api" + } else { + return nil, fmt.Errorf("Must supply a host. Example: %s --host example.com %s", executable.Name(), c.Command.Name) } if config.IsTor(c) { clientConfig.URL = config.TorURL(c) From c8502be442ac0f6d7e42cb1ff3154792e936af15 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 15:00:18 -0400 Subject: [PATCH 131/181] Output full URL publishing with --host flag Now that no scheme is required, we need to use the helper function to include it, instead of the raw --host value. This does that. Ref T595 --- api/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 8fac589..06985d2 100644 --- a/api/api.go +++ b/api/api.go @@ -126,8 +126,8 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ if p.Collection != nil { url = p.Collection.URL + p.Slug } else { - if c.GlobalString("host") != "" { - url = c.GlobalString("host") + if host := HostURL(c); host != "" { + url = host } else if cfg.Default.Host != "" { url = cfg.Default.Host } else if config.IsDev() { From b55874c6e66994f3b823b515e48601d630ddadaf Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 15:04:02 -0400 Subject: [PATCH 132/181] Require authentication for post operations in wf Unlike Write.as, WriteFreely doesn't support anonymous (unauthenticated) publishing. With these changes, we reflect that in wf-cli by wrapping creating, updating, and deleting posts in a func that checks authentication state. Ref T586 --- cmd/wf/commands.go | 23 +++++++++++++++++++++++ cmd/wf/main.go | 12 ++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 cmd/wf/commands.go diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go new file mode 100644 index 0000000..ba67145 --- /dev/null +++ b/cmd/wf/commands.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + + "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/executable" + cli "gopkg.in/urfave/cli.v1" +) + +func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { + return func(c *cli.Context) error { + u, err := config.LoadUser(c) + if err != nil { + return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + } + if u == nil { + return cli.NewExitError("You must be authenticated to "+action+".\nLog in first with: "+executable.Name()+" auth ", 1) + } + + return f(c) + } +} diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 7bb709d..1e2f107 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -34,13 +34,13 @@ func main() { app.ExtraInfo = func() map[string]string { return appInfo } - app.Action = commands.CmdPost + app.Action = requireAuth(commands.CmdPost, "publish") app.Flags = append(config.PostFlags, flags...) app.Commands = []cli.Command{ { Name: "post", Usage: "Alias for default action: create post from stdin", - Action: commands.CmdPost, + Action: requireAuth(commands.CmdPost, "publish"), Flags: config.PostFlags, Description: `Create a new post on WriteFreely from stdin. @@ -67,19 +67,19 @@ func main() { If posting fails for any reason, 'wf' will show you the temporary file location and how to pipe it to 'wf' to retry.`, - Action: commands.CmdNew, + Action: requireAuth(commands.CmdNew, "publish"), Flags: config.PostFlags, }, { Name: "publish", Usage: "Publish a file", - Action: commands.CmdPublish, + Action: requireAuth(commands.CmdPublish, "publish"), Flags: config.PostFlags, }, { Name: "delete", Usage: "Delete a post", - Action: commands.CmdDelete, + Action: requireAuth(commands.CmdDelete, "delete"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -99,7 +99,7 @@ func main() { { Name: "update", Usage: "Update (overwrite) a post", - Action: commands.CmdUpdate, + Action: requireAuth(commands.CmdUpdate, "update"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", From 8b1cc1411cd04c0c749723cb5076fb78af488283 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 15:07:33 -0400 Subject: [PATCH 133/181] Reflect wf-cli/writeas-cli in User-Agent Previously, this would always include writeas-cli. Ref T586 --- config/options.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/config/options.go b/config/options.go index adbed78..e478cba 100644 --- a/config/options.go +++ b/config/options.go @@ -11,7 +11,8 @@ import ( // Application constants. const ( - defaultUserAgent = "writeas-cli v" + writeasUserAgent = "writeas-cli v" + wfUserAgent = "wf-cli v" // Defaults for posts on Write.as. DefaultFont = PostFontMono WriteasBaseURL = "https://write.as" @@ -21,11 +22,16 @@ const ( ) func UserAgent(c *cli.Context) string { + client := wfUserAgent + if c.App.Name == "writeas" { + client = writeasUserAgent + } + ua := c.String("user-agent") if ua == "" { - return defaultUserAgent + c.App.ExtraInfo()["version"] + return client + c.App.ExtraInfo()["version"] } - return ua + " (" + defaultUserAgent + c.App.ExtraInfo()["version"] + ")" + return ua + " (" + client + c.App.ExtraInfo()["version"] + ")" } func IsTor(c *cli.Context) bool { From 8d1b4102b847d63d95af944fe02ca11a94ae1634 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 15:13:51 -0400 Subject: [PATCH 134/181] Automatically set WF account as default on first auth Now when running `wf auth`, we'll automatically set the given host + username as the default account in .writefreely/config.ini, so subsequent requests without an explicit --host and --user will use this account. Ref T635 T586 --- cmd/wf/commands.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/wf/main.go | 4 +-- config/user.go | 13 ++++++--- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index ba67145..ee8152d 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -3,8 +3,11 @@ package main import ( "fmt" + "github.com/writeas/writeas-cli/api" + "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" "github.com/writeas/writeas-cli/executable" + "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" ) @@ -21,3 +24,69 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { return f(c) } } + +func cmdAuth(c *cli.Context) error { + err := commands.CmdAuth(c) + if err != nil { + return err + } + + // Get the username from the command, just like commands.CmdAuth does + username := c.Args().Get(0) + + // Update config if this is user's first auth + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + log.Errorln("Not saving config. Unable to load config: %s", err) + return err + } + if cfg.Default.Host == "" && cfg.Default.User == "" { + // This is user's first auth, so save defaults + cfg.Default.Host = api.HostURL(c) + cfg.Default.User = username + err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) + if err != nil { + log.Errorln("Not saving config. Unable to save config: %s", err) + return err + } + fmt.Printf("Set %s on %s as default account.\n", username, c.GlobalString("host")) + } + + return nil +} + +func cmdLogOut(c *cli.Context) error { + err := commands.CmdLogOut(c) + if err != nil { + return err + } + + // Remove this from config if it's the default account + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + log.Errorln("Not updating config. Unable to load: %s", err) + return err + } + username, err := config.CurrentUser(c) + if err != nil { + log.Errorln("Not updating config. Unable to load current user: %s", err) + return err + } + reqHost := api.HostURL(c) + if reqHost == "" { + // No --host given, so we're using the default host + reqHost = cfg.Default.Host + } + if cfg.Default.Host == reqHost && cfg.Default.User == username { + // We're logging out of default username + host, so remove from config file + cfg.Default.Host = "" + cfg.Default.User = "" + err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) + if err != nil { + log.Errorln("Not updating config. Unable to save config: %s", err) + return err + } + } + + return nil +} diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 1e2f107..140cde5 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -219,7 +219,7 @@ func main() { }, { Name: "auth", Usage: "Authenticate with a WriteFreely instance", - Action: commands.CmdAuth, + Action: cmdAuth, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -239,7 +239,7 @@ func main() { { Name: "logout", Usage: "Log out of a WriteFreely instance", - Action: commands.CmdLogOut, + Action: cmdLogOut, Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", diff --git a/config/user.go b/config/user.go index 04aa6be..b39a4f5 100644 --- a/config/user.go +++ b/config/user.go @@ -16,7 +16,7 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { return nil, err } DirMustExist(dir) - username, err := currentUser(c) + username, err := CurrentUser(c) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func DeleteUser(c *cli.Context) error { return err } - username, err := currentUser(c) + username, err := CurrentUser(c) if err != nil { return err } @@ -72,7 +72,7 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { return err } // Save file - username, err := currentUser(c) + username, err := CurrentUser(c) if err != nil { return err } @@ -98,7 +98,10 @@ func userHostDir(c *cli.Context) (string, error) { return filepath.Join(dataDir, hostDir), nil } -func currentUser(c *cli.Context) (string, error) { +// CurrentUser returns the username of the user taking action in the current +// cli.Context. +func CurrentUser(c *cli.Context) (string, error) { + // Load host-level config, if host flag is set hostDir, err := userHostDir(c) if err != nil { return "", err @@ -108,12 +111,14 @@ func currentUser(c *cli.Context) (string, error) { return "", err } if cfg.Default.User == "" { + // Load app-level config cfg, err = LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) if err != nil { return "", err } } + // Use user flag value if c.GlobalString("user") != "" { return c.GlobalString("user"), nil } From eb6332945a8793cb596cbc218e1301a0bc4c64ba Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 22 Jul 2019 15:22:36 -0400 Subject: [PATCH 135/181] Remove `add` action from wf-cli WriteFreely doesn't support non-auth'd posting like Write.as, so users won't be creating non-auth'd posts with another client that they'd need to add here. Ref T586 --- cmd/wf/main.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 140cde5..46de71d 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -144,16 +144,6 @@ func main() { }, }, }, - { - Name: "add", - Usage: "Add an existing post locally", - Description: `A way to add an existing post to your local store for easy editing later. - - This requires a post ID (from e.g. https://write.as/[ID]) and an Edit Token - (exported from another WriteFreely client, such as the Android app). -`, - Action: commands.CmdAdd, - }, { Name: "posts", Usage: "List all of your posts", From 60d987eed3e24ce13683876f249744e4a851e304 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 29 Jul 2019 14:40:49 -0400 Subject: [PATCH 136/181] Clean up directories on `wf logout` On logout, this deletes host / user directories if they're empty. Ref T586 --- config/user.go | 37 ++++++++++++++++++++++++++++++++++++- fileutils/fileutils.go | 16 ++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/config/user.go b/config/user.go index b39a4f5..f1a5e9a 100644 --- a/config/user.go +++ b/config/user.go @@ -57,7 +57,42 @@ func DeleteUser(c *cli.Context) error { username = "" } - return fileutils.DeleteFile(filepath.Join(dir, username, "user.json")) + // Delete user data + err = fileutils.DeleteFile(filepath.Join(dir, username, "user.json")) + if err != nil { + return err + } + + // Do additional cleanup in wf-cli + if c.App.Name == "wf" { + // Delete user dir if it's empty + userEmpty, err := fileutils.IsEmpty(filepath.Join(dir, username)) + if err != nil { + return err + } + if !userEmpty { + return nil + } + err = fileutils.DeleteFile(filepath.Join(dir, username)) + if err != nil { + return err + } + + // Delete host dir if it's empty + hostEmpty, err := fileutils.IsEmpty(dir) + if err != nil { + return err + } + if !hostEmpty { + return nil + } + err = fileutils.DeleteFile(dir) + if err != nil { + return err + } + } + + return nil } func SaveUser(c *cli.Context, u *writeas.AuthUser) error { diff --git a/fileutils/fileutils.go b/fileutils/fileutils.go index 7e4c354..eda996b 100644 --- a/fileutils/fileutils.go +++ b/fileutils/fileutils.go @@ -3,6 +3,7 @@ package fileutils import ( "bufio" "fmt" + "io" "os" "strings" ) @@ -109,3 +110,18 @@ func FindLine(p, startsWith string) string { func DeleteFile(p string) error { return os.Remove(p) } + +// IsEmpty returns whether or not the given directory is empty +func IsEmpty(d string) (bool, error) { + f, err := os.Open(d) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) + if err == io.EOF { + return true, nil + } + return false, err +} From b85673b799abdb0c350ab7bfac38f31a919f5663 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 1 Aug 2019 09:32:14 -0700 Subject: [PATCH 137/181] move LoadUser call out of newClient to start on a fix for user actions when logged in with one user on a given host with the wf binary, a user should now only be loaded by the caller of newClient. newClient no longer takes a bool for authRequired, all calls updated to new signature and load the user where required. --- api/api.go | 46 ++++++++++++++++++++++++++++++---------------- api/posts.go | 11 ++++++++++- api/sync.go | 10 +++++++++- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/api/api.go b/api/api.go index 06985d2..35c10fc 100644 --- a/api/api.go +++ b/api/api.go @@ -25,7 +25,7 @@ func HostURL(c *cli.Context) string { return scheme + host } -func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { +func newClient(c *cli.Context) (*writeas.Client, error) { var client *writeas.Client var clientConfig writeas.Config cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) @@ -50,13 +50,6 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { client = writeas.NewClientWith(clientConfig) client.UserAgent = config.UserAgent(c) - // TODO: load user into var shared across the app - u, _ := config.LoadUser(c) - if u != nil { - client.SetToken(u.AccessToken) - } else if authRequired { - return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") - } return client, nil } @@ -64,7 +57,7 @@ func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { // DoFetch retrieves the Write.as post with the given friendlyID, // optionally via the Tor hidden service. func DoFetch(c *cli.Context, friendlyID string) error { - cl, err := newClient(c, false) + cl, err := newClient(c) if err != nil { return err } @@ -84,11 +77,18 @@ func DoFetch(c *cli.Context, friendlyID string) error { // DoFetchPosts retrieves all remote posts for the // authenticated user func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { - cl, err := newClient(c, true) + cl, err := newClient(c) if err != nil { return nil, fmt.Errorf("%v", err) } + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + posts, err := cl.GetUserPosts() if err != nil { return nil, err @@ -100,7 +100,7 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { // DoPost creates a Write.as post, returning an error if it was // unsuccessful. func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { - cl, err := newClient(c, false) + cl, err := newClient(c) if err != nil { return nil, fmt.Errorf("%v", err) } @@ -166,7 +166,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ // DoFetchCollections retrieves a list of the currently logged in users // collections. func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { - cl, err := newClient(c, true) + cl, err := newClient(c) if err != nil { if config.Debug() { log.ErrorlnQuit("could not create client: %v", err) @@ -174,6 +174,13 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { return nil, fmt.Errorf("Couldn't create new client") } + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + colls, err := cl.GetUserCollections() if err != nil { if config.Debug() { @@ -198,7 +205,7 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { // DoUpdate updates the given post on Write.as. func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { - cl, err := newClient(c, false) + cl, err := newClient(c) if err != nil { return fmt.Errorf("%v", err) } @@ -224,7 +231,7 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code // DoDelete deletes the given post on Write.as, and removes any local references func DoDelete(c *cli.Context, friendlyID, token string) error { - cl, err := newClient(c, false) + cl, err := newClient(c) if err != nil { return fmt.Errorf("%v", err) } @@ -243,7 +250,7 @@ func DoDelete(c *cli.Context, friendlyID, token string) error { } func DoLogIn(c *cli.Context, username, password string) error { - cl, err := newClient(c, false) + cl, err := newClient(c) if err != nil { return fmt.Errorf("%v", err) } @@ -265,11 +272,18 @@ func DoLogIn(c *cli.Context, username, password string) error { } func DoLogOut(c *cli.Context) error { - cl, err := newClient(c, true) + cl, err := newClient(c) if err != nil { return fmt.Errorf("%v", err) } + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else if c.App.Name == "writeas" { + return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + err = cl.LogOut() if err != nil { if config.Debug() { diff --git a/api/posts.go b/api/posts.go index 3c1e2bf..033cac3 100644 --- a/api/posts.go +++ b/api/posts.go @@ -12,6 +12,7 @@ import ( writeas "github.com/writeas/go-writeas/v2" "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/executable" "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" @@ -64,10 +65,18 @@ func AddPost(c *cli.Context, id, token string) error { // ClaimPost adds a local post to the authenticated user's account and deletes // the local reference func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { - cl, err := newClient(c, true) + cl, err := newClient(c) if err != nil { return nil, err } + + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts)) for i, post := range *localPosts { postsToClaim[i] = writeas.OwnedPostParams{ diff --git a/api/sync.go b/api/sync.go index b025608..e57a31b 100644 --- a/api/sync.go +++ b/api/sync.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/writeas/writeas-cli/config" + "github.com/writeas/writeas-cli/executable" "github.com/writeas/writeas-cli/fileutils" "github.com/writeas/writeas-cli/log" cli "gopkg.in/urfave/cli.v1" @@ -28,11 +29,18 @@ func CmdPull(c *cli.Context) error { syncSetUp(c, cfg) } - cl, err := newClient(c, true) + cl, err := newClient(c) if err != nil { return err } + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + // Fetch posts posts, err := cl.GetUserPosts() if err != nil { From c218012d4243db76a53c38776320eed8065334a3 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 10:43:56 -0700 Subject: [PATCH 138/181] export UserHostDir for use outside of package --- config/user.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/user.go b/config/user.go index f1a5e9a..adbfa64 100644 --- a/config/user.go +++ b/config/user.go @@ -11,7 +11,7 @@ import ( ) func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { - dir, err := userHostDir(c) + dir, err := UserHostDir(c) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func LoadUser(c *cli.Context) (*writeas.AuthUser, error) { } func DeleteUser(c *cli.Context) error { - dir, err := userHostDir(c) + dir, err := UserHostDir(c) if err != nil { return err } @@ -102,7 +102,7 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { return err } - dir, err := userHostDir(c) + dir, err := UserHostDir(c) if err != nil { return err } @@ -122,9 +122,9 @@ func SaveUser(c *cli.Context, u *writeas.AuthUser) error { return nil } -// userHostDir returns the path to the user data directory with the host based +// UserHostDir returns the path to the user data directory with the host based // subpath if the host flag is set -func userHostDir(c *cli.Context) (string, error) { +func UserHostDir(c *cli.Context) (string, error) { dataDir := UserDataDir(c.App.ExtraInfo()["configDir"]) hostDir, err := HostDirectory(c) if err != nil { @@ -137,7 +137,7 @@ func userHostDir(c *cli.Context) (string, error) { // cli.Context. func CurrentUser(c *cli.Context) (string, error) { // Load host-level config, if host flag is set - hostDir, err := userHostDir(c) + hostDir, err := UserHostDir(c) if err != nil { return "", err } From 58dd3f985a7fb342acaad419ad23f2ef9427d775 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 10:44:51 -0700 Subject: [PATCH 139/181] helper func for logged in users this function checks the host based path for any logged in users and returns the number or users, a list of usernames and an error if any --- cmd/wf/commands.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index ee8152d..c252e6a 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "os" + "path/filepath" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" @@ -25,6 +27,33 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { } } +// usersLoggedIn checks for logged in users for the set host flag +// it returns the number of users and a slice of usernames +func usersLoggedIn(c *cli.Context) (int, []string, error) { + path, err := config.UserHostDir(c) + if err != nil { + return 0, nil, err + } + dir, err := os.Open(path) + if err != nil { + return 0, nil, err + } + contents, err := dir.Readdir(0) + if err != nil { + return 0, nil, err + } + var names []string + for _, file := range contents { + if file.IsDir() { + // stat user.json + if _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")); err == nil { + names = append(names, file.Name()) + } + } + } + return len(names), names, nil +} + func cmdAuth(c *cli.Context) error { err := commands.CmdAuth(c) if err != nil { From 630a867a34bf6f1d84f71f8b71b2f4dfabd15164 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 10:46:55 -0700 Subject: [PATCH 140/181] update requireAuth helper, check logged in users this uses the usersLoggedIn helper to check for already logged in users, selecting the single user when only one present and returning and error when multiple are logged in. --- cmd/wf/commands.go | 18 +++++++++++++++++- cmd/wf/main.go | 8 ++++---- commands/commands.go | 6 +++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index c252e6a..a725429 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -15,9 +15,25 @@ import ( func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { return func(c *cli.Context) error { + // check for logged in users when host is provided without user + if c.GlobalIsSet("host") && !c.GlobalIsSet("user") { + // multiple users should display a list + if num, users, err := usersLoggedIn(c); num > 1 && err == nil { + return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", users), 1) + } else if num == 1 && err == nil { + // single user found for host should be set as user flag so LoadUser can + // succeed, and notify the client + if err := c.GlobalSet("user", users[0]); err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to set user flag for only logged in user at host %s: %v", users[0], err), 1) + } + fmt.Printf("Host specified without user flag, using logged in user: %s\n", users[0]) + } else if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) + } + } u, err := config.LoadUser(c) if err != nil { - return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) + return cli.NewExitError(fmt.Sprintf("Couldn't load user: %v", err), 1) } if u == nil { return cli.NewExitError("You must be authenticated to "+action+".\nLog in first with: "+executable.Name()+" auth ", 1) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 46de71d..7cce4c9 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -148,7 +148,7 @@ func main() { Name: "posts", Usage: "List all of your posts", Description: "This will list only local posts.", - Action: commands.CmdListPosts, + Action: requireAuth(commands.CmdListPosts, "posts"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", @@ -170,7 +170,7 @@ func main() { }, { Name: "blogs", Usage: "List blogs", - Action: commands.CmdCollections, + Action: requireAuth(commands.CmdCollections, "blogs"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -189,7 +189,7 @@ func main() { }, { Name: "claim", Usage: "Claim local unsynced posts", - Action: commands.CmdClaim, + Action: requireAuth(commands.CmdClaim, "claim"), Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.", Flags: []cli.Flag{ cli.BoolFlag{ @@ -229,7 +229,7 @@ func main() { { Name: "logout", Usage: "Log out of a WriteFreely instance", - Action: cmdLogOut, + Action: requireAuth(cmdLogOut, "logout"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", diff --git a/commands/commands.go b/commands/commands.go index 41b0662..19119b1 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -357,18 +357,18 @@ func CmdClaim(c *cli.Context) error { } func CmdAuth(c *cli.Context) error { + username := c.Args().Get(0) // Check configuration u, err := config.LoadUser(c) if err != nil { return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) } - if u != nil && u.AccessToken != "" { - return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: "+executable.Name()+" logout", 1) + if u != nil && u.AccessToken != "" && username == u.User.Username { + return cli.NewExitError("You're already authenticated as "+u.User.Username, 1) } // Validate arguments and get password // TODO: after global config, check for default user - username := c.Args().Get(0) if username == "" { return cli.NewExitError("usage: "+executable.Name()+" auth ", 1) } From 55dcf5e79c9b30ff40d3234cd07e95e2a33cb719 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 10:58:36 -0700 Subject: [PATCH 141/181] CurrentUser only use global when needed by needed: when host flag was supplied, matches that configured and a user is set in config as well. they should be a pair. --- config/user.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/user.go b/config/user.go index adbfa64..f62e3d7 100644 --- a/config/user.go +++ b/config/user.go @@ -147,10 +147,17 @@ func CurrentUser(c *cli.Context) (string, error) { } if cfg.Default.User == "" { // Load app-level config - cfg, err = LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) + globalCFG, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"])) if err != nil { return "", err } + // only user global defaults when both are set and hosts match + if globalCFG.Default.User != "" && + globalCFG.Default.Host != "" && + c.GlobalIsSet("host") && + globalCFG.Default.Host == c.GlobalString("host") { + cfg = globalCFG + } } // Use user flag value From 941d2343951193160840a22f0eb91eb1164b0443 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 11:09:02 -0700 Subject: [PATCH 142/181] log.Info will respect global flags --- log/logging.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/logging.go b/log/logging.go index a514a7c..42da556 100644 --- a/log/logging.go +++ b/log/logging.go @@ -10,7 +10,7 @@ import ( // Info logs general diagnostic messages, shown only when the -v or --verbose // flag is provided. func Info(c *cli.Context, s string, p ...interface{}) { - if c.Bool("v") || c.Bool("verbose") { + if c.Bool("v") || c.Bool("verbose") || c.GlobalBool("v") || c.GlobalBool("verbose") { fmt.Fprintf(os.Stderr, s+"\n", p...) } } From 72dbd1c7eff4c2f1a483b20a80a313fdc239b58c Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 2 Aug 2019 11:09:39 -0700 Subject: [PATCH 143/181] requireAuth host message verbose only --- cmd/wf/commands.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index a725429..3b64bc1 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" @@ -19,14 +20,14 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { if c.GlobalIsSet("host") && !c.GlobalIsSet("user") { // multiple users should display a list if num, users, err := usersLoggedIn(c); num > 1 && err == nil { - return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", users), 1) + return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", strings.Join(users, ", ")), 1) } else if num == 1 && err == nil { // single user found for host should be set as user flag so LoadUser can // succeed, and notify the client if err := c.GlobalSet("user", users[0]); err != nil { return cli.NewExitError(fmt.Sprintf("Failed to set user flag for only logged in user at host %s: %v", users[0], err), 1) } - fmt.Printf("Host specified without user flag, using logged in user: %s\n", users[0]) + log.Info(c, "Host specified without user flag, using logged in user: %s\n", users[0]) } else if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) } From 836eea7dab19b13de9004fdc75ded61a5abc0475 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 09:22:31 -0700 Subject: [PATCH 144/181] newClient must prepend scheme to default host this updates newClient to prepend the scheme https to the default host found in the global config, and only use said host when both the user and host are configured --- api/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 35c10fc..92cda2b 100644 --- a/api/api.go +++ b/api/api.go @@ -34,8 +34,8 @@ func newClient(c *cli.Context) (*writeas.Client, error) { } if host := HostURL(c); host != "" { clientConfig.URL = host + "/api" - } else if cfg.Default.Host != "" { - clientConfig.URL = cfg.Default.Host + "/api" + } else if cfg.Default.Host != "" && cfg.Default.User != "" { + clientConfig.URL = "https://" + cfg.Default.Host + "/api" } else if config.IsDev() { clientConfig.URL = config.DevBaseURL + "/api" } else if c.App.Name == "writeas" { From 44501a2e8ca94ac7a7628ce626cbc7fbfe899723 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 09:26:49 -0700 Subject: [PATCH 145/181] use default user when authenticating if both a user and host are configured, the command auth should assume that user and host when none specified --- commands/commands.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 19119b1..f3dd32a 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -368,9 +368,17 @@ func CmdAuth(c *cli.Context) error { } // Validate arguments and get password - // TODO: after global config, check for default user if username == "" { - return cli.NewExitError("usage: "+executable.Name()+" auth ", 1) + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to load config: %v", err), 1) + } + if cfg.Default.Host != "" && cfg.Default.User != "" { + username = cfg.Default.User + fmt.Printf("No user provided, using default user %s for host %s...\n", cfg.Default.User, cfg.Default.Host) + } else { + return cli.NewExitError("usage: "+executable.Name()+" auth ", 1) + } } fmt.Print("Password: ") From 8625e42ce7ca9a61f7f6c509f3e8282eeb06c2ed Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 09:28:09 -0700 Subject: [PATCH 146/181] do not parse url from configured host now that the scheme is not required in the configuration file, we can assume the configured value is correct. as with other uses, the configured value is only used when noth host and user are present. --- config/options.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/config/options.go b/config/options.go index e478cba..895f518 100644 --- a/config/options.go +++ b/config/options.go @@ -1,7 +1,6 @@ package config import ( - "net/url" "strings" "github.com/cloudfoundry/jibber_jabber" @@ -99,9 +98,9 @@ func HostDirectory(c *cli.Context) (string, error) { return hostFlag, nil } - u, err := url.Parse(cfg.Default.Host) - if err != nil { - return "", err + if cfg.Default.Host != "" && cfg.Default.User != "" { + return cfg.Default.Host, nil } - return u.Hostname(), nil + + return "", nil } From dea0eb2d4457376a3d4d30b531c322c09784d204 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 09:30:56 -0700 Subject: [PATCH 147/181] wf check for config when auth required this adds a check for configured defaults during the command pre check when authentication is required. using the values found if both are present and only if both flags are ommitted. --- cmd/wf/commands.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index 3b64bc1..4ffabca 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -31,6 +31,24 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { } else if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) } + } else if !c.GlobalIsSet("host") && !c.GlobalIsSet("user") { + // check for global configured pair host/user + cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to load config from file: %v", err), 1) + // set flags if found + } + // set flags if both were found in config + if cfg.Default.Host != "" && cfg.Default.User != "" { + err = c.GlobalSet("host", cfg.Default.Host) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to set host from global config: %v", err), 1) + } + err = c.GlobalSet("user", cfg.Default.User) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to set user from global config: %v", err), 1) + } + } } u, err := config.LoadUser(c) if err != nil { From abc78c7652a43e3c998416305fa2038e6c92619f Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 10:06:09 -0700 Subject: [PATCH 148/181] CurrentUser should always return the flag if set previously when the user flag was provided, the function would still try to get a user from config. flag should take precedence --- config/user.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/user.go b/config/user.go index f62e3d7..45c287d 100644 --- a/config/user.go +++ b/config/user.go @@ -136,6 +136,11 @@ func UserHostDir(c *cli.Context) (string, error) { // CurrentUser returns the username of the user taking action in the current // cli.Context. func CurrentUser(c *cli.Context) (string, error) { + // Use user flag value + if c.GlobalString("user") != "" { + return c.GlobalString("user"), nil + } + // Load host-level config, if host flag is set hostDir, err := UserHostDir(c) if err != nil { @@ -160,10 +165,5 @@ func CurrentUser(c *cli.Context) (string, error) { } } - // Use user flag value - if c.GlobalString("user") != "" { - return c.GlobalString("user"), nil - } - return cfg.Default.User, nil } From 6dbf4f98cd7c46457bd4508c182bc13c733ca288 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Thu, 8 Aug 2019 10:07:42 -0700 Subject: [PATCH 149/181] command auth should use user flag if the user flag is provided auth should attempt to authenticate with that user. --- commands/commands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands/commands.go b/commands/commands.go index f3dd32a..9419cc8 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -358,6 +358,9 @@ func CmdClaim(c *cli.Context) error { func CmdAuth(c *cli.Context) error { username := c.Args().Get(0) + if username == "" && c.GlobalIsSet("user") { + username = c.GlobalString("user") + } // Check configuration u, err := config.LoadUser(c) if err != nil { From d8d41a972944afc1768fa3046227f0a9b39c5bf6 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 9 Sep 2019 09:35:51 -0700 Subject: [PATCH 150/181] check for any logged in user when no flag passed this adds a check for any logged in users on any host when neither the user or host flag are passed and not default pair is configured --- cmd/wf/commands.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index 4ffabca..6a4950b 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -48,6 +48,14 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to set user from global config: %v", err), 1) } + } else { + num, err := totalUsersLoggedIn(c) + if err != nil { + return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) + } else if num > 0 { + return cli.NewExitError("You are authenticated, but have no default user/host set. Supply -user and -host flags.", 1) + } + fmt.Println(num) } } u, err := config.LoadUser(c) @@ -89,6 +97,41 @@ func usersLoggedIn(c *cli.Context) (int, []string, error) { return len(names), names, nil } +// totalUsersLoggedIn checks for logged in users for any host +// it returns the number of users and an error if any +func totalUsersLoggedIn(c *cli.Context) (int, error) { + path := config.UserDataDir(c.App.ExtraInfo()["configDir"]) + dir, err := os.Open(path) + if err != nil { + return 0, err + } + contents, err := dir.Readdir(0) + if err != nil { + return 0, err + } + count := 0 + for _, file := range contents { + if file.IsDir() { + subDir, err := os.Open(filepath.Join(path, file.Name())) + if err != nil { + return 0, err + } + subContents, err := subDir.Readdir(0) + if err != nil { + return 0, err + } + for _, subFile := range subContents { + if subFile.IsDir() { + if _, err := os.Stat(filepath.Join(path, file.Name(), subFile.Name(), "user.json")); err == nil { + count++ + } + } + } + } + } + return count, nil +} + func cmdAuth(c *cli.Context) error { err := commands.CmdAuth(c) if err != nil { From 284087a32f37d939d0b49c33f4823a24b57f4814 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 9 Sep 2019 10:32:26 -0700 Subject: [PATCH 151/181] remove println --- cmd/wf/commands.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index 6a4950b..1a9e6c4 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -55,7 +55,6 @@ func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { } else if num > 0 { return cli.NewExitError("You are authenticated, but have no default user/host set. Supply -user and -host flags.", 1) } - fmt.Println(num) } } u, err := config.LoadUser(c) From 86e9757c4b6a731d02304a86e69b0dd86eca33f0 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Mon, 9 Sep 2019 13:07:48 -0700 Subject: [PATCH 152/181] set token on new client for api post --- api/api.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/api.go b/api/api.go index 92cda2b..7d77a5b 100644 --- a/api/api.go +++ b/api/api.go @@ -105,6 +105,13 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ return nil, fmt.Errorf("%v", err) } + u, _ := config.LoadUser(c) + if u != nil && c.App.Name == "wf" { + cl.SetToken(u.AccessToken) + } else { + return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") + } + pp := &writeas.PostParams{ Font: config.GetFont(code, font), Collection: config.Collection(c), From 96f57d52e8bc0cc821d4632e16fe2fd5dfd2bf69 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 08:55:09 -0700 Subject: [PATCH 153/181] fix user auth for writeas posting --- api/api.go | 2 +- config/user.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 7d77a5b..3761658 100644 --- a/api/api.go +++ b/api/api.go @@ -106,7 +106,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ } u, _ := config.LoadUser(c) - if u != nil && c.App.Name == "wf" { + if u != nil { cl.SetToken(u.AccessToken) } else { return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth ") diff --git a/config/user.go b/config/user.go index 45c287d..f39cb73 100644 --- a/config/user.go +++ b/config/user.go @@ -136,6 +136,9 @@ func UserHostDir(c *cli.Context) (string, error) { // CurrentUser returns the username of the user taking action in the current // cli.Context. func CurrentUser(c *cli.Context) (string, error) { + if c.App.Name == "writeas" { + return "user", nil + } // Use user flag value if c.GlobalString("user") != "" { return c.GlobalString("user"), nil From 83bc1cf310965c2a6aa352ae561bbfbfbec6a300 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 09:00:20 -0700 Subject: [PATCH 154/181] fix hostname schema dir issues --- api/api.go | 10 +++++++++- config/options.go | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 3761658..ab665e5 100644 --- a/api/api.go +++ b/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "strings" "github.com/atotto/clipboard" writeas "github.com/writeas/go-writeas/v2" @@ -18,6 +19,9 @@ func HostURL(c *cli.Context) string { return "" } insecure := c.Bool("insecure") + if parts := strings.Split(host, "://"); len(parts) > 1 { + host = parts[1] + } scheme := "https://" if insecure { scheme = "http://" @@ -35,7 +39,11 @@ func newClient(c *cli.Context) (*writeas.Client, error) { if host := HostURL(c); host != "" { clientConfig.URL = host + "/api" } else if cfg.Default.Host != "" && cfg.Default.User != "" { - clientConfig.URL = "https://" + cfg.Default.Host + "/api" + if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { + clientConfig.URL = cfg.Default.Host + "/api" + } else { + clientConfig.URL = "https://" + cfg.Default.Host + "/api" + } } else if config.IsDev() { clientConfig.URL = config.DevBaseURL + "/api" } else if c.App.Name == "writeas" { diff --git a/config/options.go b/config/options.go index 895f518..4232bf9 100644 --- a/config/options.go +++ b/config/options.go @@ -95,10 +95,16 @@ func HostDirectory(c *cli.Context) (string, error) { } // flag takes precedence over defaults if hostFlag := c.GlobalString("host"); hostFlag != "" { + if parts := strings.Split(hostFlag, "://"); len(parts) > 1 { + return parts[1], nil + } return hostFlag, nil } if cfg.Default.Host != "" && cfg.Default.User != "" { + if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { + return parts[1], nil + } return cfg.Default.Host, nil } From 024b7090acf00daca7565decc5af8af522232352 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 09:00:48 -0700 Subject: [PATCH 155/181] clean up CurrentUser global cfg logic --- config/user.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/user.go b/config/user.go index f39cb73..1a90c55 100644 --- a/config/user.go +++ b/config/user.go @@ -159,11 +159,10 @@ func CurrentUser(c *cli.Context) (string, error) { if err != nil { return "", err } - // only user global defaults when both are set and hosts match + // only use global defaults when both are set and no host flag if globalCFG.Default.User != "" && globalCFG.Default.Host != "" && - c.GlobalIsSet("host") && - globalCFG.Default.Host == c.GlobalString("host") { + !c.GlobalIsSet("host") { cfg = globalCFG } } From b60ae1edc079a083b97d696a2b9dd1e389810122 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 10:33:52 -0700 Subject: [PATCH 156/181] get tokens for update and delete this changes DoUpdate and DoDelete to load the user and set the access token in the client when no edit token is provided. --- api/api.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/api.go b/api/api.go index ab665e5..dd550c0 100644 --- a/api/api.go +++ b/api/api.go @@ -225,6 +225,15 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code return fmt.Errorf("%v", err) } + if token == "" { + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return fmt.Errorf("You must either provide and edit token or log in to delete a post.") + } + } + params := writeas.PostParams{} params.Title, params.Content = posts.ExtractTitle(string(post)) if lang := config.Language(c, false); lang != "" { @@ -251,6 +260,15 @@ func DoDelete(c *cli.Context, friendlyID, token string) error { return fmt.Errorf("%v", err) } + if token == "" { + u, _ := config.LoadUser(c) + if u != nil { + cl.SetToken(u.AccessToken) + } else { + return fmt.Errorf("You must either provide and edit token or log in to delete a post.") + } + } + err = cl.DeletePost(friendlyID, token) if err != nil { if config.Debug() { From 05380e618fd0d46690b70d5f84eaa280c0f9507a Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 10:59:36 -0700 Subject: [PATCH 157/181] fix post url for non standard hostnames --- commands/commands.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index 9419cc8..751e984 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "text/tabwriter" "github.com/howeyc/gopass" @@ -262,9 +263,28 @@ func CmdListPosts(c *cli.Context) error { } func getPostURL(c *cli.Context, slug string) string { - base := config.WriteasBaseURL - if config.IsDev() { - base = config.DevBaseURL + var base string + if c.App.Name == "writeas" { + if config.IsDev() { + base = config.DevBaseURL + } else { + base = config.WriteasBaseURL + } + } else { + if host := api.HostURL(c); host != "" { + base = host + } else { + // TODO handle error, or load config globally, see T601 + // https://phabricator.write.as/T601 + cfg, _ := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) + if cfg.Default.Host != "" && cfg.Default.User != "" { + if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { + base = cfg.Default.Host + } else { + base = "https://" + cfg.Default.Host + } + } + } } ext := "" // Output URL in requested format From 47d8bb3e6d6e1ff7bc3424986316348cf583ab3d Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Tue, 10 Sep 2019 11:06:43 -0700 Subject: [PATCH 158/181] wf/post list: draft posts not anonymous --- commands/commands.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index 751e984..29ddc21 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -207,7 +207,11 @@ func CmdListPosts(c *cli.Context) error { } if len(remotePosts) > 0 { - fmt.Println("Anonymous Posts") + if c.App.Name == "wf" { + fmt.Println("Draft Posts") + } else { + fmt.Println("Anonymous Posts") + } if details { identifier := "URL" if ids || !urls { From 253d2e9782284c7050805da736385c197b9ba034 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 13 Sep 2019 12:32:14 -0700 Subject: [PATCH 159/181] add accounts command in wf this adds a new sub command accounts, which will list all currently authenticated user accounts grouped by hostname --- cmd/wf/commands.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/wf/main.go | 10 ++++++ go.mod | 1 + go.sum | 4 +++ 4 files changed, 93 insertions(+) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index 1a9e6c4..b3cb57f 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -2,10 +2,13 @@ package main import ( "fmt" + "io/ioutil" "os" "path/filepath" "strings" + "text/tabwriter" + "github.com/hashicorp/go-multierror" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" @@ -196,3 +199,78 @@ func cmdLogOut(c *cli.Context) error { return nil } + +func cmdAccounts(c *cli.Context) error { + // get user config dir + userDir := config.UserDataDir(c.App.ExtraInfo()["configDir"]) + // load defaults + cfg, err := config.LoadConfig(userDir) + if err != nil { + return cli.NewExitError("Could not load default user configuration", 1) + } + defaultUser := cfg.Default.User + defaultHost := cfg.Default.Host + if parts := strings.Split(defaultHost, "://"); len(parts) > 1 { + defaultHost = parts[1] + } + // get each host dir + files, err := ioutil.ReadDir(userDir) + if err != nil { + return cli.NewExitError("Could not read user configuration directory", 1) + } + // accounts will be a slice of slices of string. the first string in + // a subslice should always be the hostname + accounts := [][]string{} + for _, file := range files { + if file.IsDir() { + dirName := file.Name() + // get each user in host dir + users, err := usersFromDir(filepath.Join(userDir, dirName)) + if err != nil { + log.Info(c, "Failed to get users from %s: %v", dirName, err) + continue + } + if len(users) != 0 { + // append the slice of users as a new slice in accounts w/ the host prepended + accounts = append(accounts, append([]string{dirName}, users...)) + } + } + } + + // print out all logged in accounts + tw := tabwriter.NewWriter(os.Stdout, 10, 2, 2, ' ', tabwriter.TabIndent) + if len(accounts) == 0 { + fmt.Fprintf(tw, "%s\t", "No authenticated accounts found.") + } + for _, userList := range accounts { + host := userList[0] + for _, username := range userList[1:] { + if host == defaultHost && username == defaultUser { + fmt.Fprintf(tw, "[%s]\t%s (default)\n", host, username) + continue + } + fmt.Fprintf(tw, "[%s]\t%s\n", host, username) + } + } + return tw.Flush() +} + +func usersFromDir(path string) ([]string, error) { + users := make([]string, 0, 4) + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + var errs error + for _, file := range files { + if file.IsDir() { + _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")) + if err != nil { + err = multierror.Append(errs, err) + continue + } + users = append(users, file.Name()) + } + } + return users, errs +} diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 7cce4c9..ac726cf 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -206,6 +206,16 @@ func main() { Usage: "Make the operation more talkative", }, }, + }, { + Name: "accounts", + Usage: "List all currently logged in accounts", + Action: cmdAccounts, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, }, { Name: "auth", Usage: "Authenticate with a WriteFreely instance", diff --git a/go.mod b/go.mod index 90d8f91..f03be5d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/atotto/clipboard v0.1.1 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/hashicorp/go-multierror v1.0.0 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/microcosm-cc/bluemonday v1.0.1 // indirect diff --git a/go.sum b/go.sum index 86825a8..449244e 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= From 1123705eb7ede3260d13ee4ba7c093f574202ffa Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 18 Sep 2019 16:34:30 -0400 Subject: [PATCH 160/181] Fix un-auth'd messages on various commands --- cmd/wf/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index 7cce4c9..ef9d8f7 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -79,7 +79,7 @@ func main() { { Name: "delete", Usage: "Delete a post", - Action: requireAuth(commands.CmdDelete, "delete"), + Action: requireAuth(commands.CmdDelete, "delete a post"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -99,7 +99,7 @@ func main() { { Name: "update", Usage: "Update (overwrite) a post", - Action: requireAuth(commands.CmdUpdate, "update"), + Action: requireAuth(commands.CmdUpdate, "update a post"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -148,7 +148,7 @@ func main() { Name: "posts", Usage: "List all of your posts", Description: "This will list only local posts.", - Action: requireAuth(commands.CmdListPosts, "posts"), + Action: requireAuth(commands.CmdListPosts, "view posts"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", @@ -170,7 +170,7 @@ func main() { }, { Name: "blogs", Usage: "List blogs", - Action: requireAuth(commands.CmdCollections, "blogs"), + Action: requireAuth(commands.CmdCollections, "view blogs"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "tor, t", @@ -189,7 +189,7 @@ func main() { }, { Name: "claim", Usage: "Claim local unsynced posts", - Action: requireAuth(commands.CmdClaim, "claim"), + Action: requireAuth(commands.CmdClaim, "claim unsynced posts"), Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.", Flags: []cli.Flag{ cli.BoolFlag{ From ff67ed2bb8c63a5da2eacf8b97788ca50614615c Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 18 Sep 2019 14:10:49 -0700 Subject: [PATCH 161/181] remove default collection of username --- config/options.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/options.go b/config/options.go index 4232bf9..9a163a6 100644 --- a/config/options.go +++ b/config/options.go @@ -79,10 +79,6 @@ func Collection(c *cli.Context) string { if coll := c.String("b"); coll != "" { return coll } - u, _ := LoadUser(c) - if u != nil { - return u.User.Username - } return "" } From 400b6fe14a9cb7ac82d612eec9790599194ad6cf Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 13:01:34 -0400 Subject: [PATCH 162/181] Remove wf claim command This isn't needed / used in WriteFreely. --- cmd/wf/main.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index ef9d8f7..aa04c84 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -186,26 +186,6 @@ func main() { Usage: "Show list with URLs", }, }, - }, { - Name: "claim", - Usage: "Claim local unsynced posts", - Action: requireAuth(commands.CmdClaim, "claim unsynced posts"), - Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "tor, t", - Usage: "Authenticate via Tor hidden service", - }, - cli.IntFlag{ - Name: "tor-port", - Usage: "Use a different port to connect to Tor", - Value: 9150, - }, - cli.BoolFlag{ - Name: "verbose, v", - Usage: "Make the operation more talkative", - }, - }, }, { Name: "auth", Usage: "Authenticate with a WriteFreely instance", From 5ac95a0c41704c81ca4e8065496b006607e9d42e Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 13:17:35 -0400 Subject: [PATCH 163/181] Tweak wf flag help messages --- cmd/wf/flags.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go index 5245bb7..8df2a85 100644 --- a/cmd/wf/flags.go +++ b/cmd/wf/flags.go @@ -7,10 +7,10 @@ import ( var flags = []cli.Flag{ cli.StringFlag{ Name: "host, H", - Usage: "Operate against a custom hostname", + Usage: "Use the given WriteFreely instance hostname", }, cli.StringFlag{ Name: "user, u", - Usage: "Use authenticated user, other than default", + Usage: "Use the given account username", }, } From 139a14154b44cc02e35fc3f527dd65bf553425d8 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Fri, 13 Sep 2019 12:32:14 -0700 Subject: [PATCH 164/181] add accounts command in wf this adds a new sub command accounts, which will list all currently authenticated user accounts grouped by hostname --- cmd/wf/commands.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/wf/main.go | 10 ++++++ go.mod | 1 + go.sum | 4 +++ 4 files changed, 93 insertions(+) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index 1a9e6c4..b3cb57f 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -2,10 +2,13 @@ package main import ( "fmt" + "io/ioutil" "os" "path/filepath" "strings" + "text/tabwriter" + "github.com/hashicorp/go-multierror" "github.com/writeas/writeas-cli/api" "github.com/writeas/writeas-cli/commands" "github.com/writeas/writeas-cli/config" @@ -196,3 +199,78 @@ func cmdLogOut(c *cli.Context) error { return nil } + +func cmdAccounts(c *cli.Context) error { + // get user config dir + userDir := config.UserDataDir(c.App.ExtraInfo()["configDir"]) + // load defaults + cfg, err := config.LoadConfig(userDir) + if err != nil { + return cli.NewExitError("Could not load default user configuration", 1) + } + defaultUser := cfg.Default.User + defaultHost := cfg.Default.Host + if parts := strings.Split(defaultHost, "://"); len(parts) > 1 { + defaultHost = parts[1] + } + // get each host dir + files, err := ioutil.ReadDir(userDir) + if err != nil { + return cli.NewExitError("Could not read user configuration directory", 1) + } + // accounts will be a slice of slices of string. the first string in + // a subslice should always be the hostname + accounts := [][]string{} + for _, file := range files { + if file.IsDir() { + dirName := file.Name() + // get each user in host dir + users, err := usersFromDir(filepath.Join(userDir, dirName)) + if err != nil { + log.Info(c, "Failed to get users from %s: %v", dirName, err) + continue + } + if len(users) != 0 { + // append the slice of users as a new slice in accounts w/ the host prepended + accounts = append(accounts, append([]string{dirName}, users...)) + } + } + } + + // print out all logged in accounts + tw := tabwriter.NewWriter(os.Stdout, 10, 2, 2, ' ', tabwriter.TabIndent) + if len(accounts) == 0 { + fmt.Fprintf(tw, "%s\t", "No authenticated accounts found.") + } + for _, userList := range accounts { + host := userList[0] + for _, username := range userList[1:] { + if host == defaultHost && username == defaultUser { + fmt.Fprintf(tw, "[%s]\t%s (default)\n", host, username) + continue + } + fmt.Fprintf(tw, "[%s]\t%s\n", host, username) + } + } + return tw.Flush() +} + +func usersFromDir(path string) ([]string, error) { + users := make([]string, 0, 4) + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + var errs error + for _, file := range files { + if file.IsDir() { + _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")) + if err != nil { + err = multierror.Append(errs, err) + continue + } + users = append(users, file.Name()) + } + } + return users, errs +} diff --git a/cmd/wf/main.go b/cmd/wf/main.go index ef9d8f7..5729d44 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -206,6 +206,16 @@ func main() { Usage: "Make the operation more talkative", }, }, + }, { + Name: "accounts", + Usage: "List all currently logged in accounts", + Action: cmdAccounts, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "verbose, v", + Usage: "Make the operation more talkative", + }, + }, }, { Name: "auth", Usage: "Authenticate with a WriteFreely instance", diff --git a/go.mod b/go.mod index 90d8f91..f03be5d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/atotto/clipboard v0.1.1 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/hashicorp/go-multierror v1.0.0 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/microcosm-cc/bluemonday v1.0.1 // indirect diff --git a/go.sum b/go.sum index 86825a8..449244e 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= From 7fde5dd91b009a3486a7e3d518b5294eb67655b8 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 13:44:41 -0400 Subject: [PATCH 165/181] Clean up wf posts subcommand - Remove description - Fix usage (it only shows drafts) - Remove --md flag (Markdown is always rendered on draft posts) - Fix --verbose description --- cmd/wf/main.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cmd/wf/main.go b/cmd/wf/main.go index aa04c84..80e2f4d 100644 --- a/cmd/wf/main.go +++ b/cmd/wf/main.go @@ -145,26 +145,21 @@ func main() { }, }, { - Name: "posts", - Usage: "List all of your posts", - Description: "This will list only local posts.", - Action: requireAuth(commands.CmdListPosts, "view posts"), + Name: "posts", + Usage: "List draft posts", + Action: requireAuth(commands.CmdListPosts, "view posts"), Flags: []cli.Flag{ cli.BoolFlag{ Name: "id", Usage: "Show list with post IDs (default)", }, - cli.BoolFlag{ - Name: "md", - Usage: "Use with --url to return URLs with Markdown enabled", - }, cli.BoolFlag{ Name: "url", Usage: "Show list with URLs", }, cli.BoolFlag{ Name: "verbose, v", - Usage: "Show verbose post listing, including Edit Tokens", + Usage: "Show verbose post listing", }, }, }, { From c9f20e27f7110451d7ae93959a201f144ab20c38 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 13:57:59 -0400 Subject: [PATCH 166/181] Have `accounts` print nothing when unauth'd This replicates behavior elsewhere, where we don't output anything if there is no information to show. Instead, the "no accounts" message will only show if the user supplies a -v / --verbose flag. --- cmd/wf/commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go index b3cb57f..c55157c 100644 --- a/cmd/wf/commands.go +++ b/cmd/wf/commands.go @@ -239,8 +239,8 @@ func cmdAccounts(c *cli.Context) error { // print out all logged in accounts tw := tabwriter.NewWriter(os.Stdout, 10, 2, 2, ' ', tabwriter.TabIndent) - if len(accounts) == 0 { - fmt.Fprintf(tw, "%s\t", "No authenticated accounts found.") + if len(accounts) == 0 && (c.Bool("v") || c.Bool("verbose") || c.GlobalBool("v") || c.GlobalBool("verbose")) { + fmt.Fprintf(tw, "%s\t", "No authenticated accounts found.\n") } for _, userList := range accounts { host := userList[0] From 4d7494a1c3463d1b6e850ca1f560a56d1951ad3d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 14:24:38 -0400 Subject: [PATCH 167/181] Split up READMEs and GUIDEs for writeas-cli and wf-cli. --- GUIDE.md | 156 ++-------------------------------------- README.md | 96 +++++-------------------- cmd/wf/GUIDE.md | 164 ++++++++++++++++++++++++++++++++++++++++++ cmd/wf/README.md | 109 ++++++++++++++++++++++++++++ cmd/writeas/GUIDE.md | 149 ++++++++++++++++++++++++++++++++++++++ cmd/writeas/README.md | 109 ++++++++++++++++++++++++++++ 6 files changed, 555 insertions(+), 228 deletions(-) create mode 100644 cmd/wf/GUIDE.md create mode 100644 cmd/wf/README.md create mode 100644 cmd/writeas/GUIDE.md create mode 100644 cmd/writeas/README.md diff --git a/GUIDE.md b/GUIDE.md index 68d29d1..a385e3a 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -1,153 +1,9 @@ -# Write.as CLI User Guide +# Write.as / WriteFreely CLI User Guide -The Write.as Command-Line Interface (CLI) is a cross-platform tool for publishing text to [Write.as](https://write.as) and its other sites, like [Paste.as](https://paste.as). It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools. +**This has been split into two user guides:** -Write.as is a text-publishing service that protects your privacy. There's no sign up required to publish, but if you do sign up, you can access posts across devices and compile collections of them in what most people would call a "blog". +## Write.as CLI +See full usage documentation on our [writeas-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md). -## Uses - -These are a few common uses for `writeas`. If you get stuck or want to know more, run `writeas [command] --help`. If you still have questions, [ask us](https://write.as/contact). - -### Overview - -``` - writeas [global options] command [command options] [arguments...] - -COMMANDS: - post Alias for default action: create post from stdin - new Compose a new post from the command-line and publish - publish Publish a file to Write.as - delete Delete a post - update Update (overwrite) a post - get Read a raw post - add Add an existing post locally - posts List all of your posts - claim Claim local unsynced posts - blogs List blogs - claim Claim local unsynced posts - auth Authenticate with Write.as - logout Log out of Write.as - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - -c value, -b value Optional blog to post to - --tor, -t Perform action on Tor hidden service - --tor-port value Use a different port to connect to Tor (default: 9150) - --code Specifies this post is code - --md Returns post URL with Markdown enabled - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --lang value Sets post language to given ISO 639-1 language code - --user-agent value Sets the User-Agent for API requests - --host value, -H value Operate against a custom hostname - --user value, -u value Use authenticated user, other than default - --help, -h show help - --version, -V print the version -``` - -> Note: the host and user flags are only available in `writefreely`. - -#### Share something - -By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard: - -```bash -$ echo "Hello world!" | writeas -https://write.as/aaaazzzzzzzza -``` - -This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting): - -macOS / Linux: `cat writeas/cli.go | writeas --code` - -Windows: `type writeas/cli.go | writeas.exe --code` - -#### Output a post - -This outputs any Write.as post with the given ID. - -```bash -$ writeas get aaaazzzzzzzza -Hello world! -``` - -#### Authenticate - -This will authenticate with write.as and store the user access token locally, until you explicitly logout. -```bash -$ writeas auth username -Password: ************ -``` - -#### List all blogs - -This will output a list of the authenticated user's blogs. -```bash -$ writeas blogs -Alias Title -user An Example Blog -dev My Dev Log -``` - -#### List posts - -This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts. - -Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. - -To see post IDs with their Edit Tokens pass the `--v` flag. - -```bash -$ writeas posts -aaaazzzzzzzza - -$ writeas posts -url -https://write.as/aaaazzzzzzzza - -$ writeas posts -v -ID Token -aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j -``` - -#### Delete a post - -This permanently deletes a post you own. - -```bash -$ writeas delete aaaazzzzzzzza -``` - -#### Update a post - -This completely overwrites an existing post you own. - -```bash -$ echo "See you later!" | writeas update aaaazzzzzzzza -``` - -#### Claim a post - -This moves an unsynced local post to a draft on your account. You will need to authenticate first. -```bash -$ writeas claim aaaazzzzzzzza -``` - -### Composing posts - -If you simply have a penchant for never leaving your keyboard, `writeas` is great for composing new posts from the command-line. Just use the `new` subcommand. - -`writeas new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix. - -Customize your post's appearance with the `--font` flag: - -| Argument | Appearance (Typeface) | Word Wrap? | -| -------- | --------------------- | ---------- | -| `sans` | Sans-serif (Open Sans) | Yes | -| `serif` | Serif (Lora) | Yes | -| `wrap` | Monospace | Yes | -| `mono` | Monospace | No | -| `code` | Syntax-highlighted monospace | No | - -Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans` - -If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md` +## WriteFreely CLI +See full usage documentation on our [wf-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md). diff --git a/README.md b/README.md index a39a162..97a4a34 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ -writeas-cli -=========== +writeas-cli / wf-cli +==================== ![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development) -Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. +Command line utility for publishing to [Write.as](https://write.as) and any other [WriteFreely](https://writefreely.org) instance. Works on Windows, macOS, and Linux. ## Features -* Publish anonymously to Write.as -* Authenticate with a Write.as account +* Authenticate with a Write.as / WriteFreely account +* Publish anonymous posts or drafts to Write.as or WriteFreely, respectively * A stable, easy back-end for your [GUI app](https://write.as/apps/desktop) or desktop-based workflow -* Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/) -* Locally keeps track of any posts you make -* Update and delete posts, anonymous and authenticated +* Compatible with the [Write.as Tor hidden service](http://writeas7pm7rcdqg.onion/) +* Update and delete posts * Fetch any post by ID -* Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing +* ...and more, depending on which client you're using (see respective READMEs for more) ## Installing The easiest way to get the CLI is to download a pre-built executable for your OS. @@ -23,78 +22,19 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. -**Windows**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. +**Write.as CLI**
+See the [writeas-cli README](https://github.com/writeas/writeas-cli/cmd/writeas#readme) -**macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. - -**Debian-based Linux**
-```bash -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445 -sudo add-apt-repository "deb http://updates.writeas.org xenial main" -sudo apt-get update && sudo apt-get install writeas-cli -``` - -**Linux (other)**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. - -### Go get it -```bash -go get github.com/writeas/writeas-cli/cmd/writeas -``` - -Once this finishes, you'll see `writeas` or `writeas.exe` inside `$GOPATH/bin/`. - -## Upgrading - -To upgrade the CLI, download and replace the executable you downloaded before. - -If you previously installed with `go get`, run it again with the `-u` option. - -```bash -go get -u github.com/writeas/writeas-cli/cmd/writeas -``` +**WriteFreely CLI**
+See the [wf-cli README](https://github.com/writeas/writeas-cli/cmd/wf#readme) ## Usage -See full usage documentation on our [User Guide](GUIDE.md). - -``` - writeas [global options] command [command options] [arguments...] - -COMMANDS: - post Alias for default action: create post from stdin - new Compose a new post from the command-line and publish - publish Publish a file to Write.as - delete Delete a post - update Update (overwrite) a post - get Read a raw post - add Add an existing post locally - posts List all of your posts - blogs List blogs - claim Claim local unsynced posts - auth Authenticate with Write.as - logout Log out of Write.as - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - -c value, -b value Optional blog to post to - --tor, -t Perform action on Tor hidden service - --tor-port value Use a different port to connect to Tor (default: 9150) - --code Specifies this post is code - --md Returns post URL with Markdown enabled - --verbose, -v Make the operation more talkative - --font value Sets post font to given value (default: "mono") - --lang value Sets post language to given ISO 639-1 language code - --user-agent value Sets the User-Agent for API requests - --host value, -H value Operate against a custom hostname - --user value, -u value Use authenticated user, other than default - --help, -h show help - --version, -V print the version -``` - -> Note: the host and user flags are only available in `wf` the community edition +**Write.as CLI**
+See full usage documentation on our [writeas-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md). + +**WriteFreely CLI**
+See full usage documentation on our [wf-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md). ## Contributing to the CLI @@ -108,4 +48,4 @@ We're available on [several channels](https://write.as/contact), and prefer our ### Reporting Issues -If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version`) in your report. +If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version` or `wf --version`) in your report. diff --git a/cmd/wf/GUIDE.md b/cmd/wf/GUIDE.md new file mode 100644 index 0000000..48e4615 --- /dev/null +++ b/cmd/wf/GUIDE.md @@ -0,0 +1,164 @@ +# WriteFreely CLI User Guide + +The WriteFreely Command-Line Interface (CLI) is a cross-platform tool for publishing text to any [WriteFreely](https://writefreely.org) instance. It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools. + +WriteFreely is the software behind [Write.as](https://write.as). While the WriteFreely CLI supports publishing to Write.as, we recommend using the dedicated [Write.as CLI](https://github.com/writeas/writeas-cli/tree/master/cmd/writeas#readme) to get the full features of the platform, including anonymous publishing. + +**The WriteFreely CLI is compatible with WriteFreely v0.11 or later.** + +## Uses + +These are a few common uses for `wf`. If you get stuck or want to know more, run `wf [command] --help`. If you still have questions, [ask us](https://write.as/contact). + +### Overview + +``` + wf [global options] command [command options] [arguments...] + +COMMANDS: + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file + delete Delete a post + update Update (overwrite) a post + get Read a raw post + posts List all of your posts + blogs List blogs + auth Authenticate with a WriteFreely instance + logout Log out of a WriteFreely instance + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + -c value, -b value Optional blog to post to + --insecure Send request insecurely. + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --host value, -H value Use the given WriteFreely instance hostname + --user value, -u value Use the given account username + --help, -h show help + --version, -V print the version +``` + +#### Authenticate + +To use the WriteFreely CLI, you'll first need to authenticate with the WriteFreely instance you want to interact with. + +You may authenticate with as many WriteFreely instances and accounts as you want. But the first account you authenticate with will automatically be set as the default instance to operate on, so you don't have to supply `--host` and `--user` with every command. + +```bash +$ wf --host pencil.writefree.ly auth username +Password: ************ +``` + +In this example, you'll be authenticated as the user **username** on the WriteFreely instance **https://pencil.writefree.ly**. + +#### Choosing an account + +To select the WriteFreely instance and account you want to interact with, supply the `--host` and `--user` flags at the beginning of your `wf` command, e.g.: + +``` +$ wf --host pencil.writefree.ly --user username +``` + +If you're authenticated with only one account on any given WriteFreely instance, you only need to supply the `--host`, and `wf` will automatically use the correct account. E.g.: + +``` +$ wf --host pencil.writefree.ly +``` + +If a default account is set in `~/.writefreely/config.ini` and you want to use it, you don't need to supply any additional arguments. E.g.: + +``` +$ wf +``` + +#### Share something + +By default, `wf` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard. + +```bash +$ echo "Hello world!" | wf +https://pencil.writefree.ly/aaaaazzzzz +``` + +This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting): + +macOS / Linux: `cat cmd/wf/cli.go | wf --code` + +Windows: `type cmd/wf/cli.go | wf.exe --code` + +#### Output a post + +This outputs any WriteFreely post with the given ID. + +```bash +$ wf get aaaaazzzzz +Hello world! +``` + +#### List all blogs + +This will output a list of the authenticated user's blogs. +```bash +$ wf blogs +Alias Title +user An Example Blog +dev My Dev Log +``` + +#### List posts + +This lists all draft posts you've published. + +Pass the `--url` flag to show the list with full post URLs. + +```bash +$ wf posts +aaaaazzzzz + +$ wf posts -url +https://pencil.writefree.ly/aaaaazzzzz + +$ wf posts +ID +aaaaazzzzz +``` + +#### Delete a post + +This permanently deletes a post with the given ID. + +```bash +$ wf delete aaaaazzzzz +``` + +#### Update a post + +This completely overwrites an existing post with the given ID. + +```bash +$ echo "See you later!" | wf update aaaaazzzzz +``` + +### Composing posts + +If you simply have a penchant for never leaving your keyboard, `wf` is great for composing new posts from the command-line. Just use the `new` subcommand. + +`wf new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix. + +Customize your post's appearance with the `--font` flag: + +| Argument | Appearance (Typeface) | Word Wrap? | +| -------- | --------------------- | ---------- | +| `sans` | Sans-serif (Open Sans) | Yes | +| `serif` | Serif (Lora) | Yes | +| `wrap` | Monospace | Yes | +| `mono` | Monospace | No | +| `code` | Syntax-highlighted monospace | No | + +Put it all together, e.g. publish with a sans-serif font: `wf new --font sans` diff --git a/cmd/wf/README.md b/cmd/wf/README.md new file mode 100644 index 0000000..a725adc --- /dev/null +++ b/cmd/wf/README.md @@ -0,0 +1,109 @@ +wf-cli +====== +![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development) + +Command line utility for publishing to any [WriteFreely](https://writefreely.org) instance. Works on Windows, macOS, and Linux. + +**The WriteFreely CLI is compatible with WriteFreely v0.11 or later.** + +## Features + +* Authenticate with any WriteFreely instance +* Publish drafts +* Manage multiple WriteFreely accounts on multiple instances +* A stable, easy back-end for your GUI app or desktop-based workflow +* Locally keeps track of any posts you make +* Update and delete posts +* Fetch any post by ID + +## Installing +The easiest way to get the CLI is to download a pre-built executable for your OS. + +### Download +[![Latest release](https://img.shields.io/github/release/writeas/writeas-cli.svg)](https://github.com/writeas/writeas-cli/releases/latest) ![Total downloads](https://img.shields.io/github/downloads/writeas/writeas-cli/total.svg) + +Get the latest version for your operating system as a standalone executable. + +**Windows**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. + +**macOS**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. + +**Debian-based Linux**
+```bash +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445 +sudo add-apt-repository "deb http://updates.writeas.org xenial main" +sudo apt-get update && sudo apt-get install wf-cli +``` + +**Linux (other)**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. + +### Go get it +```bash +go get github.com/writeas/writeas-cli/cmd/wf +``` + +Once this finishes, you'll see `wf` or `wf.exe` inside `$GOPATH/bin/`. + +## Upgrading + +To upgrade the CLI, download and replace the executable you downloaded before. + +If you previously installed with `go get`, run it again with the `-u` option. + +```bash +go get -u github.com/writeas/writeas-cli/cmd/wf +``` + +## Usage + +See full usage documentation on our [User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md). + +``` + wf [global options] command [command options] [arguments...] + +COMMANDS: + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file + delete Delete a post + update Update (overwrite) a post + get Read a raw post + posts List draft posts + blogs List blogs + accounts List all currently logged in accounts + auth Authenticate with a WriteFreely instance + logout Log out of a WriteFreely instance + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + -c value, -b value Optional blog to post to + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --md Returns post URL with Markdown enabled + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --host value, -H value Operate against a custom hostname + --user value, -u value Use authenticated user, other than default + --help, -h show help + --version, -V print the version +``` + +## Contributing to the CLI + +For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). + +We welcome any kind of contributions including documentation, organizational improvements, tutorials, bug reports, feature requests, new features, answering questions, etc. + +### Getting Support + +We're available on [several channels](https://write.as/contact), and prefer our [forum](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. + +### Reporting Issues + +If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `wf --version`) in your report. diff --git a/cmd/writeas/GUIDE.md b/cmd/writeas/GUIDE.md new file mode 100644 index 0000000..fae366e --- /dev/null +++ b/cmd/writeas/GUIDE.md @@ -0,0 +1,149 @@ +# Write.as CLI User Guide + +The Write.as Command-Line Interface (CLI) is a cross-platform tool for publishing text to [Write.as](https://write.as) and its other sites, like [Paste.as](https://paste.as). It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools. + +Write.as is a text-publishing service that protects your privacy. There's no sign up required to publish, but if you do sign up, you can access posts across devices and compile collections of them in what most people would call a "blog". + +## Uses + +These are a few common uses for `writeas`. If you get stuck or want to know more, run `writeas [command] --help`. If you still have questions, [ask us](https://write.as/contact). + +### Overview + +``` + writeas [global options] command [command options] [arguments...] + +COMMANDS: + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file to Write.as + delete Delete a post + update Update (overwrite) a post + get Read a raw post + add Add an existing post locally + posts List all of your posts + claim Claim local unsynced posts + blogs List blogs + claim Claim local unsynced posts + auth Authenticate with Write.as + logout Log out of Write.as + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + -c value, -b value Optional blog to post to + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --md Returns post URL with Markdown enabled + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --help, -h show help + --version, -V print the version +``` + +#### Share something + +By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard: + +```bash +$ echo "Hello world!" | writeas +https://write.as/aaaazzzzzzzza +``` + +This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting): + +macOS / Linux: `cat writeas/cli.go | writeas --code` + +Windows: `type writeas/cli.go | writeas.exe --code` + +#### Output a post + +This outputs any Write.as post with the given ID. + +```bash +$ writeas get aaaazzzzzzzza +Hello world! +``` + +#### Authenticate + +This will authenticate with write.as and store the user access token locally, until you explicitly logout. +```bash +$ writeas auth username +Password: ************ +``` + +#### List all blogs + +This will output a list of the authenticated user's blogs. +```bash +$ writeas blogs +Alias Title +user An Example Blog +dev My Dev Log +``` + +#### List posts + +This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts. + +Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled. + +To see post IDs with their Edit Tokens pass the `--v` flag. + +```bash +$ writeas posts +aaaazzzzzzzza + +$ writeas posts -url +https://write.as/aaaazzzzzzzza + +$ writeas posts -v +ID Token +aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j +``` + +#### Delete a post + +This permanently deletes a post you own. + +```bash +$ writeas delete aaaazzzzzzzza +``` + +#### Update a post + +This completely overwrites an existing post you own. + +```bash +$ echo "See you later!" | writeas update aaaazzzzzzzza +``` + +#### Claim a post + +This moves an unsynced local post to a draft on your account. You will need to authenticate first. +```bash +$ writeas claim aaaazzzzzzzza +``` + +### Composing posts + +If you simply have a penchant for never leaving your keyboard, `writeas` is great for composing new posts from the command-line. Just use the `new` subcommand. + +`writeas new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix. + +Customize your post's appearance with the `--font` flag: + +| Argument | Appearance (Typeface) | Word Wrap? | +| -------- | --------------------- | ---------- | +| `sans` | Sans-serif (Open Sans) | Yes | +| `serif` | Serif (Lora) | Yes | +| `wrap` | Monospace | Yes | +| `mono` | Monospace | No | +| `code` | Syntax-highlighted monospace | No | + +Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans` + +If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md` diff --git a/cmd/writeas/README.md b/cmd/writeas/README.md new file mode 100644 index 0000000..8ec40e8 --- /dev/null +++ b/cmd/writeas/README.md @@ -0,0 +1,109 @@ +writeas-cli +=========== +![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development) + +Command line utility for publishing to [Write.as](https://write.as). Works on Windows, macOS, and Linux. + +## Features + +* Publish anonymously to Write.as +* Authenticate with a Write.as account +* A stable, easy back-end for your [GUI app](https://write.as/apps/desktop) or desktop-based workflow +* Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/) +* Locally keeps track of any posts you make +* Update and delete posts, anonymous and authenticated +* Fetch any post by ID +* Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing + +## Installing +The easiest way to get the CLI is to download a pre-built executable for your OS. + +### Download +[![Latest release](https://img.shields.io/github/release/writeas/writeas-cli.svg)](https://github.com/writeas/writeas-cli/releases/latest) ![Total downloads](https://img.shields.io/github/downloads/writeas/writeas-cli/total.svg) + +Get the latest version for your operating system as a standalone executable. + +**Windows**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. + +**macOS**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. + +**Debian-based Linux**
+```bash +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445 +sudo add-apt-repository "deb http://updates.writeas.org xenial main" +sudo apt-get update && sudo apt-get install writeas-cli +``` + +**Linux (other)**
+Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. + +### Go get it +```bash +go get github.com/writeas/writeas-cli/cmd/writeas +``` + +Once this finishes, you'll see `writeas` or `writeas.exe` inside `$GOPATH/bin/`. + +## Upgrading + +To upgrade the CLI, download and replace the executable you downloaded before. + +If you previously installed with `go get`, run it again with the `-u` option. + +```bash +go get -u github.com/writeas/writeas-cli/cmd/writeas +``` + +## Usage + +See full usage documentation on our [User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md). + +``` + writeas [global options] command [command options] [arguments...] + +COMMANDS: + post Alias for default action: create post from stdin + new Compose a new post from the command-line and publish + publish Publish a file to Write.as + delete Delete a post + update Update (overwrite) a post + get Read a raw post + add Add an existing post locally + posts List all of your posts + blogs List blogs + claim Claim local unsynced posts + auth Authenticate with Write.as + logout Log out of Write.as + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + -c value, -b value Optional blog to post to + --tor, -t Perform action on Tor hidden service + --tor-port value Use a different port to connect to Tor (default: 9150) + --code Specifies this post is code + --md Returns post URL with Markdown enabled + --verbose, -v Make the operation more talkative + --font value Sets post font to given value (default: "mono") + --lang value Sets post language to given ISO 639-1 language code + --user-agent value Sets the User-Agent for API requests + --host value, -H value Operate against a custom hostname + --user value, -u value Use authenticated user, other than default + --help, -h show help + --version, -V print the version +``` + +## Contributing to the CLI + +For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). + +We welcome any kind of contributions including documentation, organizational improvements, tutorials, bug reports, feature requests, new features, answering questions, etc. + +### Getting Support + +We're available on [several channels](https://write.as/contact), and prefer our [forum](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. + +### Reporting Issues + +If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version`) in your report. From 0e21d37a243af207d53a6237f0d08da78be849a0 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 14:26:26 -0400 Subject: [PATCH 168/181] Remove common endings on wf/writeas READMEs --- cmd/wf/README.md | 14 -------------- cmd/writeas/README.md | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/cmd/wf/README.md b/cmd/wf/README.md index a725adc..64be198 100644 --- a/cmd/wf/README.md +++ b/cmd/wf/README.md @@ -93,17 +93,3 @@ GLOBAL OPTIONS: --help, -h show help --version, -V print the version ``` - -## Contributing to the CLI - -For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). - -We welcome any kind of contributions including documentation, organizational improvements, tutorials, bug reports, feature requests, new features, answering questions, etc. - -### Getting Support - -We're available on [several channels](https://write.as/contact), and prefer our [forum](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. - -### Reporting Issues - -If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `wf --version`) in your report. diff --git a/cmd/writeas/README.md b/cmd/writeas/README.md index 8ec40e8..63cc7d4 100644 --- a/cmd/writeas/README.md +++ b/cmd/writeas/README.md @@ -93,17 +93,3 @@ GLOBAL OPTIONS: --help, -h show help --version, -V print the version ``` - -## Contributing to the CLI - -For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). - -We welcome any kind of contributions including documentation, organizational improvements, tutorials, bug reports, feature requests, new features, answering questions, etc. - -### Getting Support - -We're available on [several channels](https://write.as/contact), and prefer our [forum](https://discuss.write.as) for project discussion. Please don't use the GitHub issue tracker to ask questions. - -### Reporting Issues - -If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version`) in your report. From 44a8f6e943b50815315bc515a62717eaafdfdeed Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 20 Sep 2019 14:27:05 -0400 Subject: [PATCH 169/181] Fix README links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 97a4a34..1e76ed6 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. **Write.as CLI**
-See the [writeas-cli README](https://github.com/writeas/writeas-cli/cmd/writeas#readme) +See the [writeas-cli README](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas#readme) **WriteFreely CLI**
-See the [wf-cli README](https://github.com/writeas/writeas-cli/cmd/wf#readme) +See the [wf-cli README](https://github.com/writeas/writeas-cli/blob/master/cmd/wf#readme) ## Usage From e8fb68311ac8897056c44dae9427749296bc4cc9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 21 Sep 2019 11:12:29 -0400 Subject: [PATCH 170/181] Fix wf-cli download links --- cmd/wf/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/wf/README.md b/cmd/wf/README.md index 64be198..898d910 100644 --- a/cmd/wf/README.md +++ b/cmd/wf/README.md @@ -25,10 +25,10 @@ The easiest way to get the CLI is to download a pre-built executable for your OS Get the latest version for your operating system as a standalone executable. **Windows**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. **macOS**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. **Debian-based Linux**
```bash @@ -38,7 +38,7 @@ sudo apt-get update && sudo apt-get install wf-cli ``` **Linux (other)**
-Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. +Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. ### Go get it ```bash From c6af292b2a96a34eaa7176cb7ba27af5460d3b76 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Thu, 5 Sep 2019 17:23:30 -0700 Subject: [PATCH 171/181] Add password flag for authentication For T692 --- cmd/writeas/main.go | 4 ++++ commands/commands.go | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go index e9c1019..40bff74 100644 --- a/cmd/writeas/main.go +++ b/cmd/writeas/main.go @@ -238,6 +238,10 @@ func main() { Name: "verbose, v", Usage: "Make the operation more talkative", }, + cli.StringFlag{ + Name: "password, p", + Usage: "The password for the account being logged into", + }, }, }, { diff --git a/commands/commands.go b/commands/commands.go index 29ddc21..c2fc3a8 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -408,15 +408,19 @@ func CmdAuth(c *cli.Context) error { } } - fmt.Print("Password: ") - pass, err := gopass.GetPasswdMasked() - if err != nil { - return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1) - } - - // Validate password + // Take password from argument, and fall back to input + pass := c.String("p") if len(pass) == 0 { - return cli.NewExitError("Please enter your password.", 1) + fmt.Print("Password: ") + pass, err := gopass.GetPasswdMasked() + if err != nil { + return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1) + } + + // Validate password + if len(pass) == 0 { + return cli.NewExitError("Please enter your password.", 1) + } } if config.IsTor(c) { From f2bb9f5896df03fb191b88af0ed3b05fd47a4365 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Fri, 6 Sep 2019 09:51:02 -0700 Subject: [PATCH 172/181] commands: Fix up the interactive auth flow Fixes up the interactive authorization workflow so that pass is reassigned properly. For T692 --- commands/commands.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index c2fc3a8..a87a582 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -410,17 +410,18 @@ func CmdAuth(c *cli.Context) error { // Take password from argument, and fall back to input pass := c.String("p") - if len(pass) == 0 { + if pass == "" { fmt.Print("Password: ") - pass, err := gopass.GetPasswdMasked() + enteredPass, err := gopass.GetPasswdMasked() if err != nil { return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1) } // Validate password - if len(pass) == 0 { + if len(enteredPass) == 0 { return cli.NewExitError("Please enter your password.", 1) } + pass = string(enteredPass) } if config.IsTor(c) { From bd378a85ad6389a882eff2610e64a272ce0d0aca Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Tue, 8 Oct 2019 18:34:37 -0700 Subject: [PATCH 173/181] commands: Remove redundant string() cast on pass When logging in we had a cast that was made redundant, since `pass` is now a string. --- commands/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/commands.go b/commands/commands.go index a87a582..d6a55d6 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -429,7 +429,7 @@ func CmdAuth(c *cli.Context) error { } else { log.Info(c, "Logging in...") } - err = api.DoLogIn(c, username, string(pass)) + err = api.DoLogIn(c, username, pass) if err != nil { return cli.NewExitError(fmt.Sprintf("error logging in: %v", err), 1) } From a04d8064cb088a5b90342f30e882e222e7eb1e5d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 12 Nov 2019 01:16:20 +0900 Subject: [PATCH 174/181] Remove Debian install instructions This is unavailable at the moment. --- cmd/wf/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cmd/wf/README.md b/cmd/wf/README.md index 898d910..f8b9d50 100644 --- a/cmd/wf/README.md +++ b/cmd/wf/README.md @@ -30,13 +30,6 @@ Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v **macOS**
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. -**Debian-based Linux**
-```bash -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445 -sudo add-apt-repository "deb http://updates.writeas.org xenial main" -sudo apt-get update && sudo apt-get install wf-cli -``` - **Linux (other)**
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_1.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. From 1c50cb773c880168e7aab790b468e9f80b320408 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Tue, 12 Nov 2019 01:30:24 +0900 Subject: [PATCH 175/181] Link to WF v0.11 in wf-cli README --- cmd/wf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/wf/README.md b/cmd/wf/README.md index f8b9d50..1606b77 100644 --- a/cmd/wf/README.md +++ b/cmd/wf/README.md @@ -4,7 +4,7 @@ wf-cli Command line utility for publishing to any [WriteFreely](https://writefreely.org) instance. Works on Windows, macOS, and Linux. -**The WriteFreely CLI is compatible with WriteFreely v0.11 or later.** +**The WriteFreely CLI is compatible with [WriteFreely v0.11](https://github.com/writeas/writefreely/releases/tag/v0.11.0) or later.** ## Features From 13c597643e19f4e81cfe9b3c95be89b3d4282d5a Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Fri, 6 Mar 2020 11:12:40 -0800 Subject: [PATCH 176/181] Vendor dependencies for writeas-cli Use go modules to vendor the dependencies we need for the writeas-cli build. --- go.mod | 3 +- vendor/code.as/core/socks/.gitignore | 22 + vendor/code.as/core/socks/LICENSE | 22 + vendor/code.as/core/socks/README.md | 58 + vendor/code.as/core/socks/socks.go | 218 ++ .../github.com/atotto/clipboard/.travis.yml | 20 + vendor/github.com/atotto/clipboard/LICENSE | 27 + vendor/github.com/atotto/clipboard/README.md | 48 + .../github.com/atotto/clipboard/clipboard.go | 20 + .../atotto/clipboard/clipboard_darwin.go | 52 + .../atotto/clipboard/clipboard_unix.go | 112 + .../atotto/clipboard/clipboard_windows.go | 128 + vendor/github.com/atotto/clipboard/go.mod | 1 + .../cloudfoundry/jibber_jabber/.travis.yml | 11 + .../cloudfoundry/jibber_jabber/LICENSE | 201 ++ .../cloudfoundry/jibber_jabber/README.md | 44 + .../jibber_jabber/jibber_jabber.go | 22 + .../jibber_jabber/jibber_jabber_unix.go | 57 + .../jibber_jabber/jibber_jabber_windows.go | 114 + vendor/github.com/hashicorp/errwrap/LICENSE | 354 +++ vendor/github.com/hashicorp/errwrap/README.md | 89 + .../github.com/hashicorp/errwrap/errwrap.go | 169 + vendor/github.com/hashicorp/errwrap/go.mod | 1 + .../hashicorp/go-multierror/.travis.yml | 12 + .../hashicorp/go-multierror/LICENSE | 353 +++ .../hashicorp/go-multierror/Makefile | 31 + .../hashicorp/go-multierror/README.md | 97 + .../hashicorp/go-multierror/append.go | 41 + .../hashicorp/go-multierror/flatten.go | 26 + .../hashicorp/go-multierror/format.go | 27 + .../github.com/hashicorp/go-multierror/go.mod | 3 + .../github.com/hashicorp/go-multierror/go.sum | 4 + .../hashicorp/go-multierror/multierror.go | 51 + .../hashicorp/go-multierror/prefix.go | 37 + .../hashicorp/go-multierror/sort.go | 16 + vendor/github.com/howeyc/gopass/.travis.yml | 11 + vendor/github.com/howeyc/gopass/LICENSE.txt | 15 + .../howeyc/gopass/OPENSOLARIS.LICENSE | 384 +++ vendor/github.com/howeyc/gopass/README.md | 27 + vendor/github.com/howeyc/gopass/pass.go | 110 + vendor/github.com/howeyc/gopass/terminal.go | 25 + .../howeyc/gopass/terminal_solaris.go | 69 + .../microcosm-cc/bluemonday/.coveralls.yml | 1 + .../microcosm-cc/bluemonday/.travis.yml | 21 + .../microcosm-cc/bluemonday/CONTRIBUTING.md | 51 + .../microcosm-cc/bluemonday/CREDITS.md | 6 + .../microcosm-cc/bluemonday/LICENSE.md | 28 + .../microcosm-cc/bluemonday/Makefile | 42 + .../microcosm-cc/bluemonday/README.md | 350 +++ .../github.com/microcosm-cc/bluemonday/doc.go | 104 + .../microcosm-cc/bluemonday/helpers.go | 297 ++ .../microcosm-cc/bluemonday/policies.go | 253 ++ .../microcosm-cc/bluemonday/policy.go | 552 ++++ .../microcosm-cc/bluemonday/sanitize.go | 581 ++++ .../github.com/mitchellh/go-homedir/LICENSE | 21 + .../github.com/mitchellh/go-homedir/README.md | 14 + vendor/github.com/mitchellh/go-homedir/go.mod | 1 + .../mitchellh/go-homedir/homedir.go | 157 + .../sanitized_anchor_name/.travis.yml | 16 + .../shurcooL/sanitized_anchor_name/LICENSE | 21 + .../shurcooL/sanitized_anchor_name/README.md | 36 + .../shurcooL/sanitized_anchor_name/main.go | 29 + .../writeas/go-writeas/v2/.gitignore | 3 + .../github.com/writeas/go-writeas/v2/LICENSE | 21 + .../writeas/go-writeas/v2/README.md | 71 + .../github.com/writeas/go-writeas/v2/auth.go | 75 + .../writeas/go-writeas/v2/collection.go | 186 ++ .../github.com/writeas/go-writeas/v2/go.mod | 8 + .../github.com/writeas/go-writeas/v2/go.sum | 4 + .../github.com/writeas/go-writeas/v2/post.go | 330 ++ .../github.com/writeas/go-writeas/v2/user.go | 34 + .../writeas/go-writeas/v2/writeas.go | 199 ++ vendor/github.com/writeas/impart/.gitignore | 27 + vendor/github.com/writeas/impart/LICENSE | 22 + vendor/github.com/writeas/impart/README.md | 61 + vendor/github.com/writeas/impart/doc.go | 4 + vendor/github.com/writeas/impart/errors.go | 20 + vendor/github.com/writeas/impart/go.mod | 3 + vendor/github.com/writeas/impart/request.go | 13 + vendor/github.com/writeas/impart/response.go | 76 + vendor/github.com/writeas/saturday/.gitignore | 8 + .../github.com/writeas/saturday/.travis.yml | 30 + .../github.com/writeas/saturday/LICENSE.txt | 29 + vendor/github.com/writeas/saturday/README.md | 284 ++ vendor/github.com/writeas/saturday/block.go | 1412 +++++++++ vendor/github.com/writeas/saturday/html.go | 949 ++++++ vendor/github.com/writeas/saturday/inline.go | 1151 +++++++ vendor/github.com/writeas/saturday/latex.go | 332 ++ .../github.com/writeas/saturday/markdown.go | 926 ++++++ .../writeas/saturday/smartypants.go | 396 +++ vendor/github.com/writeas/web-core/LICENSE | 373 +++ .../writeas/web-core/posts/parse.go | 19 + .../writeas/web-core/posts/render.go | 63 + vendor/golang.org/x/crypto/AUTHORS | 3 + vendor/golang.org/x/crypto/CONTRIBUTORS | 3 + vendor/golang.org/x/crypto/LICENSE | 27 + vendor/golang.org/x/crypto/PATENTS | 22 + .../x/crypto/ssh/terminal/terminal.go | 951 ++++++ .../golang.org/x/crypto/ssh/terminal/util.go | 114 + .../x/crypto/ssh/terminal/util_aix.go | 12 + .../x/crypto/ssh/terminal/util_bsd.go | 12 + .../x/crypto/ssh/terminal/util_linux.go | 10 + .../x/crypto/ssh/terminal/util_plan9.go | 58 + .../x/crypto/ssh/terminal/util_solaris.go | 124 + .../x/crypto/ssh/terminal/util_windows.go | 103 + vendor/golang.org/x/net/AUTHORS | 3 + vendor/golang.org/x/net/CONTRIBUTORS | 3 + vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/html/atom/atom.go | 78 + vendor/golang.org/x/net/html/atom/table.go | 783 +++++ vendor/golang.org/x/net/html/const.go | 112 + vendor/golang.org/x/net/html/doc.go | 106 + vendor/golang.org/x/net/html/doctype.go | 156 + vendor/golang.org/x/net/html/entity.go | 2253 +++++++++++++ vendor/golang.org/x/net/html/escape.go | 258 ++ vendor/golang.org/x/net/html/foreign.go | 226 ++ vendor/golang.org/x/net/html/node.go | 220 ++ vendor/golang.org/x/net/html/parse.go | 2311 ++++++++++++++ vendor/golang.org/x/net/html/render.go | 271 ++ vendor/golang.org/x/net/html/token.go | 1219 +++++++ vendor/golang.org/x/sys/AUTHORS | 3 + vendor/golang.org/x/sys/CONTRIBUTORS | 3 + vendor/golang.org/x/sys/LICENSE | 27 + vendor/golang.org/x/sys/PATENTS | 22 + vendor/golang.org/x/sys/unix/.gitignore | 2 + vendor/golang.org/x/sys/unix/README.md | 173 + .../golang.org/x/sys/unix/affinity_linux.go | 124 + vendor/golang.org/x/sys/unix/aliases.go | 14 + vendor/golang.org/x/sys/unix/asm_aix_ppc64.s | 17 + vendor/golang.org/x/sys/unix/asm_darwin_386.s | 29 + .../golang.org/x/sys/unix/asm_darwin_amd64.s | 29 + vendor/golang.org/x/sys/unix/asm_darwin_arm.s | 30 + .../golang.org/x/sys/unix/asm_darwin_arm64.s | 30 + .../x/sys/unix/asm_dragonfly_amd64.s | 29 + .../golang.org/x/sys/unix/asm_freebsd_386.s | 29 + .../golang.org/x/sys/unix/asm_freebsd_amd64.s | 29 + .../golang.org/x/sys/unix/asm_freebsd_arm.s | 29 + vendor/golang.org/x/sys/unix/asm_linux_386.s | 65 + .../golang.org/x/sys/unix/asm_linux_amd64.s | 57 + vendor/golang.org/x/sys/unix/asm_linux_arm.s | 56 + .../golang.org/x/sys/unix/asm_linux_arm64.s | 52 + .../golang.org/x/sys/unix/asm_linux_mips64x.s | 56 + .../golang.org/x/sys/unix/asm_linux_mipsx.s | 54 + .../golang.org/x/sys/unix/asm_linux_ppc64x.s | 44 + .../golang.org/x/sys/unix/asm_linux_s390x.s | 56 + vendor/golang.org/x/sys/unix/asm_netbsd_386.s | 29 + .../golang.org/x/sys/unix/asm_netbsd_amd64.s | 29 + vendor/golang.org/x/sys/unix/asm_netbsd_arm.s | 29 + .../golang.org/x/sys/unix/asm_openbsd_386.s | 29 + .../golang.org/x/sys/unix/asm_openbsd_amd64.s | 29 + .../golang.org/x/sys/unix/asm_openbsd_arm.s | 29 + .../golang.org/x/sys/unix/asm_solaris_amd64.s | 17 + .../golang.org/x/sys/unix/bluetooth_linux.go | 35 + vendor/golang.org/x/sys/unix/cap_freebsd.go | 195 ++ vendor/golang.org/x/sys/unix/constants.go | 13 + vendor/golang.org/x/sys/unix/dev_aix_ppc.go | 27 + vendor/golang.org/x/sys/unix/dev_aix_ppc64.go | 29 + vendor/golang.org/x/sys/unix/dev_darwin.go | 24 + vendor/golang.org/x/sys/unix/dev_dragonfly.go | 30 + vendor/golang.org/x/sys/unix/dev_freebsd.go | 30 + vendor/golang.org/x/sys/unix/dev_linux.go | 42 + vendor/golang.org/x/sys/unix/dev_netbsd.go | 29 + vendor/golang.org/x/sys/unix/dev_openbsd.go | 29 + vendor/golang.org/x/sys/unix/dirent.go | 17 + vendor/golang.org/x/sys/unix/endian_big.go | 9 + vendor/golang.org/x/sys/unix/endian_little.go | 9 + vendor/golang.org/x/sys/unix/env_unix.go | 31 + .../x/sys/unix/errors_freebsd_386.go | 227 ++ .../x/sys/unix/errors_freebsd_amd64.go | 227 ++ .../x/sys/unix/errors_freebsd_arm.go | 226 ++ vendor/golang.org/x/sys/unix/fcntl.go | 32 + .../x/sys/unix/fcntl_linux_32bit.go | 13 + vendor/golang.org/x/sys/unix/gccgo.go | 62 + vendor/golang.org/x/sys/unix/gccgo_c.c | 39 + .../x/sys/unix/gccgo_linux_amd64.go | 20 + vendor/golang.org/x/sys/unix/ioctl.go | 30 + vendor/golang.org/x/sys/unix/mkall.sh | 204 ++ vendor/golang.org/x/sys/unix/mkerrors.sh | 658 ++++ .../x/sys/unix/mksyscall_aix_ppc.pl | 384 +++ .../x/sys/unix/mksyscall_aix_ppc64.pl | 579 ++++ .../x/sys/unix/mksyscall_solaris.pl | 294 ++ .../golang.org/x/sys/unix/mksysctl_openbsd.pl | 265 ++ .../golang.org/x/sys/unix/mksysnum_darwin.pl | 39 + .../x/sys/unix/mksysnum_dragonfly.pl | 50 + .../golang.org/x/sys/unix/mksysnum_freebsd.pl | 50 + .../golang.org/x/sys/unix/mksysnum_netbsd.pl | 58 + .../golang.org/x/sys/unix/mksysnum_openbsd.pl | 50 + .../golang.org/x/sys/unix/openbsd_pledge.go | 166 + .../golang.org/x/sys/unix/openbsd_unveil.go | 44 + vendor/golang.org/x/sys/unix/pagesize_unix.go | 15 + vendor/golang.org/x/sys/unix/race.go | 30 + vendor/golang.org/x/sys/unix/race0.go | 25 + .../golang.org/x/sys/unix/sockcmsg_linux.go | 36 + vendor/golang.org/x/sys/unix/sockcmsg_unix.go | 117 + vendor/golang.org/x/sys/unix/str.go | 26 + vendor/golang.org/x/sys/unix/syscall.go | 54 + vendor/golang.org/x/sys/unix/syscall_aix.go | 540 ++++ .../golang.org/x/sys/unix/syscall_aix_ppc.go | 34 + .../x/sys/unix/syscall_aix_ppc64.go | 34 + vendor/golang.org/x/sys/unix/syscall_bsd.go | 624 ++++ .../golang.org/x/sys/unix/syscall_darwin.go | 700 +++++ .../x/sys/unix/syscall_darwin_386.go | 68 + .../x/sys/unix/syscall_darwin_amd64.go | 68 + .../x/sys/unix/syscall_darwin_arm.go | 66 + .../x/sys/unix/syscall_darwin_arm64.go | 68 + .../x/sys/unix/syscall_dragonfly.go | 531 ++++ .../x/sys/unix/syscall_dragonfly_amd64.go | 52 + .../golang.org/x/sys/unix/syscall_freebsd.go | 817 +++++ .../x/sys/unix/syscall_freebsd_386.go | 52 + .../x/sys/unix/syscall_freebsd_amd64.go | 52 + .../x/sys/unix/syscall_freebsd_arm.go | 52 + vendor/golang.org/x/sys/unix/syscall_linux.go | 1697 ++++++++++ .../x/sys/unix/syscall_linux_386.go | 385 +++ .../x/sys/unix/syscall_linux_amd64.go | 189 ++ .../x/sys/unix/syscall_linux_amd64_gc.go | 13 + .../x/sys/unix/syscall_linux_arm.go | 267 ++ .../x/sys/unix/syscall_linux_arm64.go | 209 ++ .../golang.org/x/sys/unix/syscall_linux_gc.go | 14 + .../x/sys/unix/syscall_linux_gc_386.go | 16 + .../x/sys/unix/syscall_linux_gccgo_386.go | 30 + .../x/sys/unix/syscall_linux_gccgo_arm.go | 20 + .../x/sys/unix/syscall_linux_mips64x.go | 214 ++ .../x/sys/unix/syscall_linux_mipsx.go | 233 ++ .../x/sys/unix/syscall_linux_ppc64x.go | 151 + .../x/sys/unix/syscall_linux_riscv64.go | 209 ++ .../x/sys/unix/syscall_linux_s390x.go | 337 ++ .../x/sys/unix/syscall_linux_sparc64.go | 146 + .../golang.org/x/sys/unix/syscall_netbsd.go | 615 ++++ .../x/sys/unix/syscall_netbsd_386.go | 33 + .../x/sys/unix/syscall_netbsd_amd64.go | 33 + .../x/sys/unix/syscall_netbsd_arm.go | 33 + .../golang.org/x/sys/unix/syscall_openbsd.go | 392 +++ .../x/sys/unix/syscall_openbsd_386.go | 37 + .../x/sys/unix/syscall_openbsd_amd64.go | 37 + .../x/sys/unix/syscall_openbsd_arm.go | 37 + .../golang.org/x/sys/unix/syscall_solaris.go | 730 +++++ .../x/sys/unix/syscall_solaris_amd64.go | 23 + vendor/golang.org/x/sys/unix/syscall_unix.go | 386 +++ .../golang.org/x/sys/unix/syscall_unix_gc.go | 15 + .../x/sys/unix/syscall_unix_gc_ppc64x.go | 24 + vendor/golang.org/x/sys/unix/timestruct.go | 82 + vendor/golang.org/x/sys/unix/xattr_bsd.go | 240 ++ .../golang.org/x/sys/unix/zerrors_aix_ppc.go | 1372 ++++++++ .../x/sys/unix/zerrors_aix_ppc64.go | 1373 ++++++++ .../x/sys/unix/zerrors_darwin_386.go | 1783 +++++++++++ .../x/sys/unix/zerrors_darwin_amd64.go | 1783 +++++++++++ .../x/sys/unix/zerrors_darwin_arm.go | 1783 +++++++++++ .../x/sys/unix/zerrors_darwin_arm64.go | 1783 +++++++++++ .../x/sys/unix/zerrors_dragonfly_amd64.go | 1650 ++++++++++ .../x/sys/unix/zerrors_freebsd_386.go | 1793 +++++++++++ .../x/sys/unix/zerrors_freebsd_amd64.go | 1794 +++++++++++ .../x/sys/unix/zerrors_freebsd_arm.go | 1802 +++++++++++ .../x/sys/unix/zerrors_linux_386.go | 2738 ++++++++++++++++ .../x/sys/unix/zerrors_linux_amd64.go | 2738 ++++++++++++++++ .../x/sys/unix/zerrors_linux_arm.go | 2744 ++++++++++++++++ .../x/sys/unix/zerrors_linux_arm64.go | 2729 ++++++++++++++++ .../x/sys/unix/zerrors_linux_mips.go | 2745 ++++++++++++++++ .../x/sys/unix/zerrors_linux_mips64.go | 2745 ++++++++++++++++ .../x/sys/unix/zerrors_linux_mips64le.go | 2745 ++++++++++++++++ .../x/sys/unix/zerrors_linux_mipsle.go | 2745 ++++++++++++++++ .../x/sys/unix/zerrors_linux_ppc64.go | 2798 +++++++++++++++++ .../x/sys/unix/zerrors_linux_ppc64le.go | 2798 +++++++++++++++++ .../x/sys/unix/zerrors_linux_riscv64.go | 2725 ++++++++++++++++ .../x/sys/unix/zerrors_linux_s390x.go | 2798 +++++++++++++++++ .../x/sys/unix/zerrors_linux_sparc64.go | 2150 +++++++++++++ .../x/sys/unix/zerrors_netbsd_386.go | 1772 +++++++++++ .../x/sys/unix/zerrors_netbsd_amd64.go | 1762 +++++++++++ .../x/sys/unix/zerrors_netbsd_arm.go | 1751 +++++++++++ .../x/sys/unix/zerrors_openbsd_386.go | 1654 ++++++++++ .../x/sys/unix/zerrors_openbsd_amd64.go | 1765 +++++++++++ .../x/sys/unix/zerrors_openbsd_arm.go | 1656 ++++++++++ .../x/sys/unix/zerrors_solaris_amd64.go | 1532 +++++++++ .../golang.org/x/sys/unix/zptrace386_linux.go | 80 + .../golang.org/x/sys/unix/zptracearm_linux.go | 41 + .../x/sys/unix/zptracemips_linux.go | 50 + .../x/sys/unix/zptracemipsle_linux.go | 50 + .../golang.org/x/sys/unix/zsyscall_aix_ppc.go | 1450 +++++++++ .../x/sys/unix/zsyscall_aix_ppc64.go | 1408 +++++++++ .../x/sys/unix/zsyscall_aix_ppc64_gc.go | 1162 +++++++ .../x/sys/unix/zsyscall_aix_ppc64_gccgo.go | 1042 ++++++ .../x/sys/unix/zsyscall_darwin_386.go | 1769 +++++++++++ .../x/sys/unix/zsyscall_darwin_amd64.go | 1769 +++++++++++ .../x/sys/unix/zsyscall_darwin_arm.go | 1769 +++++++++++ .../x/sys/unix/zsyscall_darwin_arm64.go | 1769 +++++++++++ .../x/sys/unix/zsyscall_dragonfly_amd64.go | 1639 ++++++++++ .../x/sys/unix/zsyscall_freebsd_386.go | 2015 ++++++++++++ .../x/sys/unix/zsyscall_freebsd_amd64.go | 2015 ++++++++++++ .../x/sys/unix/zsyscall_freebsd_arm.go | 2015 ++++++++++++ .../x/sys/unix/zsyscall_linux_386.go | 2182 +++++++++++++ .../x/sys/unix/zsyscall_linux_amd64.go | 2349 ++++++++++++++ .../x/sys/unix/zsyscall_linux_arm.go | 2294 ++++++++++++++ .../x/sys/unix/zsyscall_linux_arm64.go | 2191 +++++++++++++ .../x/sys/unix/zsyscall_linux_mips.go | 2362 ++++++++++++++ .../x/sys/unix/zsyscall_linux_mips64.go | 2333 ++++++++++++++ .../x/sys/unix/zsyscall_linux_mips64le.go | 2333 ++++++++++++++ .../x/sys/unix/zsyscall_linux_mipsle.go | 2362 ++++++++++++++ .../x/sys/unix/zsyscall_linux_ppc64.go | 2411 ++++++++++++++ .../x/sys/unix/zsyscall_linux_ppc64le.go | 2411 ++++++++++++++ .../x/sys/unix/zsyscall_linux_riscv64.go | 2191 +++++++++++++ .../x/sys/unix/zsyscall_linux_s390x.go | 2181 +++++++++++++ .../x/sys/unix/zsyscall_linux_sparc64.go | 2344 ++++++++++++++ .../x/sys/unix/zsyscall_netbsd_386.go | 1826 +++++++++++ .../x/sys/unix/zsyscall_netbsd_amd64.go | 1826 +++++++++++ .../x/sys/unix/zsyscall_netbsd_arm.go | 1826 +++++++++++ .../x/sys/unix/zsyscall_openbsd_386.go | 1692 ++++++++++ .../x/sys/unix/zsyscall_openbsd_amd64.go | 1692 ++++++++++ .../x/sys/unix/zsyscall_openbsd_arm.go | 1692 ++++++++++ .../x/sys/unix/zsyscall_solaris_amd64.go | 1953 ++++++++++++ .../x/sys/unix/zsysctl_openbsd_386.go | 270 ++ .../x/sys/unix/zsysctl_openbsd_amd64.go | 270 ++ .../x/sys/unix/zsysctl_openbsd_arm.go | 270 ++ .../x/sys/unix/zsysnum_darwin_386.go | 436 +++ .../x/sys/unix/zsysnum_darwin_amd64.go | 436 +++ .../x/sys/unix/zsysnum_darwin_arm.go | 436 +++ .../x/sys/unix/zsysnum_darwin_arm64.go | 436 +++ .../x/sys/unix/zsysnum_dragonfly_amd64.go | 315 ++ .../x/sys/unix/zsysnum_freebsd_386.go | 403 +++ .../x/sys/unix/zsysnum_freebsd_amd64.go | 403 +++ .../x/sys/unix/zsysnum_freebsd_arm.go | 403 +++ .../x/sys/unix/zsysnum_linux_386.go | 392 +++ .../x/sys/unix/zsysnum_linux_amd64.go | 344 ++ .../x/sys/unix/zsysnum_linux_arm.go | 364 +++ .../x/sys/unix/zsysnum_linux_arm64.go | 288 ++ .../x/sys/unix/zsysnum_linux_mips.go | 377 +++ .../x/sys/unix/zsysnum_linux_mips64.go | 337 ++ .../x/sys/unix/zsysnum_linux_mips64le.go | 337 ++ .../x/sys/unix/zsysnum_linux_mipsle.go | 377 +++ .../x/sys/unix/zsysnum_linux_ppc64.go | 375 +++ .../x/sys/unix/zsysnum_linux_ppc64le.go | 375 +++ .../x/sys/unix/zsysnum_linux_riscv64.go | 287 ++ .../x/sys/unix/zsysnum_linux_s390x.go | 337 ++ .../x/sys/unix/zsysnum_linux_sparc64.go | 348 ++ .../x/sys/unix/zsysnum_netbsd_386.go | 274 ++ .../x/sys/unix/zsysnum_netbsd_amd64.go | 274 ++ .../x/sys/unix/zsysnum_netbsd_arm.go | 274 ++ .../x/sys/unix/zsysnum_openbsd_386.go | 218 ++ .../x/sys/unix/zsysnum_openbsd_amd64.go | 218 ++ .../x/sys/unix/zsysnum_openbsd_arm.go | 218 ++ .../golang.org/x/sys/unix/ztypes_aix_ppc.go | 345 ++ .../golang.org/x/sys/unix/ztypes_aix_ppc64.go | 354 +++ .../x/sys/unix/ztypes_darwin_386.go | 489 +++ .../x/sys/unix/ztypes_darwin_amd64.go | 499 +++ .../x/sys/unix/ztypes_darwin_arm.go | 490 +++ .../x/sys/unix/ztypes_darwin_arm64.go | 499 +++ .../x/sys/unix/ztypes_dragonfly_amd64.go | 469 +++ .../x/sys/unix/ztypes_freebsd_386.go | 603 ++++ .../x/sys/unix/ztypes_freebsd_amd64.go | 602 ++++ .../x/sys/unix/ztypes_freebsd_arm.go | 602 ++++ .../golang.org/x/sys/unix/ztypes_linux_386.go | 1991 ++++++++++++ .../x/sys/unix/ztypes_linux_amd64.go | 2013 ++++++++++++ .../golang.org/x/sys/unix/ztypes_linux_arm.go | 1981 ++++++++++++ .../x/sys/unix/ztypes_linux_arm64.go | 1992 ++++++++++++ .../x/sys/unix/ztypes_linux_mips.go | 1986 ++++++++++++ .../x/sys/unix/ztypes_linux_mips64.go | 1994 ++++++++++++ .../x/sys/unix/ztypes_linux_mips64le.go | 1994 ++++++++++++ .../x/sys/unix/ztypes_linux_mipsle.go | 1986 ++++++++++++ .../x/sys/unix/ztypes_linux_ppc64.go | 2002 ++++++++++++ .../x/sys/unix/ztypes_linux_ppc64le.go | 2002 ++++++++++++ .../x/sys/unix/ztypes_linux_riscv64.go | 2019 ++++++++++++ .../x/sys/unix/ztypes_linux_s390x.go | 2019 ++++++++++++ .../x/sys/unix/ztypes_linux_sparc64.go | 690 ++++ .../x/sys/unix/ztypes_netbsd_386.go | 465 +++ .../x/sys/unix/ztypes_netbsd_amd64.go | 472 +++ .../x/sys/unix/ztypes_netbsd_arm.go | 470 +++ .../x/sys/unix/ztypes_openbsd_386.go | 560 ++++ .../x/sys/unix/ztypes_openbsd_amd64.go | 560 ++++ .../x/sys/unix/ztypes_openbsd_arm.go | 561 ++++ .../x/sys/unix/ztypes_solaris_amd64.go | 442 +++ vendor/golang.org/x/sys/windows/aliases.go | 13 + .../x/sys/windows/asm_windows_386.s | 13 + .../x/sys/windows/asm_windows_amd64.s | 13 + .../x/sys/windows/asm_windows_arm.s | 11 + .../golang.org/x/sys/windows/dll_windows.go | 378 +++ .../golang.org/x/sys/windows/env_windows.go | 29 + vendor/golang.org/x/sys/windows/eventlog.go | 20 + .../golang.org/x/sys/windows/exec_windows.go | 97 + .../x/sys/windows/memory_windows.go | 26 + vendor/golang.org/x/sys/windows/mksyscall.go | 7 + vendor/golang.org/x/sys/windows/race.go | 30 + vendor/golang.org/x/sys/windows/race0.go | 25 + .../x/sys/windows/security_windows.go | 478 +++ vendor/golang.org/x/sys/windows/service.go | 183 ++ vendor/golang.org/x/sys/windows/str.go | 22 + vendor/golang.org/x/sys/windows/syscall.go | 74 + .../x/sys/windows/syscall_windows.go | 1205 +++++++ .../golang.org/x/sys/windows/types_windows.go | 1469 +++++++++ .../x/sys/windows/types_windows_386.go | 22 + .../x/sys/windows/types_windows_amd64.go | 22 + .../x/sys/windows/types_windows_arm.go | 22 + .../x/sys/windows/zsyscall_windows.go | 2700 ++++++++++++++++ vendor/gopkg.in/ini.v1/.gitignore | 6 + vendor/gopkg.in/ini.v1/.travis.yml | 17 + vendor/gopkg.in/ini.v1/LICENSE | 191 ++ vendor/gopkg.in/ini.v1/Makefile | 15 + vendor/gopkg.in/ini.v1/README.md | 46 + vendor/gopkg.in/ini.v1/error.go | 32 + vendor/gopkg.in/ini.v1/file.go | 418 +++ vendor/gopkg.in/ini.v1/ini.go | 217 ++ vendor/gopkg.in/ini.v1/key.go | 751 +++++ vendor/gopkg.in/ini.v1/parser.go | 486 +++ vendor/gopkg.in/ini.v1/section.go | 258 ++ vendor/gopkg.in/ini.v1/struct.go | 512 +++ vendor/gopkg.in/urfave/cli.v1/.flake8 | 2 + vendor/gopkg.in/urfave/cli.v1/.gitignore | 2 + vendor/gopkg.in/urfave/cli.v1/.travis.yml | 27 + vendor/gopkg.in/urfave/cli.v1/CHANGELOG.md | 435 +++ vendor/gopkg.in/urfave/cli.v1/LICENSE | 21 + vendor/gopkg.in/urfave/cli.v1/README.md | 1381 ++++++++ vendor/gopkg.in/urfave/cli.v1/app.go | 497 +++ vendor/gopkg.in/urfave/cli.v1/appveyor.yml | 26 + vendor/gopkg.in/urfave/cli.v1/category.go | 44 + vendor/gopkg.in/urfave/cli.v1/cli.go | 22 + vendor/gopkg.in/urfave/cli.v1/command.go | 304 ++ vendor/gopkg.in/urfave/cli.v1/context.go | 278 ++ vendor/gopkg.in/urfave/cli.v1/errors.go | 115 + vendor/gopkg.in/urfave/cli.v1/flag-types.json | 93 + vendor/gopkg.in/urfave/cli.v1/flag.go | 799 +++++ .../gopkg.in/urfave/cli.v1/flag_generated.go | 627 ++++ vendor/gopkg.in/urfave/cli.v1/funcs.go | 28 + .../urfave/cli.v1/generate-flag-types | 255 ++ vendor/gopkg.in/urfave/cli.v1/help.go | 338 ++ vendor/gopkg.in/urfave/cli.v1/runtests | 122 + vendor/modules.txt | 38 + 424 files changed, 223757 insertions(+), 1 deletion(-) create mode 100644 vendor/code.as/core/socks/.gitignore create mode 100644 vendor/code.as/core/socks/LICENSE create mode 100644 vendor/code.as/core/socks/README.md create mode 100644 vendor/code.as/core/socks/socks.go create mode 100644 vendor/github.com/atotto/clipboard/.travis.yml create mode 100644 vendor/github.com/atotto/clipboard/LICENSE create mode 100644 vendor/github.com/atotto/clipboard/README.md create mode 100644 vendor/github.com/atotto/clipboard/clipboard.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_darwin.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_unix.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_windows.go create mode 100644 vendor/github.com/atotto/clipboard/go.mod create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/.travis.yml create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/LICENSE create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/README.md create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber.go create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_unix.go create mode 100644 vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_windows.go create mode 100644 vendor/github.com/hashicorp/errwrap/LICENSE create mode 100644 vendor/github.com/hashicorp/errwrap/README.md create mode 100644 vendor/github.com/hashicorp/errwrap/errwrap.go create mode 100644 vendor/github.com/hashicorp/errwrap/go.mod create mode 100644 vendor/github.com/hashicorp/go-multierror/.travis.yml create mode 100644 vendor/github.com/hashicorp/go-multierror/LICENSE create mode 100644 vendor/github.com/hashicorp/go-multierror/Makefile create mode 100644 vendor/github.com/hashicorp/go-multierror/README.md create mode 100644 vendor/github.com/hashicorp/go-multierror/append.go create mode 100644 vendor/github.com/hashicorp/go-multierror/flatten.go create mode 100644 vendor/github.com/hashicorp/go-multierror/format.go create mode 100644 vendor/github.com/hashicorp/go-multierror/go.mod create mode 100644 vendor/github.com/hashicorp/go-multierror/go.sum create mode 100644 vendor/github.com/hashicorp/go-multierror/multierror.go create mode 100644 vendor/github.com/hashicorp/go-multierror/prefix.go create mode 100644 vendor/github.com/hashicorp/go-multierror/sort.go create mode 100644 vendor/github.com/howeyc/gopass/.travis.yml create mode 100644 vendor/github.com/howeyc/gopass/LICENSE.txt create mode 100644 vendor/github.com/howeyc/gopass/OPENSOLARIS.LICENSE create mode 100644 vendor/github.com/howeyc/gopass/README.md create mode 100644 vendor/github.com/howeyc/gopass/pass.go create mode 100644 vendor/github.com/howeyc/gopass/terminal.go create mode 100644 vendor/github.com/howeyc/gopass/terminal_solaris.go create mode 100644 vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml create mode 100644 vendor/github.com/microcosm-cc/bluemonday/.travis.yml create mode 100644 vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md create mode 100644 vendor/github.com/microcosm-cc/bluemonday/CREDITS.md create mode 100644 vendor/github.com/microcosm-cc/bluemonday/LICENSE.md create mode 100644 vendor/github.com/microcosm-cc/bluemonday/Makefile create mode 100644 vendor/github.com/microcosm-cc/bluemonday/README.md create mode 100644 vendor/github.com/microcosm-cc/bluemonday/doc.go create mode 100644 vendor/github.com/microcosm-cc/bluemonday/helpers.go create mode 100644 vendor/github.com/microcosm-cc/bluemonday/policies.go create mode 100644 vendor/github.com/microcosm-cc/bluemonday/policy.go create mode 100644 vendor/github.com/microcosm-cc/bluemonday/sanitize.go create mode 100644 vendor/github.com/mitchellh/go-homedir/LICENSE create mode 100644 vendor/github.com/mitchellh/go-homedir/README.md create mode 100644 vendor/github.com/mitchellh/go-homedir/go.mod create mode 100644 vendor/github.com/mitchellh/go-homedir/homedir.go create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/README.md create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/main.go create mode 100644 vendor/github.com/writeas/go-writeas/v2/.gitignore create mode 100644 vendor/github.com/writeas/go-writeas/v2/LICENSE create mode 100644 vendor/github.com/writeas/go-writeas/v2/README.md create mode 100644 vendor/github.com/writeas/go-writeas/v2/auth.go create mode 100644 vendor/github.com/writeas/go-writeas/v2/collection.go create mode 100644 vendor/github.com/writeas/go-writeas/v2/go.mod create mode 100644 vendor/github.com/writeas/go-writeas/v2/go.sum create mode 100644 vendor/github.com/writeas/go-writeas/v2/post.go create mode 100644 vendor/github.com/writeas/go-writeas/v2/user.go create mode 100644 vendor/github.com/writeas/go-writeas/v2/writeas.go create mode 100644 vendor/github.com/writeas/impart/.gitignore create mode 100644 vendor/github.com/writeas/impart/LICENSE create mode 100644 vendor/github.com/writeas/impart/README.md create mode 100644 vendor/github.com/writeas/impart/doc.go create mode 100644 vendor/github.com/writeas/impart/errors.go create mode 100644 vendor/github.com/writeas/impart/go.mod create mode 100644 vendor/github.com/writeas/impart/request.go create mode 100644 vendor/github.com/writeas/impart/response.go create mode 100644 vendor/github.com/writeas/saturday/.gitignore create mode 100644 vendor/github.com/writeas/saturday/.travis.yml create mode 100644 vendor/github.com/writeas/saturday/LICENSE.txt create mode 100644 vendor/github.com/writeas/saturday/README.md create mode 100644 vendor/github.com/writeas/saturday/block.go create mode 100644 vendor/github.com/writeas/saturday/html.go create mode 100644 vendor/github.com/writeas/saturday/inline.go create mode 100644 vendor/github.com/writeas/saturday/latex.go create mode 100644 vendor/github.com/writeas/saturday/markdown.go create mode 100644 vendor/github.com/writeas/saturday/smartypants.go create mode 100644 vendor/github.com/writeas/web-core/LICENSE create mode 100644 vendor/github.com/writeas/web-core/posts/parse.go create mode 100644 vendor/github.com/writeas/web-core/posts/render.go create mode 100644 vendor/golang.org/x/crypto/AUTHORS create mode 100644 vendor/golang.org/x/crypto/CONTRIBUTORS create mode 100644 vendor/golang.org/x/crypto/LICENSE create mode 100644 vendor/golang.org/x/crypto/PATENTS create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/terminal.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_aix.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_bsd.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_linux.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_plan9.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go create mode 100644 vendor/golang.org/x/crypto/ssh/terminal/util_windows.go create mode 100644 vendor/golang.org/x/net/AUTHORS create mode 100644 vendor/golang.org/x/net/CONTRIBUTORS create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/html/atom/atom.go create mode 100644 vendor/golang.org/x/net/html/atom/table.go create mode 100644 vendor/golang.org/x/net/html/const.go create mode 100644 vendor/golang.org/x/net/html/doc.go create mode 100644 vendor/golang.org/x/net/html/doctype.go create mode 100644 vendor/golang.org/x/net/html/entity.go create mode 100644 vendor/golang.org/x/net/html/escape.go create mode 100644 vendor/golang.org/x/net/html/foreign.go create mode 100644 vendor/golang.org/x/net/html/node.go create mode 100644 vendor/golang.org/x/net/html/parse.go create mode 100644 vendor/golang.org/x/net/html/render.go create mode 100644 vendor/golang.org/x/net/html/token.go create mode 100644 vendor/golang.org/x/sys/AUTHORS create mode 100644 vendor/golang.org/x/sys/CONTRIBUTORS create mode 100644 vendor/golang.org/x/sys/LICENSE create mode 100644 vendor/golang.org/x/sys/PATENTS create mode 100644 vendor/golang.org/x/sys/unix/.gitignore create mode 100644 vendor/golang.org/x/sys/unix/README.md create mode 100644 vendor/golang.org/x/sys/unix/affinity_linux.go create mode 100644 vendor/golang.org/x/sys/unix/aliases.go create mode 100644 vendor/golang.org/x/sys/unix/asm_aix_ppc64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_darwin_386.s create mode 100644 vendor/golang.org/x/sys/unix/asm_darwin_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_darwin_arm.s create mode 100644 vendor/golang.org/x/sys/unix/asm_darwin_arm64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_dragonfly_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_freebsd_386.s create mode 100644 vendor/golang.org/x/sys/unix/asm_freebsd_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_freebsd_arm.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_386.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_arm.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_arm64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_mips64x.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_mipsx.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_ppc64x.s create mode 100644 vendor/golang.org/x/sys/unix/asm_linux_s390x.s create mode 100644 vendor/golang.org/x/sys/unix/asm_netbsd_386.s create mode 100644 vendor/golang.org/x/sys/unix/asm_netbsd_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_netbsd_arm.s create mode 100644 vendor/golang.org/x/sys/unix/asm_openbsd_386.s create mode 100644 vendor/golang.org/x/sys/unix/asm_openbsd_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/asm_openbsd_arm.s create mode 100644 vendor/golang.org/x/sys/unix/asm_solaris_amd64.s create mode 100644 vendor/golang.org/x/sys/unix/bluetooth_linux.go create mode 100644 vendor/golang.org/x/sys/unix/cap_freebsd.go create mode 100644 vendor/golang.org/x/sys/unix/constants.go create mode 100644 vendor/golang.org/x/sys/unix/dev_aix_ppc.go create mode 100644 vendor/golang.org/x/sys/unix/dev_aix_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/dev_darwin.go create mode 100644 vendor/golang.org/x/sys/unix/dev_dragonfly.go create mode 100644 vendor/golang.org/x/sys/unix/dev_freebsd.go create mode 100644 vendor/golang.org/x/sys/unix/dev_linux.go create mode 100644 vendor/golang.org/x/sys/unix/dev_netbsd.go create mode 100644 vendor/golang.org/x/sys/unix/dev_openbsd.go create mode 100644 vendor/golang.org/x/sys/unix/dirent.go create mode 100644 vendor/golang.org/x/sys/unix/endian_big.go create mode 100644 vendor/golang.org/x/sys/unix/endian_little.go create mode 100644 vendor/golang.org/x/sys/unix/env_unix.go create mode 100644 vendor/golang.org/x/sys/unix/errors_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/errors_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/errors_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/fcntl.go create mode 100644 vendor/golang.org/x/sys/unix/fcntl_linux_32bit.go create mode 100644 vendor/golang.org/x/sys/unix/gccgo.go create mode 100644 vendor/golang.org/x/sys/unix/gccgo_c.c create mode 100644 vendor/golang.org/x/sys/unix/gccgo_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ioctl.go create mode 100644 vendor/golang.org/x/sys/unix/mkall.sh create mode 100644 vendor/golang.org/x/sys/unix/mkerrors.sh create mode 100644 vendor/golang.org/x/sys/unix/mksyscall_aix_ppc.pl create mode 100644 vendor/golang.org/x/sys/unix/mksyscall_aix_ppc64.pl create mode 100644 vendor/golang.org/x/sys/unix/mksyscall_solaris.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysctl_openbsd.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysnum_darwin.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysnum_dragonfly.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysnum_freebsd.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysnum_netbsd.pl create mode 100644 vendor/golang.org/x/sys/unix/mksysnum_openbsd.pl create mode 100644 vendor/golang.org/x/sys/unix/openbsd_pledge.go create mode 100644 vendor/golang.org/x/sys/unix/openbsd_unveil.go create mode 100644 vendor/golang.org/x/sys/unix/pagesize_unix.go create mode 100644 vendor/golang.org/x/sys/unix/race.go create mode 100644 vendor/golang.org/x/sys/unix/race0.go create mode 100644 vendor/golang.org/x/sys/unix/sockcmsg_linux.go create mode 100644 vendor/golang.org/x/sys/unix/sockcmsg_unix.go create mode 100644 vendor/golang.org/x/sys/unix/str.go create mode 100644 vendor/golang.org/x/sys/unix/syscall.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_aix.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_aix_ppc.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_aix_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_bsd.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_dragonfly.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_dragonfly_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_freebsd.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_amd64_gc.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_gc.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_gc_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_gccgo_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_gccgo_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_mips64x.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_mipsx.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_ppc64x.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_s390x.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_linux_sparc64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_netbsd.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_netbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_netbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_netbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_openbsd.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_solaris.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_solaris_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_unix.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_unix_gc.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_unix_gc_ppc64x.go create mode 100644 vendor/golang.org/x/sys/unix/timestruct.go create mode 100644 vendor/golang.org/x/sys/unix/xattr_bsd.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_aix_ppc.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_aix_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_darwin_386.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_darwin_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_dragonfly_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_386.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_mips.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_netbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_netbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_netbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zerrors_solaris_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zptrace386_linux.go create mode 100644 vendor/golang.org/x/sys/unix/zptracearm_linux.go create mode 100644 vendor/golang.org/x/sys/unix/zptracemips_linux.go create mode 100644 vendor/golang.org/x/sys/unix/zptracemipsle_linux.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_aix_ppc.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_aix_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_aix_ppc64_gc.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_aix_ppc64_gccgo.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_dragonfly_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_mips.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_mips64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_mips64le.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_mipsle.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_ppc64le.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_s390x.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_linux_sparc64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_netbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_netbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_netbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_solaris_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysctl_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysctl_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysctl_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_darwin_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_darwin_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_darwin_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_dragonfly_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_netbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_netbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_netbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/zsysnum_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_aix_ppc.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_aix_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_darwin_386.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_darwin_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_386.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_mips.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_openbsd_386.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_openbsd_amd64.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_openbsd_arm.go create mode 100644 vendor/golang.org/x/sys/unix/ztypes_solaris_amd64.go create mode 100644 vendor/golang.org/x/sys/windows/aliases.go create mode 100644 vendor/golang.org/x/sys/windows/asm_windows_386.s create mode 100644 vendor/golang.org/x/sys/windows/asm_windows_amd64.s create mode 100644 vendor/golang.org/x/sys/windows/asm_windows_arm.s create mode 100644 vendor/golang.org/x/sys/windows/dll_windows.go create mode 100644 vendor/golang.org/x/sys/windows/env_windows.go create mode 100644 vendor/golang.org/x/sys/windows/eventlog.go create mode 100644 vendor/golang.org/x/sys/windows/exec_windows.go create mode 100644 vendor/golang.org/x/sys/windows/memory_windows.go create mode 100644 vendor/golang.org/x/sys/windows/mksyscall.go create mode 100644 vendor/golang.org/x/sys/windows/race.go create mode 100644 vendor/golang.org/x/sys/windows/race0.go create mode 100644 vendor/golang.org/x/sys/windows/security_windows.go create mode 100644 vendor/golang.org/x/sys/windows/service.go create mode 100644 vendor/golang.org/x/sys/windows/str.go create mode 100644 vendor/golang.org/x/sys/windows/syscall.go create mode 100644 vendor/golang.org/x/sys/windows/syscall_windows.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_386.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_amd64.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_arm.go create mode 100644 vendor/golang.org/x/sys/windows/zsyscall_windows.go create mode 100644 vendor/gopkg.in/ini.v1/.gitignore create mode 100644 vendor/gopkg.in/ini.v1/.travis.yml create mode 100644 vendor/gopkg.in/ini.v1/LICENSE create mode 100644 vendor/gopkg.in/ini.v1/Makefile create mode 100644 vendor/gopkg.in/ini.v1/README.md create mode 100644 vendor/gopkg.in/ini.v1/error.go create mode 100644 vendor/gopkg.in/ini.v1/file.go create mode 100644 vendor/gopkg.in/ini.v1/ini.go create mode 100644 vendor/gopkg.in/ini.v1/key.go create mode 100644 vendor/gopkg.in/ini.v1/parser.go create mode 100644 vendor/gopkg.in/ini.v1/section.go create mode 100644 vendor/gopkg.in/ini.v1/struct.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/.flake8 create mode 100644 vendor/gopkg.in/urfave/cli.v1/.gitignore create mode 100644 vendor/gopkg.in/urfave/cli.v1/.travis.yml create mode 100644 vendor/gopkg.in/urfave/cli.v1/CHANGELOG.md create mode 100644 vendor/gopkg.in/urfave/cli.v1/LICENSE create mode 100644 vendor/gopkg.in/urfave/cli.v1/README.md create mode 100644 vendor/gopkg.in/urfave/cli.v1/app.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/appveyor.yml create mode 100644 vendor/gopkg.in/urfave/cli.v1/category.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/cli.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/command.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/context.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/errors.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/flag-types.json create mode 100644 vendor/gopkg.in/urfave/cli.v1/flag.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/flag_generated.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/funcs.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/generate-flag-types create mode 100644 vendor/gopkg.in/urfave/cli.v1/help.go create mode 100644 vendor/gopkg.in/urfave/cli.v1/runtests create mode 100644 vendor/modules.txt diff --git a/go.mod b/go.mod index f03be5d..c717a98 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/writeas/writeas-cli require ( - code.as/core/socks v1.0.0 github.com/atotto/clipboard v0.1.1 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect @@ -24,3 +23,5 @@ require ( gopkg.in/ini.v1 v1.39.3 gopkg.in/urfave/cli.v1 v1.20.0 ) + +go 1.13 diff --git a/vendor/code.as/core/socks/.gitignore b/vendor/code.as/core/socks/.gitignore new file mode 100644 index 0000000..0026861 --- /dev/null +++ b/vendor/code.as/core/socks/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/code.as/core/socks/LICENSE b/vendor/code.as/core/socks/LICENSE new file mode 100644 index 0000000..47289bb --- /dev/null +++ b/vendor/code.as/core/socks/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012, Hailiang Wang. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/code.as/core/socks/README.md b/vendor/code.as/core/socks/README.md new file mode 100644 index 0000000..869c183 --- /dev/null +++ b/vendor/code.as/core/socks/README.md @@ -0,0 +1,58 @@ +SOCKS +===== + +[![GoDoc](https://godoc.org/code.as/core/socks?status.svg)](https://godoc.org/code.as/core/socks) + +SOCKS is a SOCKS4, SOCKS4A and SOCKS5 proxy package for Go, forked from [h12w/socks](https://github.com/h12w/socks) and patched so it's `go get`able. + +## Quick Start +### Get the package + + go get -u "code.as/core/socks" + +### Import the package + + import "code.as/core/socks" + +### Create a SOCKS proxy dialing function + + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") + tr := &http.Transport{Dial: dialSocksProxy} + httpClient := &http.Client{Transport: tr} + +## Example + +```go +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + + "code.as/core/socks" +) + +func main() { + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") + tr := &http.Transport{Dial: dialSocksProxy} + httpClient := &http.Client{Transport: tr} + resp, err := httpClient.Get("http://www.google.com") + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Fatal(resp.StatusCode) + } + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(buf)) +} +``` + +## Alternatives +http://godoc.org/golang.org/x/net/proxy diff --git a/vendor/code.as/core/socks/socks.go b/vendor/code.as/core/socks/socks.go new file mode 100644 index 0000000..78602ec --- /dev/null +++ b/vendor/code.as/core/socks/socks.go @@ -0,0 +1,218 @@ +// Copyright 2012, Hailiang Wang. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package socks implements a SOCKS (SOCKS4, SOCKS4A and SOCKS5) proxy client. + +A complete example using this package: + package main + + import ( + "code.as/core/socks" + "fmt" + "net/http" + "io/ioutil" + ) + + func main() { + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, "127.0.0.1:1080") + tr := &http.Transport{Dial: dialSocksProxy} + httpClient := &http.Client{Transport: tr} + + bodyText, err := TestHttpsGet(httpClient, "https://h12.io/about") + if err != nil { + fmt.Println(err.Error()) + } + fmt.Print(bodyText) + } + + func TestHttpsGet(c *http.Client, url string) (bodyText string, err error) { + resp, err := c.Get(url) + if err != nil { return } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { return } + bodyText = string(body) + return + } +*/ +package socks + +import ( + "errors" + "fmt" + "net" + "strconv" +) + +// Constants to choose which version of SOCKS protocol to use. +const ( + SOCKS4 = iota + SOCKS4A + SOCKS5 +) + +// DialSocksProxy returns the dial function to be used in http.Transport object. +// Argument socksType should be one of SOCKS4, SOCKS4A and SOCKS5. +// Argument proxy should be in this format "127.0.0.1:1080". +func DialSocksProxy(socksType int, proxy string) func(string, string) (net.Conn, error) { + if socksType == SOCKS5 { + return func(_, targetAddr string) (conn net.Conn, err error) { + return dialSocks5(proxy, targetAddr) + } + } + + // SOCKS4, SOCKS4A + return func(_, targetAddr string) (conn net.Conn, err error) { + return dialSocks4(socksType, proxy, targetAddr) + } +} + +func dialSocks5(proxy, targetAddr string) (conn net.Conn, err error) { + // dial TCP + conn, err = net.Dial("tcp", proxy) + if err != nil { + return + } + + // version identifier/method selection request + req := []byte{ + 5, // version number + 1, // number of methods + 0, // method 0: no authentication (only anonymous access supported for now) + } + resp, err := sendReceive(conn, req) + if err != nil { + return + } else if len(resp) != 2 { + err = errors.New("Server does not respond properly.") + return + } else if resp[0] != 5 { + err = errors.New("Server does not support Socks 5.") + return + } else if resp[1] != 0 { // no auth + err = errors.New("socks method negotiation failed.") + return + } + + // detail request + host, port, err := splitHostPort(targetAddr) + req = []byte{ + 5, // version number + 1, // connect command + 0, // reserved, must be zero + 3, // address type, 3 means domain name + byte(len(host)), // address length + } + req = append(req, []byte(host)...) + req = append(req, []byte{ + byte(port >> 8), // higher byte of destination port + byte(port), // lower byte of destination port (big endian) + }...) + resp, err = sendReceive(conn, req) + if err != nil { + return + } else if len(resp) != 10 { + err = errors.New("Server does not respond properly.") + } else if resp[1] != 0 { + err = errors.New("Can't complete SOCKS5 connection.") + } + + return +} + +func dialSocks4(socksType int, proxy, targetAddr string) (conn net.Conn, err error) { + // dial TCP + conn, err = net.Dial("tcp", proxy) + if err != nil { + return + } + + // connection request + host, port, err := splitHostPort(targetAddr) + if err != nil { + return + } + ip := net.IPv4(0, 0, 0, 1).To4() + if socksType == SOCKS4 { + ip, err = lookupIP(host) + if err != nil { + return + } + } + req := []byte{ + 4, // version number + 1, // command CONNECT + byte(port >> 8), // higher byte of destination port + byte(port), // lower byte of destination port (big endian) + ip[0], ip[1], ip[2], ip[3], // special invalid IP address to indicate the host name is provided + 0, // user id is empty, anonymous proxy only + } + if socksType == SOCKS4A { + req = append(req, []byte(host+"\x00")...) + } + + resp, err := sendReceive(conn, req) + if err != nil { + return + } else if len(resp) != 8 { + err = errors.New("Server does not respond properly.") + return + } + switch resp[1] { + case 90: + // request granted + case 91: + err = errors.New("Socks connection request rejected or failed.") + case 92: + err = errors.New("Socks connection request rejected becasue SOCKS server cannot connect to identd on the client.") + case 93: + err = errors.New("Socks connection request rejected because the client program and identd report different user-ids.") + default: + err = errors.New("Socks connection request failed, unknown error.") + } + return +} + +func sendReceive(conn net.Conn, req []byte) (resp []byte, err error) { + _, err = conn.Write(req) + if err != nil { + return + } + resp, err = readAll(conn) + return +} + +func readAll(conn net.Conn) (resp []byte, err error) { + resp = make([]byte, 1024) + n, err := conn.Read(resp) + resp = resp[:n] + return +} + +func lookupIP(host string) (ip net.IP, err error) { + ips, err := net.LookupIP(host) + if err != nil { + return + } + if len(ips) == 0 { + err = errors.New(fmt.Sprintf("Cannot resolve host: %s.", host)) + return + } + ip = ips[0].To4() + if len(ip) != net.IPv4len { + fmt.Println(len(ip), ip) + err = errors.New("IPv6 is not supported by SOCKS4.") + return + } + return +} + +func splitHostPort(addr string) (host string, port uint16, err error) { + host, portStr, err := net.SplitHostPort(addr) + portInt, err := strconv.ParseUint(portStr, 10, 16) + port = uint16(portInt) + return +} diff --git a/vendor/github.com/atotto/clipboard/.travis.yml b/vendor/github.com/atotto/clipboard/.travis.yml new file mode 100644 index 0000000..5bd5ae3 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/.travis.yml @@ -0,0 +1,20 @@ +language: go + +go: + - go1.4.3 + - go1.5.4 + - go1.6.4 + - go1.7.6 + - go1.8.7 + - go1.9.4 + - go1.10 + +before_install: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + +script: + - sudo apt-get install xsel + - go test -v . + - sudo apt-get install xclip + - go test -v . diff --git a/vendor/github.com/atotto/clipboard/LICENSE b/vendor/github.com/atotto/clipboard/LICENSE new file mode 100644 index 0000000..dee3257 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Ato Araki. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of @atotto. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/atotto/clipboard/README.md b/vendor/github.com/atotto/clipboard/README.md new file mode 100644 index 0000000..41fdd57 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/README.md @@ -0,0 +1,48 @@ +[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard) + +[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard) + +# Clipboard for Go + +Provide copying and pasting to the Clipboard for Go. + +Build: + + $ go get github.com/atotto/clipboard + +Platforms: + +* OSX +* Windows 7 (probably work on other Windows) +* Linux, Unix (requires 'xclip' or 'xsel' command to be installed) + + +Document: + +* http://godoc.org/github.com/atotto/clipboard + +Notes: + +* Text string only +* UTF-8 text encoding only (no conversion) + +TODO: + +* Clipboard watcher(?) + +## Commands: + +paste shell command: + + $ go get github.com/atotto/clipboard/cmd/gopaste + $ # example: + $ gopaste > document.txt + +copy shell command: + + $ go get github.com/atotto/clipboard/cmd/gocopy + $ # example: + $ cat document.txt | gocopy + + + diff --git a/vendor/github.com/atotto/clipboard/clipboard.go b/vendor/github.com/atotto/clipboard/clipboard.go new file mode 100644 index 0000000..d7907d3 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard.go @@ -0,0 +1,20 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package clipboard read/write on clipboard +package clipboard + +// ReadAll read string from clipboard +func ReadAll() (string, error) { + return readAll() +} + +// WriteAll write string to clipboard +func WriteAll(text string) error { + return writeAll(text) +} + +// Unsupported might be set true during clipboard init, to help callers decide +// whether or not to offer clipboard options. +var Unsupported bool diff --git a/vendor/github.com/atotto/clipboard/clipboard_darwin.go b/vendor/github.com/atotto/clipboard/clipboard_darwin.go new file mode 100644 index 0000000..6f33078 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_darwin.go @@ -0,0 +1,52 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin + +package clipboard + +import ( + "os/exec" +) + +var ( + pasteCmdArgs = "pbpaste" + copyCmdArgs = "pbcopy" +) + +func getPasteCommand() *exec.Cmd { + return exec.Command(pasteCmdArgs) +} + +func getCopyCommand() *exec.Cmd { + return exec.Command(copyCmdArgs) +} + +func readAll() (string, error) { + pasteCmd := getPasteCommand() + out, err := pasteCmd.Output() + if err != nil { + return "", err + } + return string(out), nil +} + +func writeAll(text string) error { + copyCmd := getCopyCommand() + in, err := copyCmd.StdinPipe() + if err != nil { + return err + } + + if err := copyCmd.Start(); err != nil { + return err + } + if _, err := in.Write([]byte(text)); err != nil { + return err + } + if err := in.Close(); err != nil { + return err + } + return copyCmd.Wait() +} diff --git a/vendor/github.com/atotto/clipboard/clipboard_unix.go b/vendor/github.com/atotto/clipboard/clipboard_unix.go new file mode 100644 index 0000000..d4f0df2 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_unix.go @@ -0,0 +1,112 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd linux netbsd openbsd solaris dragonfly + +package clipboard + +import ( + "errors" + "os/exec" +) + +const ( + xsel = "xsel" + xclip = "xclip" + termuxClipboardGet = "termux-clipboard-get" + termuxClipboardSet = "termux-clipboard-set" +) + +var ( + Primary bool + + pasteCmdArgs []string + copyCmdArgs []string + + xselPasteArgs = []string{xsel, "--output", "--clipboard"} + xselCopyArgs = []string{xsel, "--input", "--clipboard"} + + xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} + xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} + + termuxPasteArgs = []string{termuxClipboardGet} + termuxCopyArgs = []string{termuxClipboardSet} + + missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, or Termux:API add-on for termux-clipboard-get/set.") +) + +func init() { + pasteCmdArgs = xclipPasteArgs + copyCmdArgs = xclipCopyArgs + + if _, err := exec.LookPath(xclip); err == nil { + return + } + + pasteCmdArgs = xselPasteArgs + copyCmdArgs = xselCopyArgs + + if _, err := exec.LookPath(xsel); err == nil { + return + } + + pasteCmdArgs = termuxPasteArgs + copyCmdArgs = termuxCopyArgs + + if _, err := exec.LookPath(termuxClipboardSet); err == nil { + if _, err := exec.LookPath(termuxClipboardGet); err == nil { + return + } + } + + Unsupported = true +} + +func getPasteCommand() *exec.Cmd { + if Primary { + pasteCmdArgs = pasteCmdArgs[:1] + } + return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) +} + +func getCopyCommand() *exec.Cmd { + if Primary { + copyCmdArgs = copyCmdArgs[:1] + } + return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) +} + +func readAll() (string, error) { + if Unsupported { + return "", missingCommands + } + pasteCmd := getPasteCommand() + out, err := pasteCmd.Output() + if err != nil { + return "", err + } + return string(out), nil +} + +func writeAll(text string) error { + if Unsupported { + return missingCommands + } + copyCmd := getCopyCommand() + in, err := copyCmd.StdinPipe() + if err != nil { + return err + } + + if err := copyCmd.Start(); err != nil { + return err + } + if _, err := in.Write([]byte(text)); err != nil { + return err + } + if err := in.Close(); err != nil { + return err + } + return copyCmd.Wait() +} diff --git a/vendor/github.com/atotto/clipboard/clipboard_windows.go b/vendor/github.com/atotto/clipboard/clipboard_windows.go new file mode 100644 index 0000000..4b4aedb --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_windows.go @@ -0,0 +1,128 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package clipboard + +import ( + "syscall" + "time" + "unsafe" +) + +const ( + cfUnicodetext = 13 + gmemMoveable = 0x0002 +) + +var ( + user32 = syscall.MustLoadDLL("user32") + openClipboard = user32.MustFindProc("OpenClipboard") + closeClipboard = user32.MustFindProc("CloseClipboard") + emptyClipboard = user32.MustFindProc("EmptyClipboard") + getClipboardData = user32.MustFindProc("GetClipboardData") + setClipboardData = user32.MustFindProc("SetClipboardData") + + kernel32 = syscall.NewLazyDLL("kernel32") + globalAlloc = kernel32.NewProc("GlobalAlloc") + globalFree = kernel32.NewProc("GlobalFree") + globalLock = kernel32.NewProc("GlobalLock") + globalUnlock = kernel32.NewProc("GlobalUnlock") + lstrcpy = kernel32.NewProc("lstrcpyW") +) + +// waitOpenClipboard opens the clipboard, waiting for up to a second to do so. +func waitOpenClipboard() error { + started := time.Now() + limit := started.Add(time.Second) + var r uintptr + var err error + for time.Now().Before(limit) { + r, _, err = openClipboard.Call(0) + if r != 0 { + return nil + } + time.Sleep(time.Millisecond) + } + return err +} + +func readAll() (string, error) { + err := waitOpenClipboard() + if err != nil { + return "", err + } + defer closeClipboard.Call() + + h, _, err := getClipboardData.Call(cfUnicodetext) + if h == 0 { + return "", err + } + + l, _, err := globalLock.Call(h) + if l == 0 { + return "", err + } + + text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) + + r, _, err := globalUnlock.Call(h) + if r == 0 { + return "", err + } + + return text, nil +} + +func writeAll(text string) error { + err := waitOpenClipboard() + if err != nil { + return err + } + defer closeClipboard.Call() + + r, _, err := emptyClipboard.Call(0) + if r == 0 { + return err + } + + data := syscall.StringToUTF16(text) + + // "If the hMem parameter identifies a memory object, the object must have + // been allocated using the function with the GMEM_MOVEABLE flag." + h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) + if h == 0 { + return err + } + defer func() { + if h != 0 { + globalFree.Call(h) + } + }() + + l, _, err := globalLock.Call(h) + if l == 0 { + return err + } + + r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) + if r == 0 { + return err + } + + r, _, err = globalUnlock.Call(h) + if r == 0 { + if err.(syscall.Errno) != 0 { + return err + } + } + + r, _, err = setClipboardData.Call(cfUnicodetext, h) + if r == 0 { + return err + } + h = 0 // suppress deferred cleanup + return nil +} diff --git a/vendor/github.com/atotto/clipboard/go.mod b/vendor/github.com/atotto/clipboard/go.mod new file mode 100644 index 0000000..68ec980 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/go.mod @@ -0,0 +1 @@ +module github.com/atotto/clipboard diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/.travis.yml b/vendor/github.com/cloudfoundry/jibber_jabber/.travis.yml new file mode 100644 index 0000000..b19c2e5 --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/.travis.yml @@ -0,0 +1,11 @@ +language: go +go: + - 1.2 +before_install: +- go get github.com/onsi/ginkgo/... +- go get github.com/onsi/gomega/... +- go install github.com/onsi/ginkgo/ginkgo +script: PATH=$PATH:$HOME/gopath/bin ginkgo -r . +branches: + only: + - master diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/LICENSE b/vendor/github.com/cloudfoundry/jibber_jabber/LICENSE new file mode 100644 index 0000000..915b208 --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2014 Pivotal + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/README.md b/vendor/github.com/cloudfoundry/jibber_jabber/README.md new file mode 100644 index 0000000..d696eb6 --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/README.md @@ -0,0 +1,44 @@ +# Jibber Jabber [![Build Status](https://travis-ci.org/cloudfoundry/jibber_jabber.svg?branch=master)](https://travis-ci.org/cloudfoundry/jibber_jabber) +Jibber Jabber is a GoLang Library that can be used to detect an operating system's current language. + +### OS Support + +OSX and Linux via the `LC_ALL` and `LANG` environment variables. These are standard variables that are used in ALL versions of UNIX for language detection. + +Windows via [GetUserDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318136.aspx) and [GetSystemDefaultLocaleName](http://msdn.microsoft.com/en-us/library/windows/desktop/dd318122.aspx) system calls. These calls are supported in Windows Vista and up. + +# Usage +Add the following line to your go `import`: + +``` + "github.com/cloudfoundry/jibber_jabber" +``` + +### DetectIETF +`DetectIETF` will return the current locale as a string. The format of the locale will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code, a DASH, then an [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. + +``` + userLocale, err := jibber_jabber.DetectIETF() + println("Locale:", userLocale) +``` + +### DetectLanguage +`DetectLanguage` will return the current languge as a string. The format will be the [ISO 639](http://en.wikipedia.org/wiki/ISO_639) two-letter language code. + +``` + userLanguage, err := jibber_jabber.DetectLanguage() + println("Language:", userLanguage) +``` + +### DetectTerritory +`DetectTerritory` will return the current locale territory as a string. The format will be the [ISO 3166](http://en.wikipedia.org/wiki/ISO_3166-1) two-letter country code. + +``` + localeTerritory, err := jibber_jabber.DetectTerritory() + println("Territory:", localeTerritory) +``` + +### Errors +All the Detect commands will return an error if they are unable to read the Locale from the system. + +For Windows, additional error information is provided due to the nature of the system call being used. diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber.go b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber.go new file mode 100644 index 0000000..45d288e --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber.go @@ -0,0 +1,22 @@ +package jibber_jabber + +import ( + "strings" +) + +const ( + COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE = "Could not detect Language" +) + +func splitLocale(locale string) (string, string) { + formattedLocale := strings.Split(locale, ".")[0] + formattedLocale = strings.Replace(formattedLocale, "-", "_", -1) + + pieces := strings.Split(formattedLocale, "_") + language := pieces[0] + territory := "" + if len(pieces) > 1 { + territory = strings.Split(formattedLocale, "_")[1] + } + return language, territory +} diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_unix.go b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_unix.go new file mode 100644 index 0000000..374d761 --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_unix.go @@ -0,0 +1,57 @@ +// +build darwin freebsd linux netbsd openbsd + +package jibber_jabber + +import ( + "errors" + "os" + "strings" +) + +func getLangFromEnv() (locale string) { + locale = os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LANG") + } + return +} + +func getUnixLocale() (unix_locale string, err error) { + unix_locale = getLangFromEnv() + if unix_locale == "" { + err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE) + } + + return +} + +func DetectIETF() (locale string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + language, territory := splitLocale(unix_locale) + locale = language + if territory != "" { + locale = strings.Join([]string{language, territory}, "-") + } + } + + return +} + +func DetectLanguage() (language string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + language, _ = splitLocale(unix_locale) + } + + return +} + +func DetectTerritory() (territory string, err error) { + unix_locale, err := getUnixLocale() + if err == nil { + _, territory = splitLocale(unix_locale) + } + + return +} diff --git a/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_windows.go b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_windows.go new file mode 100644 index 0000000..1acd96c --- /dev/null +++ b/vendor/github.com/cloudfoundry/jibber_jabber/jibber_jabber_windows.go @@ -0,0 +1,114 @@ +// +build windows + +package jibber_jabber + +import ( + "errors" + "syscall" + "unsafe" +) + +const LOCALE_NAME_MAX_LENGTH uint32 = 85 + +var SUPPORTED_LOCALES = map[uintptr]string{ + 0x0407: "de-DE", + 0x0409: "en-US", + 0x0c0a: "es-ES", //or is it 0x040a + 0x040c: "fr-FR", + 0x0410: "it-IT", + 0x0411: "ja-JA", + 0x0412: "ko_KR", + 0x0416: "pt-BR", + //0x0419: "ru_RU", - Will add support for Russian when nicksnyder/go-i18n supports Russian + 0x0804: "zh-CN", + 0x0c04: "zh-HK", + 0x0404: "zh-TW", +} + +func getWindowsLocaleFrom(sysCall string) (locale string, err error) { + buffer := make([]uint16, LOCALE_NAME_MAX_LENGTH) + + dll := syscall.MustLoadDLL("kernel32") + proc := dll.MustFindProc(sysCall) + r, _, dllError := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(LOCALE_NAME_MAX_LENGTH)) + if r == 0 { + err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) + return + } + + locale = syscall.UTF16ToString(buffer) + + return +} + +func getAllWindowsLocaleFrom(sysCall string) (string, error) { + dll, err := syscall.LoadDLL("kernel32") + if err != nil { + return "", errors.New("Could not find kernel32 dll") + } + + proc, err := dll.FindProc(sysCall) + if err != nil { + return "", err + } + + locale, _, dllError := proc.Call() + if locale == 0 { + return "", errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error()) + } + + return SUPPORTED_LOCALES[locale], nil +} + +func getWindowsLocale() (locale string, err error) { + dll, err := syscall.LoadDLL("kernel32") + if err != nil { + return "", errors.New("Could not find kernel32 dll") + } + + proc, err := dll.FindProc("GetVersion") + if err != nil { + return "", err + } + + v, _, _ := proc.Call() + windowsVersion := byte(v) + isVistaOrGreater := (windowsVersion >= 6) + + if isVistaOrGreater { + locale, err = getWindowsLocaleFrom("GetUserDefaultLocaleName") + if err != nil { + locale, err = getWindowsLocaleFrom("GetSystemDefaultLocaleName") + } + } else if !isVistaOrGreater { + locale, err = getAllWindowsLocaleFrom("GetUserDefaultLCID") + if err != nil { + locale, err = getAllWindowsLocaleFrom("GetSystemDefaultLCID") + } + } else { + panic(v) + } + return +} +func DetectIETF() (locale string, err error) { + locale, err = getWindowsLocale() + return +} + +func DetectLanguage() (language string, err error) { + windows_locale, err := getWindowsLocale() + if err == nil { + language, _ = splitLocale(windows_locale) + } + + return +} + +func DetectTerritory() (territory string, err error) { + windows_locale, err := getWindowsLocale() + if err == nil { + _, territory = splitLocale(windows_locale) + } + + return +} diff --git a/vendor/github.com/hashicorp/errwrap/LICENSE b/vendor/github.com/hashicorp/errwrap/LICENSE new file mode 100644 index 0000000..c33dcc7 --- /dev/null +++ b/vendor/github.com/hashicorp/errwrap/LICENSE @@ -0,0 +1,354 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/hashicorp/errwrap/README.md b/vendor/github.com/hashicorp/errwrap/README.md new file mode 100644 index 0000000..444df08 --- /dev/null +++ b/vendor/github.com/hashicorp/errwrap/README.md @@ -0,0 +1,89 @@ +# errwrap + +`errwrap` is a package for Go that formalizes the pattern of wrapping errors +and checking if an error contains another error. + +There is a common pattern in Go of taking a returned `error` value and +then wrapping it (such as with `fmt.Errorf`) before returning it. The problem +with this pattern is that you completely lose the original `error` structure. + +Arguably the _correct_ approach is that you should make a custom structure +implementing the `error` interface, and have the original error as a field +on that structure, such [as this example](http://golang.org/pkg/os/#PathError). +This is a good approach, but you have to know the entire chain of possible +rewrapping that happens, when you might just care about one. + +`errwrap` formalizes this pattern (it doesn't matter what approach you use +above) by giving a single interface for wrapping errors, checking if a specific +error is wrapped, and extracting that error. + +## Installation and Docs + +Install using `go get github.com/hashicorp/errwrap`. + +Full documentation is available at +http://godoc.org/github.com/hashicorp/errwrap + +## Usage + +#### Basic Usage + +Below is a very basic example of its usage: + +```go +// A function that always returns an error, but wraps it, like a real +// function might. +func tryOpen() error { + _, err := os.Open("/i/dont/exist") + if err != nil { + return errwrap.Wrapf("Doesn't exist: {{err}}", err) + } + + return nil +} + +func main() { + err := tryOpen() + + // We can use the Contains helpers to check if an error contains + // another error. It is safe to do this with a nil error, or with + // an error that doesn't even use the errwrap package. + if errwrap.Contains(err, "does not exist") { + // Do something + } + if errwrap.ContainsType(err, new(os.PathError)) { + // Do something + } + + // Or we can use the associated `Get` functions to just extract + // a specific error. This would return nil if that specific error doesn't + // exist. + perr := errwrap.GetType(err, new(os.PathError)) +} +``` + +#### Custom Types + +If you're already making custom types that properly wrap errors, then +you can get all the functionality of `errwraps.Contains` and such by +implementing the `Wrapper` interface with just one function. Example: + +```go +type AppError { + Code ErrorCode + Err error +} + +func (e *AppError) WrappedErrors() []error { + return []error{e.Err} +} +``` + +Now this works: + +```go +err := &AppError{Err: fmt.Errorf("an error")} +if errwrap.ContainsType(err, fmt.Errorf("")) { + // This will work! +} +``` diff --git a/vendor/github.com/hashicorp/errwrap/errwrap.go b/vendor/github.com/hashicorp/errwrap/errwrap.go new file mode 100644 index 0000000..a733bef --- /dev/null +++ b/vendor/github.com/hashicorp/errwrap/errwrap.go @@ -0,0 +1,169 @@ +// Package errwrap implements methods to formalize error wrapping in Go. +// +// All of the top-level functions that take an `error` are built to be able +// to take any error, not just wrapped errors. This allows you to use errwrap +// without having to type-check and type-cast everywhere. +package errwrap + +import ( + "errors" + "reflect" + "strings" +) + +// WalkFunc is the callback called for Walk. +type WalkFunc func(error) + +// Wrapper is an interface that can be implemented by custom types to +// have all the Contains, Get, etc. functions in errwrap work. +// +// When Walk reaches a Wrapper, it will call the callback for every +// wrapped error in addition to the wrapper itself. Since all the top-level +// functions in errwrap use Walk, this means that all those functions work +// with your custom type. +type Wrapper interface { + WrappedErrors() []error +} + +// Wrap defines that outer wraps inner, returning an error type that +// can be cleanly used with the other methods in this package, such as +// Contains, GetAll, etc. +// +// This function won't modify the error message at all (the outer message +// will be used). +func Wrap(outer, inner error) error { + return &wrappedError{ + Outer: outer, + Inner: inner, + } +} + +// Wrapf wraps an error with a formatting message. This is similar to using +// `fmt.Errorf` to wrap an error. If you're using `fmt.Errorf` to wrap +// errors, you should replace it with this. +// +// format is the format of the error message. The string '{{err}}' will +// be replaced with the original error message. +func Wrapf(format string, err error) error { + outerMsg := "" + if err != nil { + outerMsg = err.Error() + } + + outer := errors.New(strings.Replace( + format, "{{err}}", outerMsg, -1)) + + return Wrap(outer, err) +} + +// Contains checks if the given error contains an error with the +// message msg. If err is not a wrapped error, this will always return +// false unless the error itself happens to match this msg. +func Contains(err error, msg string) bool { + return len(GetAll(err, msg)) > 0 +} + +// ContainsType checks if the given error contains an error with +// the same concrete type as v. If err is not a wrapped error, this will +// check the err itself. +func ContainsType(err error, v interface{}) bool { + return len(GetAllType(err, v)) > 0 +} + +// Get is the same as GetAll but returns the deepest matching error. +func Get(err error, msg string) error { + es := GetAll(err, msg) + if len(es) > 0 { + return es[len(es)-1] + } + + return nil +} + +// GetType is the same as GetAllType but returns the deepest matching error. +func GetType(err error, v interface{}) error { + es := GetAllType(err, v) + if len(es) > 0 { + return es[len(es)-1] + } + + return nil +} + +// GetAll gets all the errors that might be wrapped in err with the +// given message. The order of the errors is such that the outermost +// matching error (the most recent wrap) is index zero, and so on. +func GetAll(err error, msg string) []error { + var result []error + + Walk(err, func(err error) { + if err.Error() == msg { + result = append(result, err) + } + }) + + return result +} + +// GetAllType gets all the errors that are the same type as v. +// +// The order of the return value is the same as described in GetAll. +func GetAllType(err error, v interface{}) []error { + var result []error + + var search string + if v != nil { + search = reflect.TypeOf(v).String() + } + Walk(err, func(err error) { + var needle string + if err != nil { + needle = reflect.TypeOf(err).String() + } + + if needle == search { + result = append(result, err) + } + }) + + return result +} + +// Walk walks all the wrapped errors in err and calls the callback. If +// err isn't a wrapped error, this will be called once for err. If err +// is a wrapped error, the callback will be called for both the wrapper +// that implements error as well as the wrapped error itself. +func Walk(err error, cb WalkFunc) { + if err == nil { + return + } + + switch e := err.(type) { + case *wrappedError: + cb(e.Outer) + Walk(e.Inner, cb) + case Wrapper: + cb(err) + + for _, err := range e.WrappedErrors() { + Walk(err, cb) + } + default: + cb(err) + } +} + +// wrappedError is an implementation of error that has both the +// outer and inner errors. +type wrappedError struct { + Outer error + Inner error +} + +func (w *wrappedError) Error() string { + return w.Outer.Error() +} + +func (w *wrappedError) WrappedErrors() []error { + return []error{w.Outer, w.Inner} +} diff --git a/vendor/github.com/hashicorp/errwrap/go.mod b/vendor/github.com/hashicorp/errwrap/go.mod new file mode 100644 index 0000000..c9b8402 --- /dev/null +++ b/vendor/github.com/hashicorp/errwrap/go.mod @@ -0,0 +1 @@ +module github.com/hashicorp/errwrap diff --git a/vendor/github.com/hashicorp/go-multierror/.travis.yml b/vendor/github.com/hashicorp/go-multierror/.travis.yml new file mode 100644 index 0000000..304a835 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/.travis.yml @@ -0,0 +1,12 @@ +sudo: false + +language: go + +go: + - 1.x + +branches: + only: + - master + +script: make test testrace diff --git a/vendor/github.com/hashicorp/go-multierror/LICENSE b/vendor/github.com/hashicorp/go-multierror/LICENSE new file mode 100644 index 0000000..82b4de9 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/LICENSE @@ -0,0 +1,353 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. diff --git a/vendor/github.com/hashicorp/go-multierror/Makefile b/vendor/github.com/hashicorp/go-multierror/Makefile new file mode 100644 index 0000000..b97cd6e --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/Makefile @@ -0,0 +1,31 @@ +TEST?=./... + +default: test + +# test runs the test suite and vets the code. +test: generate + @echo "==> Running tests..." + @go list $(TEST) \ + | grep -v "/vendor/" \ + | xargs -n1 go test -timeout=60s -parallel=10 ${TESTARGS} + +# testrace runs the race checker +testrace: generate + @echo "==> Running tests (race)..." + @go list $(TEST) \ + | grep -v "/vendor/" \ + | xargs -n1 go test -timeout=60s -race ${TESTARGS} + +# updatedeps installs all the dependencies needed to run and build. +updatedeps: + @sh -c "'${CURDIR}/scripts/deps.sh' '${NAME}'" + +# generate runs `go generate` to build the dynamically generated source files. +generate: + @echo "==> Generating..." + @find . -type f -name '.DS_Store' -delete + @go list ./... \ + | grep -v "/vendor/" \ + | xargs -n1 go generate + +.PHONY: default test testrace updatedeps generate diff --git a/vendor/github.com/hashicorp/go-multierror/README.md b/vendor/github.com/hashicorp/go-multierror/README.md new file mode 100644 index 0000000..ead5830 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/README.md @@ -0,0 +1,97 @@ +# go-multierror + +[![Build Status](http://img.shields.io/travis/hashicorp/go-multierror.svg?style=flat-square)][travis] +[![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godocs] + +[travis]: https://travis-ci.org/hashicorp/go-multierror +[godocs]: https://godoc.org/github.com/hashicorp/go-multierror + +`go-multierror` is a package for Go that provides a mechanism for +representing a list of `error` values as a single `error`. + +This allows a function in Go to return an `error` that might actually +be a list of errors. If the caller knows this, they can unwrap the +list and access the errors. If the caller doesn't know, the error +formats to a nice human-readable format. + +`go-multierror` implements the +[errwrap](https://github.com/hashicorp/errwrap) interface so that it can +be used with that library, as well. + +## Installation and Docs + +Install using `go get github.com/hashicorp/go-multierror`. + +Full documentation is available at +http://godoc.org/github.com/hashicorp/go-multierror + +## Usage + +go-multierror is easy to use and purposely built to be unobtrusive in +existing Go applications/libraries that may not be aware of it. + +**Building a list of errors** + +The `Append` function is used to create a list of errors. This function +behaves a lot like the Go built-in `append` function: it doesn't matter +if the first argument is nil, a `multierror.Error`, or any other `error`, +the function behaves as you would expect. + +```go +var result error + +if err := step1(); err != nil { + result = multierror.Append(result, err) +} +if err := step2(); err != nil { + result = multierror.Append(result, err) +} + +return result +``` + +**Customizing the formatting of the errors** + +By specifying a custom `ErrorFormat`, you can customize the format +of the `Error() string` function: + +```go +var result *multierror.Error + +// ... accumulate errors here, maybe using Append + +if result != nil { + result.ErrorFormat = func([]error) string { + return "errors!" + } +} +``` + +**Accessing the list of errors** + +`multierror.Error` implements `error` so if the caller doesn't know about +multierror, it will work just fine. But if you're aware a multierror might +be returned, you can use type switches to access the list of errors: + +```go +if err := something(); err != nil { + if merr, ok := err.(*multierror.Error); ok { + // Use merr.Errors + } +} +``` + +**Returning a multierror only if there are errors** + +If you build a `multierror.Error`, you can use the `ErrorOrNil` function +to return an `error` implementation only if there are errors to return: + +```go +var result *multierror.Error + +// ... accumulate errors here + +// Return the `error` only if errors were added to the multierror, otherwise +// return nil since there are no errors. +return result.ErrorOrNil() +``` diff --git a/vendor/github.com/hashicorp/go-multierror/append.go b/vendor/github.com/hashicorp/go-multierror/append.go new file mode 100644 index 0000000..775b6e7 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/append.go @@ -0,0 +1,41 @@ +package multierror + +// Append is a helper function that will append more errors +// onto an Error in order to create a larger multi-error. +// +// If err is not a multierror.Error, then it will be turned into +// one. If any of the errs are multierr.Error, they will be flattened +// one level into err. +func Append(err error, errs ...error) *Error { + switch err := err.(type) { + case *Error: + // Typed nils can reach here, so initialize if we are nil + if err == nil { + err = new(Error) + } + + // Go through each error and flatten + for _, e := range errs { + switch e := e.(type) { + case *Error: + if e != nil { + err.Errors = append(err.Errors, e.Errors...) + } + default: + if e != nil { + err.Errors = append(err.Errors, e) + } + } + } + + return err + default: + newErrs := make([]error, 0, len(errs)+1) + if err != nil { + newErrs = append(newErrs, err) + } + newErrs = append(newErrs, errs...) + + return Append(&Error{}, newErrs...) + } +} diff --git a/vendor/github.com/hashicorp/go-multierror/flatten.go b/vendor/github.com/hashicorp/go-multierror/flatten.go new file mode 100644 index 0000000..aab8e9a --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/flatten.go @@ -0,0 +1,26 @@ +package multierror + +// Flatten flattens the given error, merging any *Errors together into +// a single *Error. +func Flatten(err error) error { + // If it isn't an *Error, just return the error as-is + if _, ok := err.(*Error); !ok { + return err + } + + // Otherwise, make the result and flatten away! + flatErr := new(Error) + flatten(err, flatErr) + return flatErr +} + +func flatten(err error, flatErr *Error) { + switch err := err.(type) { + case *Error: + for _, e := range err.Errors { + flatten(e, flatErr) + } + default: + flatErr.Errors = append(flatErr.Errors, err) + } +} diff --git a/vendor/github.com/hashicorp/go-multierror/format.go b/vendor/github.com/hashicorp/go-multierror/format.go new file mode 100644 index 0000000..47f13c4 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/format.go @@ -0,0 +1,27 @@ +package multierror + +import ( + "fmt" + "strings" +) + +// ErrorFormatFunc is a function callback that is called by Error to +// turn the list of errors into a string. +type ErrorFormatFunc func([]error) string + +// ListFormatFunc is a basic formatter that outputs the number of errors +// that occurred along with a bullet point list of the errors. +func ListFormatFunc(es []error) string { + if len(es) == 1 { + return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", es[0]) + } + + points := make([]string, len(es)) + for i, err := range es { + points[i] = fmt.Sprintf("* %s", err) + } + + return fmt.Sprintf( + "%d errors occurred:\n\t%s\n\n", + len(es), strings.Join(points, "\n\t")) +} diff --git a/vendor/github.com/hashicorp/go-multierror/go.mod b/vendor/github.com/hashicorp/go-multierror/go.mod new file mode 100644 index 0000000..2534331 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/go.mod @@ -0,0 +1,3 @@ +module github.com/hashicorp/go-multierror + +require github.com/hashicorp/errwrap v1.0.0 diff --git a/vendor/github.com/hashicorp/go-multierror/go.sum b/vendor/github.com/hashicorp/go-multierror/go.sum new file mode 100644 index 0000000..85b1f8f --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/go.sum @@ -0,0 +1,4 @@ +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/vendor/github.com/hashicorp/go-multierror/multierror.go b/vendor/github.com/hashicorp/go-multierror/multierror.go new file mode 100644 index 0000000..89b1422 --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/multierror.go @@ -0,0 +1,51 @@ +package multierror + +import ( + "fmt" +) + +// Error is an error type to track multiple errors. This is used to +// accumulate errors in cases and return them as a single "error". +type Error struct { + Errors []error + ErrorFormat ErrorFormatFunc +} + +func (e *Error) Error() string { + fn := e.ErrorFormat + if fn == nil { + fn = ListFormatFunc + } + + return fn(e.Errors) +} + +// ErrorOrNil returns an error interface if this Error represents +// a list of errors, or returns nil if the list of errors is empty. This +// function is useful at the end of accumulation to make sure that the value +// returned represents the existence of errors. +func (e *Error) ErrorOrNil() error { + if e == nil { + return nil + } + if len(e.Errors) == 0 { + return nil + } + + return e +} + +func (e *Error) GoString() string { + return fmt.Sprintf("*%#v", *e) +} + +// WrappedErrors returns the list of errors that this Error is wrapping. +// It is an implementation of the errwrap.Wrapper interface so that +// multierror.Error can be used with that library. +// +// This method is not safe to be called concurrently and is no different +// than accessing the Errors field directly. It is implemented only to +// satisfy the errwrap.Wrapper interface. +func (e *Error) WrappedErrors() []error { + return e.Errors +} diff --git a/vendor/github.com/hashicorp/go-multierror/prefix.go b/vendor/github.com/hashicorp/go-multierror/prefix.go new file mode 100644 index 0000000..5c477ab --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/prefix.go @@ -0,0 +1,37 @@ +package multierror + +import ( + "fmt" + + "github.com/hashicorp/errwrap" +) + +// Prefix is a helper function that will prefix some text +// to the given error. If the error is a multierror.Error, then +// it will be prefixed to each wrapped error. +// +// This is useful to use when appending multiple multierrors +// together in order to give better scoping. +func Prefix(err error, prefix string) error { + if err == nil { + return nil + } + + format := fmt.Sprintf("%s {{err}}", prefix) + switch err := err.(type) { + case *Error: + // Typed nils can reach here, so initialize if we are nil + if err == nil { + err = new(Error) + } + + // Wrap each of the errors + for i, e := range err.Errors { + err.Errors[i] = errwrap.Wrapf(format, e) + } + + return err + default: + return errwrap.Wrapf(format, err) + } +} diff --git a/vendor/github.com/hashicorp/go-multierror/sort.go b/vendor/github.com/hashicorp/go-multierror/sort.go new file mode 100644 index 0000000..fecb14e --- /dev/null +++ b/vendor/github.com/hashicorp/go-multierror/sort.go @@ -0,0 +1,16 @@ +package multierror + +// Len implements sort.Interface function for length +func (err Error) Len() int { + return len(err.Errors) +} + +// Swap implements sort.Interface function for swapping elements +func (err Error) Swap(i, j int) { + err.Errors[i], err.Errors[j] = err.Errors[j], err.Errors[i] +} + +// Less implements sort.Interface function for determining order +func (err Error) Less(i, j int) bool { + return err.Errors[i].Error() < err.Errors[j].Error() +} diff --git a/vendor/github.com/howeyc/gopass/.travis.yml b/vendor/github.com/howeyc/gopass/.travis.yml new file mode 100644 index 0000000..cc5d509 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/.travis.yml @@ -0,0 +1,11 @@ +language: go + +os: + - linux + - osx + +go: + - 1.3 + - 1.4 + - 1.5 + - tip diff --git a/vendor/github.com/howeyc/gopass/LICENSE.txt b/vendor/github.com/howeyc/gopass/LICENSE.txt new file mode 100644 index 0000000..14f7470 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/LICENSE.txt @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012 Chris Howey + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/howeyc/gopass/OPENSOLARIS.LICENSE b/vendor/github.com/howeyc/gopass/OPENSOLARIS.LICENSE new file mode 100644 index 0000000..da23621 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/OPENSOLARIS.LICENSE @@ -0,0 +1,384 @@ +Unless otherwise noted, all files in this distribution are released +under the Common Development and Distribution License (CDDL). +Exceptions are noted within the associated source files. + +-------------------------------------------------------------------- + + +COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 + +1. Definitions. + + 1.1. "Contributor" means each individual or entity that creates + or contributes to the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Software, prior Modifications used by a Contributor (if any), + and the Modifications made by that particular Contributor. + + 1.3. "Covered Software" means (a) the Original Software, or (b) + Modifications, or (c) the combination of files containing + Original Software with files containing Modifications, in + each case including portions thereof. + + 1.4. "Executable" means the Covered Software in any form other + than Source Code. + + 1.5. "Initial Developer" means the individual or entity that first + makes Original Software available under this License. + + 1.6. "Larger Work" means a work which combines Covered Software or + portions thereof with code not governed by the terms of this + License. + + 1.7. "License" means this document. + + 1.8. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed + herein. + + 1.9. "Modifications" means the Source Code and Executable form of + any of the following: + + A. Any file that results from an addition to, deletion from or + modification of the contents of a file containing Original + Software or previous Modifications; + + B. Any new file that contains any part of the Original + Software or previous Modifications; or + + C. Any new file that is contributed or otherwise made + available under the terms of this License. + + 1.10. "Original Software" means the Source Code and Executable + form of computer software code that is originally released + under this License. + + 1.11. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, + process, and apparatus claims, in any patent Licensable by + grantor. + + 1.12. "Source Code" means (a) the common form of computer software + code in which modifications are made and (b) associated + documentation included in or with such code. + + 1.13. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms + of, this License. For legal entities, "You" includes any + entity which controls, is controlled by, or is under common + control with You. For purposes of this definition, + "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty + percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants. + + 2.1. The Initial Developer Grant. + + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, the Initial + Developer hereby grants You a world-wide, royalty-free, + non-exclusive license: + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer, to use, + reproduce, modify, display, perform, sublicense and + distribute the Original Software (or portions thereof), + with or without Modifications, and/or as part of a Larger + Work; and + + (b) under Patent Claims infringed by the making, using or + selling of Original Software, to make, have made, use, + practice, sell, and offer for sale, and/or otherwise + dispose of the Original Software (or portions thereof). + + (c) The licenses granted in Sections 2.1(a) and (b) are + effective on the date Initial Developer first distributes + or otherwise makes the Original Software available to a + third party under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: (1) for code that You delete from the Original + Software, or (2) for infringements caused by: (i) the + modification of the Original Software, or (ii) the + combination of the Original Software with other software + or devices. + + 2.2. Contributor Grant. + + Conditioned upon Your compliance with Section 3.1 below and + subject to third party intellectual property claims, each + Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor to use, reproduce, + modify, display, perform, sublicense and distribute the + Modifications created by such Contributor (or portions + thereof), either on an unmodified basis, with other + Modifications, as Covered Software and/or as part of a + Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either + alone and/or in combination with its Contributor Version + (or portions of such combination), to make, use, sell, + offer for sale, have made, and/or otherwise dispose of: + (1) Modifications made by that Contributor (or portions + thereof); and (2) the combination of Modifications made by + that Contributor with its Contributor Version (or portions + of such combination). + + (c) The licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first distributes or + otherwise makes the Modifications available to a third + party. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: (1) for any code that Contributor has deleted + from the Contributor Version; (2) for infringements caused + by: (i) third party modifications of Contributor Version, + or (ii) the combination of Modifications made by that + Contributor with other software (except as part of the + Contributor Version) or other devices; or (3) under Patent + Claims infringed by Covered Software in the absence of + Modifications made by that Contributor. + +3. Distribution Obligations. + + 3.1. Availability of Source Code. + + Any Covered Software that You distribute or otherwise make + available in Executable form must also be made available in Source + Code form and that Source Code form must be distributed only under + the terms of this License. You must include a copy of this + License with every copy of the Source Code form of the Covered + Software You distribute or otherwise make available. You must + inform recipients of any such Covered Software in Executable form + as to how they can obtain such Covered Software in Source Code + form in a reasonable manner on or through a medium customarily + used for software exchange. + + 3.2. Modifications. + + The Modifications that You create or to which You contribute are + governed by the terms of this License. You represent that You + believe Your Modifications are Your original creation(s) and/or + You have sufficient rights to grant the rights conveyed by this + License. + + 3.3. Required Notices. + + You must include a notice in each of Your Modifications that + identifies You as the Contributor of the Modification. You may + not remove or alter any copyright, patent or trademark notices + contained within the Covered Software, or any notices of licensing + or any descriptive text giving attribution to any Contributor or + the Initial Developer. + + 3.4. Application of Additional Terms. + + You may not offer or impose any terms on any Covered Software in + Source Code form that alters or restricts the applicable version + of this License or the recipients' rights hereunder. You may + choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of + Covered Software. However, you may do so only on Your own behalf, + and not on behalf of the Initial Developer or any Contributor. + You must make it absolutely clear that any such warranty, support, + indemnity or liability obligation is offered by You alone, and You + hereby agree to indemnify the Initial Developer and every + Contributor for any liability incurred by the Initial Developer or + such Contributor as a result of warranty, support, indemnity or + liability terms You offer. + + 3.5. Distribution of Executable Versions. + + You may distribute the Executable form of the Covered Software + under the terms of this License or under the terms of a license of + Your choice, which may contain terms different from this License, + provided that You are in compliance with the terms of this License + and that the license for the Executable form does not attempt to + limit or alter the recipient's rights in the Source Code form from + the rights set forth in this License. If You distribute the + Covered Software in Executable form under a different license, You + must make it absolutely clear that any terms which differ from + this License are offered by You alone, not by the Initial + Developer or Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred + by the Initial Developer or such Contributor as a result of any + such terms You offer. + + 3.6. Larger Works. + + You may create a Larger Work by combining Covered Software with + other code not governed by the terms of this License and + distribute the Larger Work as a single product. In such a case, + You must make sure the requirements of this License are fulfilled + for the Covered Software. + +4. Versions of the License. + + 4.1. New Versions. + + Sun Microsystems, Inc. is the initial license steward and may + publish revised and/or new versions of this License from time to + time. Each version will be given a distinguishing version number. + Except as provided in Section 4.3, no one other than the license + steward has the right to modify this License. + + 4.2. Effect of New Versions. + + You may always continue to use, distribute or otherwise make the + Covered Software available under the terms of the version of the + License under which You originally received the Covered Software. + If the Initial Developer includes a notice in the Original + Software prohibiting it from being distributed or otherwise made + available under any subsequent version of the License, You must + distribute and make the Covered Software available under the terms + of the version of the License under which You originally received + the Covered Software. Otherwise, You may also choose to use, + distribute or otherwise make the Covered Software available under + the terms of any subsequent version of the License published by + the license steward. + + 4.3. Modified Versions. + + When You are an Initial Developer and You want to create a new + license for Your Original Software, You may create and use a + modified version of this License if You: (a) rename the license + and remove any references to the name of the license steward + (except to note that the license differs from this License); and + (b) otherwise make it clear that the license contains terms which + differ from this License. + +5. DISCLAIMER OF WARRANTY. + + COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" + BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, + INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED + SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR + PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND + PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY + COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE + INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY + NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF + WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS + DISCLAIMER. + +6. TERMINATION. + + 6.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to + cure such breach within 30 days of becoming aware of the breach. + Provisions which, by their nature, must remain in effect beyond + the termination of this License shall survive. + + 6.2. If You assert a patent infringement claim (excluding + declaratory judgment actions) against Initial Developer or a + Contributor (the Initial Developer or Contributor against whom You + assert such claim is referred to as "Participant") alleging that + the Participant Software (meaning the Contributor Version where + the Participant is a Contributor or the Original Software where + the Participant is the Initial Developer) directly or indirectly + infringes any patent, then any and all rights granted directly or + indirectly to You by such Participant, the Initial Developer (if + the Initial Developer is not the Participant) and all Contributors + under Sections 2.1 and/or 2.2 of this License shall, upon 60 days + notice from Participant terminate prospectively and automatically + at the expiration of such 60 day notice period, unless if within + such 60 day period You withdraw Your claim with respect to the + Participant Software against such Participant either unilaterally + or pursuant to a written agreement with Participant. + + 6.3. In the event of termination under Sections 6.1 or 6.2 above, + all end user licenses that have been validly granted by You or any + distributor hereunder prior to termination (excluding licenses + granted to You by any distributor) shall survive termination. + +7. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE + INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF + COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE + LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR + CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT + LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK + STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL + INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT + APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO + NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR + CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT + APPLY TO YOU. + +8. U.S. GOVERNMENT END USERS. + + The Covered Software is a "commercial item," as that term is + defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial + computer software" (as that term is defined at 48 + C.F.R. 252.227-7014(a)(1)) and "commercial computer software + documentation" as such terms are used in 48 C.F.R. 12.212 + (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 + C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all + U.S. Government End Users acquire Covered Software with only those + rights set forth herein. This U.S. Government Rights clause is in + lieu of, and supersedes, any other FAR, DFAR, or other clause or + provision that addresses Government rights in computer software + under this License. + +9. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed + by the law of the jurisdiction specified in a notice contained + within the Original Software (except to the extent applicable law, + if any, provides otherwise), excluding such jurisdiction's + conflict-of-law provisions. Any litigation relating to this + License shall be subject to the jurisdiction of the courts located + in the jurisdiction and venue specified in a notice contained + within the Original Software, with the losing party responsible + for costs, including, without limitation, court costs and + reasonable attorneys' fees and expenses. The application of the + United Nations Convention on Contracts for the International Sale + of Goods is expressly excluded. Any law or regulation which + provides that the language of a contract shall be construed + against the drafter shall not apply to this License. You agree + that You alone are responsible for compliance with the United + States export administration regulations (and the export control + laws and regulation of any other countries) when You use, + distribute or otherwise make available any Covered Software. + +10. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or + indirectly, out of its utilization of rights under this License + and You agree to work with Initial Developer and Contributors to + distribute such responsibility on an equitable basis. Nothing + herein is intended or shall be deemed to constitute any admission + of liability. + +-------------------------------------------------------------------- + +NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND +DISTRIBUTION LICENSE (CDDL) + +For Covered Software in this distribution, this License shall +be governed by the laws of the State of California (excluding +conflict-of-law provisions). + +Any litigation relating to this License shall be subject to the +jurisdiction of the Federal Courts of the Northern District of +California and the state courts of the State of California, with +venue lying in Santa Clara County, California. diff --git a/vendor/github.com/howeyc/gopass/README.md b/vendor/github.com/howeyc/gopass/README.md new file mode 100644 index 0000000..2d6a4e7 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/README.md @@ -0,0 +1,27 @@ +# getpasswd in Go [![GoDoc](https://godoc.org/github.com/howeyc/gopass?status.svg)](https://godoc.org/github.com/howeyc/gopass) [![Build Status](https://secure.travis-ci.org/howeyc/gopass.png?branch=master)](http://travis-ci.org/howeyc/gopass) + +Retrieve password from user terminal or piped input without echo. + +Verified on BSD, Linux, and Windows. + +Example: +```go +package main + +import "fmt" +import "github.com/howeyc/gopass" + +func main() { + fmt.Printf("Password: ") + + // Silent. For printing *'s use gopass.GetPasswdMasked() + pass, err := gopass.GetPasswd() + if err != nil { + // Handle gopass.ErrInterrupted or getch() read error + } + + // Do something with pass +} +``` + +Caution: Multi-byte characters not supported! diff --git a/vendor/github.com/howeyc/gopass/pass.go b/vendor/github.com/howeyc/gopass/pass.go new file mode 100644 index 0000000..f5bd5a5 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/pass.go @@ -0,0 +1,110 @@ +package gopass + +import ( + "errors" + "fmt" + "io" + "os" +) + +type FdReader interface { + io.Reader + Fd() uintptr +} + +var defaultGetCh = func(r io.Reader) (byte, error) { + buf := make([]byte, 1) + if n, err := r.Read(buf); n == 0 || err != nil { + if err != nil { + return 0, err + } + return 0, io.EOF + } + return buf[0], nil +} + +var ( + maxLength = 512 + ErrInterrupted = errors.New("interrupted") + ErrMaxLengthExceeded = fmt.Errorf("maximum byte limit (%v) exceeded", maxLength) + + // Provide variable so that tests can provide a mock implementation. + getch = defaultGetCh +) + +// getPasswd returns the input read from terminal. +// If prompt is not empty, it will be output as a prompt to the user +// If masked is true, typing will be matched by asterisks on the screen. +// Otherwise, typing will echo nothing. +func getPasswd(prompt string, masked bool, r FdReader, w io.Writer) ([]byte, error) { + var err error + var pass, bs, mask []byte + if masked { + bs = []byte("\b \b") + mask = []byte("*") + } + + if isTerminal(r.Fd()) { + if oldState, err := makeRaw(r.Fd()); err != nil { + return pass, err + } else { + defer func() { + restore(r.Fd(), oldState) + fmt.Fprintln(w) + }() + } + } + + if prompt != "" { + fmt.Fprint(w, prompt) + } + + // Track total bytes read, not just bytes in the password. This ensures any + // errors that might flood the console with nil or -1 bytes infinitely are + // capped. + var counter int + for counter = 0; counter <= maxLength; counter++ { + if v, e := getch(r); e != nil { + err = e + break + } else if v == 127 || v == 8 { + if l := len(pass); l > 0 { + pass = pass[:l-1] + fmt.Fprint(w, string(bs)) + } + } else if v == 13 || v == 10 { + break + } else if v == 3 { + err = ErrInterrupted + break + } else if v != 0 { + pass = append(pass, v) + fmt.Fprint(w, string(mask)) + } + } + + if counter > maxLength { + err = ErrMaxLengthExceeded + } + + return pass, err +} + +// GetPasswd returns the password read from the terminal without echoing input. +// The returned byte array does not include end-of-line characters. +func GetPasswd() ([]byte, error) { + return getPasswd("", false, os.Stdin, os.Stdout) +} + +// GetPasswdMasked returns the password read from the terminal, echoing asterisks. +// The returned byte array does not include end-of-line characters. +func GetPasswdMasked() ([]byte, error) { + return getPasswd("", true, os.Stdin, os.Stdout) +} + +// GetPasswdPrompt prompts the user and returns the password read from the terminal. +// If mask is true, then asterisks are echoed. +// The returned byte array does not include end-of-line characters. +func GetPasswdPrompt(prompt string, mask bool, r FdReader, w io.Writer) ([]byte, error) { + return getPasswd(prompt, mask, r, w) +} diff --git a/vendor/github.com/howeyc/gopass/terminal.go b/vendor/github.com/howeyc/gopass/terminal.go new file mode 100644 index 0000000..0835641 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/terminal.go @@ -0,0 +1,25 @@ +// +build !solaris + +package gopass + +import "golang.org/x/crypto/ssh/terminal" + +type terminalState struct { + state *terminal.State +} + +func isTerminal(fd uintptr) bool { + return terminal.IsTerminal(int(fd)) +} + +func makeRaw(fd uintptr) (*terminalState, error) { + state, err := terminal.MakeRaw(int(fd)) + + return &terminalState{ + state: state, + }, err +} + +func restore(fd uintptr, oldState *terminalState) error { + return terminal.Restore(int(fd), oldState.state) +} diff --git a/vendor/github.com/howeyc/gopass/terminal_solaris.go b/vendor/github.com/howeyc/gopass/terminal_solaris.go new file mode 100644 index 0000000..257e1b4 --- /dev/null +++ b/vendor/github.com/howeyc/gopass/terminal_solaris.go @@ -0,0 +1,69 @@ +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License, Version 1.0 only + * (the "License"). You may not use this file except in compliance + * with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or http://www.opensolaris.org/os/licensing. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ +// Below is derived from Solaris source, so CDDL license is included. + +package gopass + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +type terminalState struct { + state *unix.Termios +} + +// isTerminal returns true if there is a terminal attached to the given +// file descriptor. +// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c +func isTerminal(fd uintptr) bool { + var termio unix.Termio + err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) + return err == nil +} + +// makeRaw puts the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +// Source: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c +func makeRaw(fd uintptr) (*terminalState, error) { + oldTermiosPtr, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) + if err != nil { + return nil, err + } + oldTermios := *oldTermiosPtr + + newTermios := oldTermios + newTermios.Lflag &^= syscall.ECHO | syscall.ECHOE | syscall.ECHOK | syscall.ECHONL + if err := unix.IoctlSetTermios(int(fd), unix.TCSETS, &newTermios); err != nil { + return nil, err + } + + return &terminalState{ + state: oldTermiosPtr, + }, nil +} + +func restore(fd uintptr, oldState *terminalState) error { + return unix.IoctlSetTermios(int(fd), unix.TCSETS, oldState.state) +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml b/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml new file mode 100644 index 0000000..e0c8760 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/.coveralls.yml @@ -0,0 +1 @@ +repo_token: x2wlA1x0X8CK45ybWpZRCVRB4g7vtkhaw diff --git a/vendor/github.com/microcosm-cc/bluemonday/.travis.yml b/vendor/github.com/microcosm-cc/bluemonday/.travis.yml new file mode 100644 index 0000000..31fbbdd --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/.travis.yml @@ -0,0 +1,21 @@ +language: go +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - 1.9 + - 1.10 + - tip +matrix: + allow_failures: + - go: tip + fast_finish: true +install: + - go get golang.org/x/net/html +script: + - go test -v ./... diff --git a/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md new file mode 100644 index 0000000..d2b1230 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing to bluemonday + +Third-party patches are essential for keeping bluemonday secure and offering the features developers want. However there are a few guidelines that we need contributors to follow so that we can maintain the quality of work that developers who use bluemonday expect. + +## Getting Started + +* Make sure you have a [Github account](https://github.com/signup/free) + +## Guidelines + +1. Do not vendor dependencies. As a security package, were we to vendor dependencies the projects that then vendor bluemonday may not receive the latest security updates to the dependencies. By not vendoring dependencies the project that implements bluemonday will vendor the latest version of any dependent packages. Vendoring is a project problem, not a package problem. bluemonday will be tested against the latest version of dependencies periodically and during any PR/merge. + +## Submitting an Issue + +* Submit a ticket for your issue, assuming one does not already exist +* Clearly describe the issue including the steps to reproduce (with sample input and output) if it is a bug + +If you are reporting a security flaw, you may expect that we will provide the code to fix it for you. Otherwise you may want to submit a pull request to ensure the resolution is applied sooner rather than later: + +* Fork the repository on Github +* Issue a pull request containing code to resolve the issue + +## Submitting a Pull Request + +* Submit a ticket for your issue, assuming one does not already exist +* Describe the reason for the pull request and if applicable show some example inputs and outputs to demonstrate what the patch does +* Fork the repository on Github +* Before submitting the pull request you should + 1. Include tests for your patch, 1 test should encapsulate the entire patch and should refer to the Github issue + 1. If you have added new exposed/public functionality, you should ensure it is documented appropriately + 1. If you have added new exposed/public functionality, you should consider demonstrating how to use it within one of the helpers or shipped policies if appropriate or within a test if modifying a helper or policy is not appropriate + 1. Run all of the tests `go test -v ./...` or `make test` and ensure all tests pass + 1. Run gofmt `gofmt -w ./$*` or `make fmt` + 1. Run vet `go tool vet *.go` or `make vet` and resolve any issues + 1. Install golint using `go get -u github.com/golang/lint/golint` and run vet `golint *.go` or `make lint` and resolve every warning +* When submitting the pull request you should + 1. Note the issue(s) it resolves, i.e. `Closes #6` in the pull request comment to close issue #6 when the pull request is accepted + +Once you have submitted a pull request, we *may* merge it without changes. If we have any comments or feedback, or need you to make changes to your pull request we will update the Github pull request or the associated issue. We expect responses from you within two weeks, and we may close the pull request is there is no activity. + +### Contributor Licence Agreement + +We haven't gone for the formal "Sign a Contributor Licence Agreement" thing that projects like [puppet](https://cla.puppetlabs.com/), [Mojito](https://developer.yahoo.com/cocktails/mojito/cla/) and companies like [Google](http://code.google.com/legal/individual-cla-v1.0.html) are using. + +But we do need to know that we can accept and merge your contributions, so for now the act of contributing a pull request should be considered equivalent to agreeing to a contributor licence agreement, specifically: + +You accept that the act of submitting code to the bluemonday project is to grant a copyright licence to the project that is perpetual, worldwide, non-exclusive, no-charge, royalty free and irrevocable. + +You accept that all who comply with the licence of the project (BSD 3-clause) are permitted to use your contributions to the project. + +You accept, and by submitting code do declare, that you have the legal right to grant such a licence to the project and that each of the contributions is your own original creation. diff --git a/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md b/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md new file mode 100644 index 0000000..b98873f --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/CREDITS.md @@ -0,0 +1,6 @@ +1. Andrew Krasichkov @buglloc https://github.com/buglloc +1. John Graham-Cumming http://jgc.org/ +1. Mike Samuel mikesamuel@gmail.com +1. Dmitri Shuralyov shurcooL@gmail.com +1. https://github.com/opennota +1. https://github.com/Gufran \ No newline at end of file diff --git a/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md new file mode 100644 index 0000000..f822458 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/LICENSE.md @@ -0,0 +1,28 @@ +Copyright (c) 2014, David Kitchen + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the organisation (Microcosm) nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/microcosm-cc/bluemonday/Makefile b/vendor/github.com/microcosm-cc/bluemonday/Makefile new file mode 100644 index 0000000..b15dc74 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/Makefile @@ -0,0 +1,42 @@ +# Targets: +# +# all: Builds the code locally after testing +# +# fmt: Formats the source files +# build: Builds the code locally +# vet: Vets the code +# lint: Runs lint over the code (you do not need to fix everything) +# test: Runs the tests +# cover: Gives you the URL to a nice test coverage report +# +# install: Builds, tests and installs the code locally + +.PHONY: all fmt build vet lint test cover install + +# The first target is always the default action if `make` is called without +# args we build and install into $GOPATH so that it can just be run + +all: fmt vet test install + +fmt: + @gofmt -s -w ./$* + +build: + @go build + +vet: + @go vet *.go + +lint: + @golint *.go + +test: + @go test -v ./... + +cover: COVERAGE_FILE := coverage.out +cover: + @go test -coverprofile=$(COVERAGE_FILE) && \ + cover -html=$(COVERAGE_FILE) && rm $(COVERAGE_FILE) + +install: + @go install ./... diff --git a/vendor/github.com/microcosm-cc/bluemonday/README.md b/vendor/github.com/microcosm-cc/bluemonday/README.md new file mode 100644 index 0000000..ce679c1 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/README.md @@ -0,0 +1,350 @@ +# bluemonday [![Build Status](https://travis-ci.org/microcosm-cc/bluemonday.svg?branch=master)](https://travis-ci.org/microcosm-cc/bluemonday) [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge) + +bluemonday is a HTML sanitizer implemented in Go. It is fast and highly configurable. + +bluemonday takes untrusted user generated content as an input, and will return HTML that has been sanitised against a whitelist of approved HTML elements and attributes so that you can safely include the content in your web page. + +If you accept user generated content, and your server uses Go, you **need** bluemonday. + +The default policy for user generated content (`bluemonday.UGCPolicy().Sanitize()`) turns this: +```html +Hello World +``` + +Into a harmless: +```html +Hello World +``` + +And it turns this: +```html +XSS +``` + +Into this: +```html +XSS +``` + +Whilst still allowing this: +```html + + + +``` + +To pass through mostly unaltered (it gained a rel="nofollow" which is a good thing for user generated content): +```html + + + +``` + +It protects sites from [XSS](http://en.wikipedia.org/wiki/Cross-site_scripting) attacks. There are many [vectors for an XSS attack](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) and the best way to mitigate the risk is to sanitize user input against a known safe list of HTML elements and attributes. + +You should **always** run bluemonday **after** any other processing. + +If you use [blackfriday](https://github.com/russross/blackfriday) or [Pandoc](http://johnmacfarlane.net/pandoc/) then bluemonday should be run after these steps. This ensures that no insecure HTML is introduced later in your process. + +bluemonday is heavily inspired by both the [OWASP Java HTML Sanitizer](https://code.google.com/p/owasp-java-html-sanitizer/) and the [HTML Purifier](http://htmlpurifier.org/). + +## Technical Summary + +Whitelist based, you need to either build a policy describing the HTML elements and attributes to permit (and the `regexp` patterns of attributes), or use one of the supplied policies representing good defaults. + +The policy containing the whitelist is applied using a fast non-validating, forward only, token-based parser implemented in the [Go net/html library](https://godoc.org/golang.org/x/net/html) by the core Go team. + +We expect to be supplied with well-formatted HTML (closing elements for every applicable open element, nested correctly) and so we do not focus on repairing badly nested or incomplete HTML. We focus on simply ensuring that whatever elements do exist are described in the policy whitelist and that attributes and links are safe for use on your web page. [GIGO](http://en.wikipedia.org/wiki/Garbage_in,_garbage_out) does apply and if you feed it bad HTML bluemonday is not tasked with figuring out how to make it good again. + +### Supported Go Versions + +bluemonday is tested against Go 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, and tip. + +We do not support Go 1.0 as we depend on `golang.org/x/net/html` which includes a reference to `io.ErrNoProgress` which did not exist in Go 1.0. + +## Is it production ready? + +*Yes* + +We are using bluemonday in production having migrated from the widely used and heavily field tested OWASP Java HTML Sanitizer. + +We are passing our extensive test suite (including AntiSamy tests as well as tests for any issues raised). Check for any [unresolved issues](https://github.com/microcosm-cc/bluemonday/issues?page=1&state=open) to see whether anything may be a blocker for you. + +We invite pull requests and issues to help us ensure we are offering comprehensive protection against various attacks via user generated content. + +## Usage + +Install in your `${GOPATH}` using `go get -u github.com/microcosm-cc/bluemonday` + +Then call it: +```go +package main + +import ( + "fmt" + + "github.com/microcosm-cc/bluemonday" +) + +func main() { + // Do this once for each unique policy, and use the policy for the life of the program + // Policy creation/editing is not safe to use in multiple goroutines + p := bluemonday.UGCPolicy() + + // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines + html := p.Sanitize( + `Google`, + ) + + // Output: + // Google + fmt.Println(html) +} +``` + +We offer three ways to call Sanitize: +```go +p.Sanitize(string) string +p.SanitizeBytes([]byte) []byte +p.SanitizeReader(io.Reader) bytes.Buffer +``` + +If you are obsessed about performance, `p.SanitizeReader(r).Bytes()` will return a `[]byte` without performing any unnecessary casting of the inputs or outputs. Though the difference is so negligible you should never need to care. + +You can build your own policies: +```go +package main + +import ( + "fmt" + + "github.com/microcosm-cc/bluemonday" +) + +func main() { + p := bluemonday.NewPolicy() + + // Require URLs to be parseable by net/url.Parse and either: + // mailto: http:// or https:// + p.AllowStandardURLs() + + // We only allow

and + p.AllowAttrs("href").OnElements("a") + p.AllowElements("p") + + html := p.Sanitize( + `Google`, + ) + + // Output: + // Google + fmt.Println(html) +} +``` + +We ship two default policies: + +1. `bluemonday.StrictPolicy()` which can be thought of as equivalent to stripping all HTML elements and their attributes as it has nothing on its whitelist. An example usage scenario would be blog post titles where HTML tags are not expected at all and if they are then the elements *and* the content of the elements should be stripped. This is a *very* strict policy. +2. `bluemonday.UGCPolicy()` which allows a broad selection of HTML elements and attributes that are safe for user generated content. Note that this policy does *not* whitelist iframes, object, embed, styles, script, etc. An example usage scenario would be blog post bodies where a variety of formatting is expected along with the potential for TABLEs and IMGs. + +## Policy Building + +The essence of building a policy is to determine which HTML elements and attributes are considered safe for your scenario. OWASP provide an [XSS prevention cheat sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) to help explain the risks, but essentially: + +1. Avoid anything other than the standard HTML elements +1. Avoid `script`, `style`, `iframe`, `object`, `embed`, `base` elements that allow code to be executed by the client or third party content to be included that can execute code +1. Avoid anything other than plain HTML attributes with values matched to a regexp + +Basically, you should be able to describe what HTML is fine for your scenario. If you do not have confidence that you can describe your policy please consider using one of the shipped policies such as `bluemonday.UGCPolicy()`. + +To create a new policy: +```go +p := bluemonday.NewPolicy() +``` + +To add elements to a policy either add just the elements: +```go +p.AllowElements("b", "strong") +``` + +Or add elements as a virtue of adding an attribute: +```go +// Not the recommended pattern, see the recommendation on using .Matching() below +p.AllowAttrs("nowrap").OnElements("td", "th") +``` + +Attributes can either be added to all elements: +```go +p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally() +``` + +Or attributes can be added to specific elements: +```go +// Not the recommended pattern, see the recommendation on using .Matching() below +p.AllowAttrs("value").OnElements("li") +``` + +It is **always** recommended that an attribute be made to match a pattern. XSS in HTML attributes is very easy otherwise: +```go +// \p{L} matches unicode letters, \p{N} matches unicode numbers +p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally() +``` + +You can stop at any time and call .Sanitize(): +```go +// string htmlIn passed in from a HTTP POST +htmlOut := p.Sanitize(htmlIn) +``` + +And you can take any existing policy and extend it: +```go +p := bluemonday.UGCPolicy() +p.AllowElements("fieldset", "select", "option") +``` + +### Links + +Links are difficult beasts to sanitise safely and also one of the biggest attack vectors for malicious content. + +It is possible to do this: +```go +p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a") +``` + +But that will not protect you as the regular expression is insufficient in this case to have prevented a malformed value doing something unexpected. + +We provide some additional global options for safely working with links. + +`RequireParseableURLs` will ensure that URLs are parseable by Go's `net/url` package: +```go +p.RequireParseableURLs(true) +``` + +If you have enabled parseable URLs then the following option will `AllowRelativeURLs`. By default this is disabled (bluemonday is a whitelist tool... you need to explicitly tell us to permit things) and when disabled it will prevent all local and scheme relative URLs (i.e. `href="localpage.html"`, `href="../home.html"` and even `href="//www.google.com"` are relative): +```go +p.AllowRelativeURLs(true) +``` + +If you have enabled parseable URLs then you can whitelist the schemes (commonly called protocol when thinking of `http` and `https`) that are permitted. Bear in mind that allowing relative URLs in the above option will allow for a blank scheme: +```go +p.AllowURLSchemes("mailto", "http", "https") +``` + +Regardless of whether you have enabled parseable URLs, you can force all URLs to have a rel="nofollow" attribute. This will be added if it does not exist, but only when the `href` is valid: +```go +// This applies to "a" "area" "link" elements that have a "href" attribute +p.RequireNoFollowOnLinks(true) +``` + +We provide a convenience method that applies all of the above, but you will still need to whitelist the linkable elements for the URL rules to be applied to: +```go +p.AllowStandardURLs() +p.AllowAttrs("cite").OnElements("blockquote", "q") +p.AllowAttrs("href").OnElements("a", "area") +p.AllowAttrs("src").OnElements("img") +``` + +An additional complexity regarding links is the data URI as defined in [RFC2397](http://tools.ietf.org/html/rfc2397). The data URI allows for images to be served inline using this format: + +```html + +``` + +We have provided a helper to verify the mimetype followed by base64 content of data URIs links: + +```go +p.AllowDataURIImages() +``` + +That helper will enable GIF, JPEG, PNG and WEBP images. + +It should be noted that there is a potential [security](http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/) [risk](https://capec.mitre.org/data/definitions/244.html) with the use of data URI links. You should only enable data URI links if you already trust the content. + +We also have some features to help deal with user generated content: +```go +p.AddTargetBlankToFullyQualifiedLinks(true) +``` + +This will ensure that anchor `` links that are fully qualified (the href destination includes a host name) will get `target="_blank"` added to them. + +Additionally any link that has `target="_blank"` after the policy has been applied will also have the `rel` attribute adjusted to add `noopener`. This means a link may start like `` and will end up as ``. It is important to note that the addition of `noopener` is a security feature and not an issue. There is an unfortunate feature to browsers that a browser window opened as a result of `target="_blank"` can still control the opener (your web page) and this protects against that. The background to this can be found here: [https://dev.to/ben/the-targetblank-vulnerability-by-example](https://dev.to/ben/the-targetblank-vulnerability-by-example) + +### Policy Building Helpers + +We also bundle some helpers to simplify policy building: +```go + +// Permits the "dir", "id", "lang", "title" attributes globally +p.AllowStandardAttributes() + +// Permits the "img" element and its standard attributes +p.AllowImages() + +// Permits ordered and unordered lists, and also definition lists +p.AllowLists() + +// Permits HTML tables and all applicable elements and non-styling attributes +p.AllowTables() +``` + +### Invalid Instructions + +The following are invalid: +```go +// This does not say where the attributes are allowed, you need to add +// .Globally() or .OnElements(...) +// This will be ignored without error. +p.AllowAttrs("value") + +// This does not say where the attributes are allowed, you need to add +// .Globally() or .OnElements(...) +// This will be ignored without error. +p.AllowAttrs( + "type", +).Matching( + regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"), +) +``` + +Both examples exhibit the same issue, they declare attributes but do not then specify whether they are whitelisted globally or only on specific elements (and which elements). Attributes belong to one or more elements, and the policy needs to declare this. + +## Limitations + +We are not yet including any tools to help whitelist and sanitize CSS. Which means that unless you wish to do the heavy lifting in a single regular expression (inadvisable), **you should not allow the "style" attribute anywhere**. + +It is not the job of bluemonday to fix your bad HTML, it is merely the job of bluemonday to prevent malicious HTML getting through. If you have mismatched HTML elements, or non-conforming nesting of elements, those will remain. But if you have well-structured HTML bluemonday will not break it. + +## TODO + +* Add support for CSS sanitisation to allow some CSS properties based on a whitelist, possibly using the [Gorilla CSS3 scanner](http://www.gorillatoolkit.org/pkg/css/scanner) - PRs welcome so long as testing covers XSS and demonstrates safety first +* Investigate whether devs want to blacklist elements and attributes. This would allow devs to take an existing policy (such as the `bluemonday.UGCPolicy()` ) that encapsulates 90% of what they're looking for but does more than they need, and to remove the extra things they do not want to make it 100% what they want +* Investigate whether devs want a validating HTML mode, in which the HTML elements are not just transformed into a balanced tree (every start tag has a closing tag at the correct depth) but also that elements and character data appear only in their allowed context (i.e. that a `table` element isn't a descendent of a `caption`, that `colgroup`, `thead`, `tbody`, `tfoot` and `tr` are permitted, and that character data is not permitted) + +## Development + +If you have cloned this repo you will probably need the dependency: + +`go get golang.org/x/net/html` + +Gophers can use their familiar tools: + +`go build` + +`go test` + +I personally use a Makefile as it spares typing the same args over and over whilst providing consistency for those of us who jump from language to language and enjoy just typing `make` in a project directory and watch magic happen. + +`make` will build, vet, test and install the library. + +`make clean` will remove the library from a *single* `${GOPATH}/pkg` directory tree + +`make test` will run the tests + +`make cover` will run the tests and *open a browser window* with the coverage report + +`make lint` will run golint (install via `go get github.com/golang/lint/golint`) + +## Long term goals + +1. Open the code to adversarial peer review similar to the [Attack Review Ground Rules](https://code.google.com/p/owasp-java-html-sanitizer/wiki/AttackReviewGroundRules) +1. Raise funds and pay for an external security review diff --git a/vendor/github.com/microcosm-cc/bluemonday/doc.go b/vendor/github.com/microcosm-cc/bluemonday/doc.go new file mode 100644 index 0000000..71dab60 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/doc.go @@ -0,0 +1,104 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +Package bluemonday provides a way of describing a whitelist of HTML elements +and attributes as a policy, and for that policy to be applied to untrusted +strings from users that may contain markup. All elements and attributes not on +the whitelist will be stripped. + +The default bluemonday.UGCPolicy().Sanitize() turns this: + + Hello World + +Into the more harmless: + + Hello World + +And it turns this: + + XSS + +Into this: + + XSS + +Whilst still allowing this: + + + + + +To pass through mostly unaltered (it gained a rel="nofollow"): + + + + + +The primary purpose of bluemonday is to take potentially unsafe user generated +content (from things like Markdown, HTML WYSIWYG tools, etc) and make it safe +for you to put on your website. + +It protects sites against XSS (http://en.wikipedia.org/wiki/Cross-site_scripting) +and other malicious content that a user interface may deliver. There are many +vectors for an XSS attack (https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) +and the safest thing to do is to sanitize user input against a known safe list +of HTML elements and attributes. + +Note: You should always run bluemonday after any other processing. + +If you use blackfriday (https://github.com/russross/blackfriday) or +Pandoc (http://johnmacfarlane.net/pandoc/) then bluemonday should be run after +these steps. This ensures that no insecure HTML is introduced later in your +process. + +bluemonday is heavily inspired by both the OWASP Java HTML Sanitizer +(https://code.google.com/p/owasp-java-html-sanitizer/) and the HTML Purifier +(http://htmlpurifier.org/). + +We ship two default policies, one is bluemonday.StrictPolicy() and can be +thought of as equivalent to stripping all HTML elements and their attributes as +it has nothing on its whitelist. + +The other is bluemonday.UGCPolicy() and allows a broad selection of HTML +elements and attributes that are safe for user generated content. Note that +this policy does not whitelist iframes, object, embed, styles, script, etc. + +The essence of building a policy is to determine which HTML elements and +attributes are considered safe for your scenario. OWASP provide an XSS +prevention cheat sheet ( https://www.google.com/search?q=xss+prevention+cheat+sheet ) +to help explain the risks, but essentially: + + 1. Avoid whitelisting anything other than plain HTML elements + 2. Avoid whitelisting `script`, `style`, `iframe`, `object`, `embed`, `base` + elements + 3. Avoid whitelisting anything other than plain HTML elements with simple + values that you can match to a regexp +*/ +package bluemonday diff --git a/vendor/github.com/microcosm-cc/bluemonday/helpers.go b/vendor/github.com/microcosm-cc/bluemonday/helpers.go new file mode 100644 index 0000000..dfa5868 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/helpers.go @@ -0,0 +1,297 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "encoding/base64" + "net/url" + "regexp" +) + +// A selection of regular expressions that can be used as .Matching() rules on +// HTML attributes. +var ( + // CellAlign handles the `align` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-align + CellAlign = regexp.MustCompile(`(?i)^(center|justify|left|right|char)$`) + + // CellVerticalAlign handles the `valign` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-valign + CellVerticalAlign = regexp.MustCompile(`(?i)^(baseline|bottom|middle|top)$`) + + // Direction handles the `dir` attribute + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo#attr-dir + Direction = regexp.MustCompile(`(?i)^(rtl|ltr)$`) + + // ImageAlign handles the `align` attribute on the `image` tag + // http://www.w3.org/MarkUp/Test/Img/imgtest.html + ImageAlign = regexp.MustCompile( + `(?i)^(left|right|top|texttop|middle|absmiddle|baseline|bottom|absbottom)$`, + ) + + // Integer describes whole positive integers (including 0) used in places + // like td.colspan + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-colspan + Integer = regexp.MustCompile(`^[0-9]+$`) + + // ISO8601 according to the W3 group is only a subset of the ISO8601 + // standard: http://www.w3.org/TR/NOTE-datetime + // + // Used in places like time.datetime + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time#attr-datetime + // + // Matches patterns: + // Year: + // YYYY (eg 1997) + // Year and month: + // YYYY-MM (eg 1997-07) + // Complete date: + // YYYY-MM-DD (eg 1997-07-16) + // Complete date plus hours and minutes: + // YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) + // Complete date plus hours, minutes and seconds: + // YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) + // Complete date plus hours, minutes, seconds and a decimal fraction of a + // second + // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) + ISO8601 = regexp.MustCompile( + `^[0-9]{4}(-[0-9]{2}(-[0-9]{2}([ T][0-9]{2}(:[0-9]{2}){1,2}(.[0-9]{1,6})` + + `?Z?([\+-][0-9]{2}:[0-9]{2})?)?)?)?$`, + ) + + // ListType encapsulates the common value as well as the latest spec + // values for lists + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#attr-type + ListType = regexp.MustCompile(`(?i)^(circle|disc|square|a|A|i|I|1)$`) + + // SpaceSeparatedTokens is used in places like `a.rel` and the common attribute + // `class` which both contain space delimited lists of data tokens + // http://www.w3.org/TR/html-markup/datatypes.html#common.data.tokens-def + // Regexp: \p{L} matches unicode letters, \p{N} matches unicode numbers + SpaceSeparatedTokens = regexp.MustCompile(`^([\s\p{L}\p{N}_-]+)$`) + + // Number is a double value used on HTML5 meter and progress elements + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-button-element.html#the-meter-element + Number = regexp.MustCompile(`^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$`) + + // NumberOrPercent is used predominantly as units of measurement in width + // and height attributes + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-height + NumberOrPercent = regexp.MustCompile(`^[0-9]+[%]?$`) + + // Paragraph of text in an attribute such as *.'title', img.alt, etc + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-title + // Note that we are not allowing chars that could close tags like '>' + Paragraph = regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$`) + + // dataURIImagePrefix is used by AllowDataURIImages to define the acceptable + // prefix of data URIs that contain common web image formats. + // + // This is not exported as it's not useful by itself, and only has value + // within the AllowDataURIImages func + dataURIImagePrefix = regexp.MustCompile( + `^image/(gif|jpeg|png|webp);base64,`, + ) +) + +// AllowStandardURLs is a convenience function that will enable rel="nofollow" +// on "a", "area" and "link" (if you have allowed those elements) and will +// ensure that the URL values are parseable and either relative or belong to the +// "mailto", "http", or "https" schemes +func (p *Policy) AllowStandardURLs() { + // URLs must be parseable by net/url.Parse() + p.RequireParseableURLs(true) + + // !url.IsAbs() is permitted + p.AllowRelativeURLs(true) + + // Most common URL schemes only + p.AllowURLSchemes("mailto", "http", "https") + + // For all anchors we will add rel="nofollow" if it does not already exist + // This applies to "a" "area" "link" + p.RequireNoFollowOnLinks(true) +} + +// AllowStandardAttributes will enable "id", "title" and the language specific +// attributes "dir" and "lang" on all elements that are whitelisted +func (p *Policy) AllowStandardAttributes() { + // "dir" "lang" are permitted as both language attributes affect charsets + // and direction of text. + p.AllowAttrs("dir").Matching(Direction).Globally() + p.AllowAttrs( + "lang", + ).Matching(regexp.MustCompile(`[a-zA-Z]{2,20}`)).Globally() + + // "id" is permitted. This is pretty much as some HTML elements require this + // to work well ("dfn" is an example of a "id" being value) + // This does create a risk that JavaScript and CSS within your web page + // might identify the wrong elements. Ensure that you select things + // accurately + p.AllowAttrs("id").Matching( + regexp.MustCompile(`[a-zA-Z0-9\:\-_\.]+`), + ).Globally() + + // "title" is permitted as it improves accessibility. + p.AllowAttrs("title").Matching(Paragraph).Globally() +} + +// AllowStyling presently enables the class attribute globally. +// +// Note: When bluemonday ships a CSS parser and we can safely sanitise that, +// this will also allow sanitized styling of elements via the style attribute. +func (p *Policy) AllowStyling() { + + // "class" is permitted globally + p.AllowAttrs("class").Matching(SpaceSeparatedTokens).Globally() +} + +// AllowImages enables the img element and some popular attributes. It will also +// ensure that URL values are parseable. This helper does not enable data URI +// images, for that you should also use the AllowDataURIImages() helper. +func (p *Policy) AllowImages() { + + // "img" is permitted + p.AllowAttrs("align").Matching(ImageAlign).OnElements("img") + p.AllowAttrs("alt").Matching(Paragraph).OnElements("img") + p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("img") + + // Standard URLs enabled + p.AllowStandardURLs() + p.AllowAttrs("src").OnElements("img") +} + +// AllowDataURIImages permits the use of inline images defined in RFC2397 +// http://tools.ietf.org/html/rfc2397 +// http://en.wikipedia.org/wiki/Data_URI_scheme +// +// Images must have a mimetype matching: +// image/gif +// image/jpeg +// image/png +// image/webp +// +// NOTE: There is a potential security risk to allowing data URIs and you should +// only permit them on content you already trust. +// http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/ +// https://capec.mitre.org/data/definitions/244.html +func (p *Policy) AllowDataURIImages() { + + // URLs must be parseable by net/url.Parse() + p.RequireParseableURLs(true) + + // Supply a function to validate images contained within data URI + p.AllowURLSchemeWithCustomPolicy( + "data", + func(url *url.URL) (allowUrl bool) { + if url.RawQuery != "" || url.Fragment != "" { + return false + } + + matched := dataURIImagePrefix.FindString(url.Opaque) + if matched == "" { + return false + } + + _, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) + if err != nil { + return false + } + + return true + }, + ) +} + +// AllowLists will enabled ordered and unordered lists, as well as definition +// lists +func (p *Policy) AllowLists() { + // "ol" "ul" are permitted + p.AllowAttrs("type").Matching(ListType).OnElements("ol", "ul") + + // "li" is permitted + p.AllowAttrs("type").Matching(ListType).OnElements("li") + p.AllowAttrs("value").Matching(Integer).OnElements("li") + + // "dl" "dt" "dd" are permitted + p.AllowElements("dl", "dt", "dd") +} + +// AllowTables will enable a rich set of elements and attributes to describe +// HTML tables +func (p *Policy) AllowTables() { + + // "table" is permitted + p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("table") + p.AllowAttrs("summary").Matching(Paragraph).OnElements("table") + + // "caption" is permitted + p.AllowElements("caption") + + // "col" "colgroup" are permitted + p.AllowAttrs("align").Matching(CellAlign).OnElements("col", "colgroup") + p.AllowAttrs("height", "width").Matching( + NumberOrPercent, + ).OnElements("col", "colgroup") + p.AllowAttrs("span").Matching(Integer).OnElements("colgroup", "col") + p.AllowAttrs("valign").Matching( + CellVerticalAlign, + ).OnElements("col", "colgroup") + + // "thead" "tr" are permitted + p.AllowAttrs("align").Matching(CellAlign).OnElements("thead", "tr") + p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("thead", "tr") + + // "td" "th" are permitted + p.AllowAttrs("abbr").Matching(Paragraph).OnElements("td", "th") + p.AllowAttrs("align").Matching(CellAlign).OnElements("td", "th") + p.AllowAttrs("colspan", "rowspan").Matching(Integer).OnElements("td", "th") + p.AllowAttrs("headers").Matching( + SpaceSeparatedTokens, + ).OnElements("td", "th") + p.AllowAttrs("height", "width").Matching( + NumberOrPercent, + ).OnElements("td", "th") + p.AllowAttrs( + "scope", + ).Matching( + regexp.MustCompile(`(?i)(?:row|col)(?:group)?`), + ).OnElements("td", "th") + p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("td", "th") + p.AllowAttrs("nowrap").Matching( + regexp.MustCompile(`(?i)|nowrap`), + ).OnElements("td", "th") + + // "tbody" "tfoot" + p.AllowAttrs("align").Matching(CellAlign).OnElements("tbody", "tfoot") + p.AllowAttrs("valign").Matching( + CellVerticalAlign, + ).OnElements("tbody", "tfoot") +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policies.go b/vendor/github.com/microcosm-cc/bluemonday/policies.go new file mode 100644 index 0000000..570bba8 --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/policies.go @@ -0,0 +1,253 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "regexp" +) + +// StrictPolicy returns an empty policy, which will effectively strip all HTML +// elements and their attributes from a document. +func StrictPolicy() *Policy { + return NewPolicy() +} + +// StripTagsPolicy is DEPRECATED. Use StrictPolicy instead. +func StripTagsPolicy() *Policy { + return StrictPolicy() +} + +// UGCPolicy returns a policy aimed at user generated content that is a result +// of HTML WYSIWYG tools and Markdown conversions. +// +// This is expected to be a fairly rich document where as much markup as +// possible should be retained. Markdown permits raw HTML so we are basically +// providing a policy to sanitise HTML5 documents safely but with the +// least intrusion on the formatting expectations of the user. +func UGCPolicy() *Policy { + + p := NewPolicy() + + /////////////////////// + // Global attributes // + /////////////////////// + + // "class" is not permitted as we are not allowing users to style their own + // content + + p.AllowStandardAttributes() + + ////////////////////////////// + // Global URL format policy // + ////////////////////////////// + + p.AllowStandardURLs() + + //////////////////////////////// + // Declarations and structure // + //////////////////////////////// + + // "xml" "xslt" "DOCTYPE" "html" "head" are not permitted as we are + // expecting user generated content to be a fragment of HTML and not a full + // document. + + ////////////////////////// + // Sectioning root tags // + ////////////////////////// + + // "article" and "aside" are permitted and takes no attributes + p.AllowElements("article", "aside") + + // "body" is not permitted as we are expecting user generated content to be a fragment + // of HTML and not a full document. + + // "details" is permitted, including the "open" attribute which can either + // be blank or the value "open". + p.AllowAttrs( + "open", + ).Matching(regexp.MustCompile(`(?i)^(|open)$`)).OnElements("details") + + // "fieldset" is not permitted as we are not allowing forms to be created. + + // "figure" is permitted and takes no attributes + p.AllowElements("figure") + + // "nav" is not permitted as it is assumed that the site (and not the user) + // has defined navigation elements + + // "section" is permitted and takes no attributes + p.AllowElements("section") + + // "summary" is permitted and takes no attributes + p.AllowElements("summary") + + ////////////////////////// + // Headings and footers // + ////////////////////////// + + // "footer" is not permitted as we expect user content to be a fragment and + // not structural to this extent + + // "h1" through "h6" are permitted and take no attributes + p.AllowElements("h1", "h2", "h3", "h4", "h5", "h6") + + // "header" is not permitted as we expect user content to be a fragment and + // not structural to this extent + + // "hgroup" is permitted and takes no attributes + p.AllowElements("hgroup") + + ///////////////////////////////////// + // Content grouping and separating // + ///////////////////////////////////// + + // "blockquote" is permitted, including the "cite" attribute which must be + // a standard URL. + p.AllowAttrs("cite").OnElements("blockquote") + + // "br" "div" "hr" "p" "span" "wbr" are permitted and take no attributes + p.AllowElements("br", "div", "hr", "p", "span", "wbr") + + /////////// + // Links // + /////////// + + // "a" is permitted + p.AllowAttrs("href").OnElements("a") + + // "area" is permitted along with the attributes that map image maps work + p.AllowAttrs("name").Matching( + regexp.MustCompile(`^([\p{L}\p{N}_-]+)$`), + ).OnElements("map") + p.AllowAttrs("alt").Matching(Paragraph).OnElements("area") + p.AllowAttrs("coords").Matching( + regexp.MustCompile(`^([0-9]+,)+[0-9]+$`), + ).OnElements("area") + p.AllowAttrs("href").OnElements("area") + p.AllowAttrs("rel").Matching(SpaceSeparatedTokens).OnElements("area") + p.AllowAttrs("shape").Matching( + regexp.MustCompile(`(?i)^(default|circle|rect|poly)$`), + ).OnElements("area") + p.AllowAttrs("usemap").Matching( + regexp.MustCompile(`(?i)^#[\p{L}\p{N}_-]+$`), + ).OnElements("img") + + // "link" is not permitted + + ///////////////////// + // Phrase elements // + ///////////////////// + + // The following are all inline phrasing elements + p.AllowElements("abbr", "acronym", "cite", "code", "dfn", "em", + "figcaption", "mark", "s", "samp", "strong", "sub", "sup", "var") + + // "q" is permitted and "cite" is a URL and handled by URL policies + p.AllowAttrs("cite").OnElements("q") + + // "time" is permitted + p.AllowAttrs("datetime").Matching(ISO8601).OnElements("time") + + //////////////////// + // Style elements // + //////////////////// + + // block and inline elements that impart no semantic meaning but style the + // document + p.AllowElements("b", "i", "pre", "small", "strike", "tt", "u") + + // "style" is not permitted as we are not yet sanitising CSS and it is an + // XSS attack vector + + ////////////////////// + // HTML5 Formatting // + ////////////////////// + + // "bdi" "bdo" are permitted + p.AllowAttrs("dir").Matching(Direction).OnElements("bdi", "bdo") + + // "rp" "rt" "ruby" are permitted + p.AllowElements("rp", "rt", "ruby") + + /////////////////////////// + // HTML5 Change tracking // + /////////////////////////// + + // "del" "ins" are permitted + p.AllowAttrs("cite").Matching(Paragraph).OnElements("del", "ins") + p.AllowAttrs("datetime").Matching(ISO8601).OnElements("del", "ins") + + /////////// + // Lists // + /////////// + + p.AllowLists() + + //////////// + // Tables // + //////////// + + p.AllowTables() + + /////////// + // Forms // + /////////// + + // By and large, forms are not permitted. However there are some form + // elements that can be used to present data, and we do permit those + // + // "button" "fieldset" "input" "keygen" "label" "output" "select" "datalist" + // "textarea" "optgroup" "option" are all not permitted + + // "meter" is permitted + p.AllowAttrs( + "value", + "min", + "max", + "low", + "high", + "optimum", + ).Matching(Number).OnElements("meter") + + // "progress" is permitted + p.AllowAttrs("value", "max").Matching(Number).OnElements("progress") + + ////////////////////// + // Embedded content // + ////////////////////// + + // Vast majority not permitted + // "audio" "canvas" "embed" "iframe" "object" "param" "source" "svg" "track" + // "video" are all not permitted + + p.AllowImages() + + return p +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policy.go b/vendor/github.com/microcosm-cc/bluemonday/policy.go new file mode 100644 index 0000000..f61d98f --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/policy.go @@ -0,0 +1,552 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "net/url" + "regexp" + "strings" +) + +// Policy encapsulates the whitelist of HTML elements and attributes that will +// be applied to the sanitised HTML. +// +// You should use bluemonday.NewPolicy() to create a blank policy as the +// unexported fields contain maps that need to be initialized. +type Policy struct { + + // Declares whether the maps have been initialized, used as a cheap check to + // ensure that those using Policy{} directly won't cause nil pointer + // exceptions + initialized bool + + // If true then we add spaces when stripping tags, specifically the closing + // tag is replaced by a space character. + addSpaces bool + + // When true, add rel="nofollow" to HTML anchors + requireNoFollow bool + + // When true, add rel="nofollow" to HTML anchors + // Will add for href="http://foo" + // Will skip for href="/foo" or href="foo" + requireNoFollowFullyQualifiedLinks bool + + // When true add target="_blank" to fully qualified links + // Will add for href="http://foo" + // Will skip for href="/foo" or href="foo" + addTargetBlankToFullyQualifiedLinks bool + + // When true, URLs must be parseable by "net/url" url.Parse() + requireParseableURLs bool + + // When true, u, _ := url.Parse("url"); !u.IsAbs() is permitted + allowRelativeURLs bool + + // When true, allow data attributes. + allowDataAttributes bool + + // map[htmlElementName]map[htmlAttributeName]attrPolicy + elsAndAttrs map[string]map[string]attrPolicy + + // map[htmlAttributeName]attrPolicy + globalAttrs map[string]attrPolicy + + // If urlPolicy is nil, all URLs with matching schema are allowed. + // Otherwise, only the URLs with matching schema and urlPolicy(url) + // returning true are allowed. + allowURLSchemes map[string]urlPolicy + + // If an element has had all attributes removed as a result of a policy + // being applied, then the element would be removed from the output. + // + // However some elements are valid and have strong layout meaning without + // any attributes, i.e. . To prevent those being removed we maintain + // a list of elements that are allowed to have no attributes and that will + // be maintained in the output HTML. + setOfElementsAllowedWithoutAttrs map[string]struct{} + + setOfElementsToSkipContent map[string]struct{} +} + +type attrPolicy struct { + + // optional pattern to match, when not nil the regexp needs to match + // otherwise the attribute is removed + regexp *regexp.Regexp +} + +type attrPolicyBuilder struct { + p *Policy + + attrNames []string + regexp *regexp.Regexp + allowEmpty bool +} + +type urlPolicy func(url *url.URL) (allowUrl bool) + +// init initializes the maps if this has not been done already +func (p *Policy) init() { + if !p.initialized { + p.elsAndAttrs = make(map[string]map[string]attrPolicy) + p.globalAttrs = make(map[string]attrPolicy) + p.allowURLSchemes = make(map[string]urlPolicy) + p.setOfElementsAllowedWithoutAttrs = make(map[string]struct{}) + p.setOfElementsToSkipContent = make(map[string]struct{}) + p.initialized = true + } +} + +// NewPolicy returns a blank policy with nothing whitelisted or permitted. This +// is the recommended way to start building a policy and you should now use +// AllowAttrs() and/or AllowElements() to construct the whitelist of HTML +// elements and attributes. +func NewPolicy() *Policy { + + p := Policy{} + + p.addDefaultElementsWithoutAttrs() + p.addDefaultSkipElementContent() + + return &p +} + +// AllowAttrs takes a range of HTML attribute names and returns an +// attribute policy builder that allows you to specify the pattern and scope of +// the whitelisted attribute. +// +// The attribute policy is only added to the core policy when either Globally() +// or OnElements(...) are called. +func (p *Policy) AllowAttrs(attrNames ...string) *attrPolicyBuilder { + + p.init() + + abp := attrPolicyBuilder{ + p: p, + allowEmpty: false, + } + + for _, attrName := range attrNames { + abp.attrNames = append(abp.attrNames, strings.ToLower(attrName)) + } + + return &abp +} + +// AllowDataAttributes whitelists all data attributes. We can't specify the name +// of each attribute exactly as they are customized. +// +// NOTE: These values are not sanitized and applications that evaluate or process +// them without checking and verification of the input may be at risk if this option +// is enabled. This is a 'caveat emptor' option and the person enabling this option +// needs to fully understand the potential impact with regards to whatever application +// will be consuming the sanitized HTML afterwards, i.e. if you know you put a link in a +// data attribute and use that to automatically load some new window then you're giving +// the author of a HTML fragment the means to open a malicious destination automatically. +// Use with care! +func (p *Policy) AllowDataAttributes() { + p.allowDataAttributes = true +} + +// AllowNoAttrs says that attributes on element are optional. +// +// The attribute policy is only added to the core policy when OnElements(...) +// are called. +func (p *Policy) AllowNoAttrs() *attrPolicyBuilder { + + p.init() + + abp := attrPolicyBuilder{ + p: p, + allowEmpty: true, + } + return &abp +} + +// AllowNoAttrs says that attributes on element are optional. +// +// The attribute policy is only added to the core policy when OnElements(...) +// are called. +func (abp *attrPolicyBuilder) AllowNoAttrs() *attrPolicyBuilder { + + abp.allowEmpty = true + + return abp +} + +// Matching allows a regular expression to be applied to a nascent attribute +// policy, and returns the attribute policy. Calling this more than once will +// replace the existing regexp. +func (abp *attrPolicyBuilder) Matching(regex *regexp.Regexp) *attrPolicyBuilder { + + abp.regexp = regex + + return abp +} + +// OnElements will bind an attribute policy to a given range of HTML elements +// and return the updated policy +func (abp *attrPolicyBuilder) OnElements(elements ...string) *Policy { + + for _, element := range elements { + element = strings.ToLower(element) + + for _, attr := range abp.attrNames { + + if _, ok := abp.p.elsAndAttrs[element]; !ok { + abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + + ap := attrPolicy{} + if abp.regexp != nil { + ap.regexp = abp.regexp + } + + abp.p.elsAndAttrs[element][attr] = ap + } + + if abp.allowEmpty { + abp.p.setOfElementsAllowedWithoutAttrs[element] = struct{}{} + + if _, ok := abp.p.elsAndAttrs[element]; !ok { + abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + } + } + + return abp.p +} + +// Globally will bind an attribute policy to all HTML elements and return the +// updated policy +func (abp *attrPolicyBuilder) Globally() *Policy { + + for _, attr := range abp.attrNames { + if _, ok := abp.p.globalAttrs[attr]; !ok { + abp.p.globalAttrs[attr] = attrPolicy{} + } + + ap := attrPolicy{} + if abp.regexp != nil { + ap.regexp = abp.regexp + } + + abp.p.globalAttrs[attr] = ap + } + + return abp.p +} + +// AllowElements will append HTML elements to the whitelist without applying an +// attribute policy to those elements (the elements are permitted +// sans-attributes) +func (p *Policy) AllowElements(names ...string) *Policy { + p.init() + + for _, element := range names { + element = strings.ToLower(element) + + if _, ok := p.elsAndAttrs[element]; !ok { + p.elsAndAttrs[element] = make(map[string]attrPolicy) + } + } + + return p +} + +// RequireNoFollowOnLinks will result in all tags having a rel="nofollow" +// added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy { + + p.requireNoFollow = require + p.requireParseableURLs = true + + return p +} + +// RequireNoFollowOnFullyQualifiedLinks will result in all tags that point +// to a non-local destination (i.e. starts with a protocol and has a host) +// having a rel="nofollow" added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy { + + p.requireNoFollowFullyQualifiedLinks = require + p.requireParseableURLs = true + + return p +} + +// AddTargetBlankToFullyQualifiedLinks will result in all tags that point +// to a non-local destination (i.e. starts with a protocol and has a host) +// having a target="_blank" added to them if one does not already exist +// +// Note: This requires p.RequireParseableURLs(true) and will enable it. +func (p *Policy) AddTargetBlankToFullyQualifiedLinks(require bool) *Policy { + + p.addTargetBlankToFullyQualifiedLinks = require + p.requireParseableURLs = true + + return p +} + +// RequireParseableURLs will result in all URLs requiring that they be parseable +// by "net/url" url.Parse() +// This applies to: +// - a.href +// - area.href +// - blockquote.cite +// - img.src +// - link.href +// - script.src +func (p *Policy) RequireParseableURLs(require bool) *Policy { + + p.requireParseableURLs = require + + return p +} + +// AllowRelativeURLs enables RequireParseableURLs and then permits URLs that +// are parseable, have no schema information and url.IsAbs() returns false +// This permits local URLs +func (p *Policy) AllowRelativeURLs(require bool) *Policy { + + p.RequireParseableURLs(true) + p.allowRelativeURLs = require + + return p +} + +// AllowURLSchemes will append URL schemes to the whitelist +// Example: p.AllowURLSchemes("mailto", "http", "https") +func (p *Policy) AllowURLSchemes(schemes ...string) *Policy { + p.init() + + p.RequireParseableURLs(true) + + for _, scheme := range schemes { + scheme = strings.ToLower(scheme) + + // Allow all URLs with matching scheme. + p.allowURLSchemes[scheme] = nil + } + + return p +} + +// AllowURLSchemeWithCustomPolicy will append URL schemes with +// a custom URL policy to the whitelist. +// Only the URLs with matching schema and urlPolicy(url) +// returning true will be allowed. +func (p *Policy) AllowURLSchemeWithCustomPolicy( + scheme string, + urlPolicy func(url *url.URL) (allowUrl bool), +) *Policy { + + p.init() + + p.RequireParseableURLs(true) + + scheme = strings.ToLower(scheme) + + p.allowURLSchemes[scheme] = urlPolicy + + return p +} + +// AddSpaceWhenStrippingTag states whether to add a single space " " when +// removing tags that are not whitelisted by the policy. +// +// This is useful if you expect to strip tags in dense markup and may lose the +// value of whitespace. +// +// For example: "

Hello

World

"" would be sanitized to "HelloWorld" +// with the default value of false, but you may wish to sanitize this to +// " Hello World " by setting AddSpaceWhenStrippingTag to true as this would +// retain the intent of the text. +func (p *Policy) AddSpaceWhenStrippingTag(allow bool) *Policy { + + p.addSpaces = allow + + return p +} + +// SkipElementsContent adds the HTML elements whose tags is needed to be removed +// with its content. +func (p *Policy) SkipElementsContent(names ...string) *Policy { + + p.init() + + for _, element := range names { + element = strings.ToLower(element) + + if _, ok := p.setOfElementsToSkipContent[element]; !ok { + p.setOfElementsToSkipContent[element] = struct{}{} + } + } + + return p +} + +// AllowElementsContent marks the HTML elements whose content should be +// retained after removing the tag. +func (p *Policy) AllowElementsContent(names ...string) *Policy { + + p.init() + + for _, element := range names { + delete(p.setOfElementsToSkipContent, strings.ToLower(element)) + } + + return p +} + +// addDefaultElementsWithoutAttrs adds the HTML elements that we know are valid +// without any attributes to an internal map. +// i.e. we know that
is valid, but isn't valid as the "dir" attr +// is mandatory +func (p *Policy) addDefaultElementsWithoutAttrs() { + p.init() + + p.setOfElementsAllowedWithoutAttrs["abbr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["acronym"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["address"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["article"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["aside"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["audio"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["b"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["bdi"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["blockquote"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["body"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["br"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["button"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["canvas"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["caption"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["center"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["cite"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["code"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["col"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["colgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["datalist"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dd"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["del"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["details"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dfn"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["div"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dl"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["dt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["em"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["fieldset"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["figcaption"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["figure"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["footer"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h1"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h2"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h3"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h4"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h5"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["h6"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["head"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["header"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["hgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["hr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["html"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["i"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ins"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["kbd"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["li"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["mark"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["marquee"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["nav"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ol"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["optgroup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["option"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["p"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["pre"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["q"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["rp"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["rt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ruby"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["s"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["samp"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["script"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["section"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["select"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["small"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["span"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["strike"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["strong"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["style"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["sub"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["summary"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["sup"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["svg"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["table"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tbody"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["td"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["textarea"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tfoot"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["th"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["thead"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["title"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["time"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tr"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["tt"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["u"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["ul"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["var"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["video"] = struct{}{} + p.setOfElementsAllowedWithoutAttrs["wbr"] = struct{}{} + +} + +// addDefaultSkipElementContent adds the HTML elements that we should skip +// rendering the character content of, if the element itself is not allowed. +// This is all character data that the end user would not normally see. +// i.e. if we exclude a tag. +func (p *Policy) addDefaultSkipElementContent() { + p.init() + + p.setOfElementsToSkipContent["frame"] = struct{}{} + p.setOfElementsToSkipContent["frameset"] = struct{}{} + p.setOfElementsToSkipContent["iframe"] = struct{}{} + p.setOfElementsToSkipContent["noembed"] = struct{}{} + p.setOfElementsToSkipContent["noframes"] = struct{}{} + p.setOfElementsToSkipContent["noscript"] = struct{}{} + p.setOfElementsToSkipContent["nostyle"] = struct{}{} + p.setOfElementsToSkipContent["object"] = struct{}{} + p.setOfElementsToSkipContent["script"] = struct{}{} + p.setOfElementsToSkipContent["style"] = struct{}{} + p.setOfElementsToSkipContent["title"] = struct{}{} +} diff --git a/vendor/github.com/microcosm-cc/bluemonday/sanitize.go b/vendor/github.com/microcosm-cc/bluemonday/sanitize.go new file mode 100644 index 0000000..65ed89b --- /dev/null +++ b/vendor/github.com/microcosm-cc/bluemonday/sanitize.go @@ -0,0 +1,581 @@ +// Copyright (c) 2014, David Kitchen +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the organisation (Microcosm) nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package bluemonday + +import ( + "bytes" + "io" + "net/url" + "regexp" + "strings" + + "golang.org/x/net/html" +) + +var ( + dataAttribute = regexp.MustCompile("^data-.+") + dataAttributeXMLPrefix = regexp.MustCompile("^xml.+") + dataAttributeInvalidChars = regexp.MustCompile("[A-Z;]+") +) + +// Sanitize takes a string that contains a HTML fragment or document and applies +// the given policy whitelist. +// +// It returns a HTML string that has been sanitized by the policy or an empty +// string if an error has occurred (most likely as a consequence of extremely +// malformed input) +func (p *Policy) Sanitize(s string) string { + if strings.TrimSpace(s) == "" { + return s + } + + return p.sanitize(strings.NewReader(s)).String() +} + +// SanitizeBytes takes a []byte that contains a HTML fragment or document and applies +// the given policy whitelist. +// +// It returns a []byte containing the HTML that has been sanitized by the policy +// or an empty []byte if an error has occurred (most likely as a consequence of +// extremely malformed input) +func (p *Policy) SanitizeBytes(b []byte) []byte { + if len(bytes.TrimSpace(b)) == 0 { + return b + } + + return p.sanitize(bytes.NewReader(b)).Bytes() +} + +// SanitizeReader takes an io.Reader that contains a HTML fragment or document +// and applies the given policy whitelist. +// +// It returns a bytes.Buffer containing the HTML that has been sanitized by the +// policy. Errors during sanitization will merely return an empty result. +func (p *Policy) SanitizeReader(r io.Reader) *bytes.Buffer { + return p.sanitize(r) +} + +// Performs the actual sanitization process. +func (p *Policy) sanitize(r io.Reader) *bytes.Buffer { + + // It is possible that the developer has created the policy via: + // p := bluemonday.Policy{} + // rather than: + // p := bluemonday.NewPolicy() + // If this is the case, and if they haven't yet triggered an action that + // would initiliaze the maps, then we need to do that. + p.init() + + var ( + buff bytes.Buffer + skipElementContent bool + skippingElementsCount int64 + skipClosingTag bool + closingTagToSkipStack []string + mostRecentlyStartedToken string + ) + + tokenizer := html.NewTokenizer(r) + for { + if tokenizer.Next() == html.ErrorToken { + err := tokenizer.Err() + if err == io.EOF { + // End of input means end of processing + return &buff + } + + // Raw tokenizer error + return &bytes.Buffer{} + } + + token := tokenizer.Token() + switch token.Type { + case html.DoctypeToken: + + // DocType is not handled as there is no safe parsing mechanism + // provided by golang.org/x/net/html for the content, and this can + // be misused to insert HTML tags that are not then sanitized + // + // One might wish to recursively sanitize here using the same policy + // but I will need to do some further testing before considering + // this. + + case html.CommentToken: + + // Comments are ignored by default + + case html.StartTagToken: + + mostRecentlyStartedToken = token.Data + + aps, ok := p.elsAndAttrs[token.Data] + if !ok { + if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { + skipElementContent = true + skippingElementsCount++ + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if len(token.Attr) != 0 { + token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) + } + + if len(token.Attr) == 0 { + if !p.allowNoAttrs(token.Data) { + skipClosingTag = true + closingTagToSkipStack = append(closingTagToSkipStack, token.Data) + if p.addSpaces { + buff.WriteString(" ") + } + break + } + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.EndTagToken: + + if mostRecentlyStartedToken == token.Data { + mostRecentlyStartedToken = "" + } + + if skipClosingTag && closingTagToSkipStack[len(closingTagToSkipStack)-1] == token.Data { + closingTagToSkipStack = closingTagToSkipStack[:len(closingTagToSkipStack)-1] + if len(closingTagToSkipStack) == 0 { + skipClosingTag = false + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if _, ok := p.elsAndAttrs[token.Data]; !ok { + if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { + skippingElementsCount-- + if skippingElementsCount == 0 { + skipElementContent = false + } + } + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.SelfClosingTagToken: + + aps, ok := p.elsAndAttrs[token.Data] + if !ok { + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if len(token.Attr) != 0 { + token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) + } + + if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) { + if p.addSpaces { + buff.WriteString(" ") + } + break + } + + if !skipElementContent { + buff.WriteString(token.String()) + } + + case html.TextToken: + + if !skipElementContent { + switch mostRecentlyStartedToken { + case "script": + // not encouraged, but if a policy allows JavaScript we + // should not HTML escape it as that would break the output + buff.WriteString(token.Data) + case "style": + // not encouraged, but if a policy allows CSS styles we + // should not HTML escape it as that would break the output + buff.WriteString(token.Data) + default: + // HTML escape the text + buff.WriteString(token.String()) + } + } + default: + // A token that didn't exist in the html package when we wrote this + return &bytes.Buffer{} + } + } +} + +// sanitizeAttrs takes a set of element attribute policies and the global +// attribute policies and applies them to the []html.Attribute returning a set +// of html.Attributes that match the policies +func (p *Policy) sanitizeAttrs( + elementName string, + attrs []html.Attribute, + aps map[string]attrPolicy, +) []html.Attribute { + + if len(attrs) == 0 { + return attrs + } + + // Builds a new attribute slice based on the whether the attribute has been + // whitelisted explicitly or globally. + cleanAttrs := []html.Attribute{} + for _, htmlAttr := range attrs { + if p.allowDataAttributes { + // If we see a data attribute, let it through. + if isDataAttribute(htmlAttr.Key) { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } + // Is there an element specific attribute policy that applies? + if ap, ok := aps[htmlAttr.Key]; ok { + if ap.regexp != nil { + if ap.regexp.MatchString(htmlAttr.Val) { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } else { + cleanAttrs = append(cleanAttrs, htmlAttr) + continue + } + } + + // Is there a global attribute policy that applies? + if ap, ok := p.globalAttrs[htmlAttr.Key]; ok { + + if ap.regexp != nil { + if ap.regexp.MatchString(htmlAttr.Val) { + cleanAttrs = append(cleanAttrs, htmlAttr) + } + } else { + cleanAttrs = append(cleanAttrs, htmlAttr) + } + } + } + + if len(cleanAttrs) == 0 { + // If nothing was allowed, let's get out of here + return cleanAttrs + } + // cleanAttrs now contains the attributes that are permitted + + if linkable(elementName) { + if p.requireParseableURLs { + // Ensure URLs are parseable: + // - a.href + // - area.href + // - link.href + // - blockquote.cite + // - q.cite + // - img.src + // - script.src + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + switch elementName { + case "a", "area", "link": + if htmlAttr.Key == "href" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + case "blockquote", "q": + if htmlAttr.Key == "cite" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + case "img", "script": + if htmlAttr.Key == "src" { + if u, ok := p.validURL(htmlAttr.Val); ok { + htmlAttr.Val = u + tmpAttrs = append(tmpAttrs, htmlAttr) + } + break + } + tmpAttrs = append(tmpAttrs, htmlAttr) + default: + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + cleanAttrs = tmpAttrs + } + + if (p.requireNoFollow || + p.requireNoFollowFullyQualifiedLinks || + p.addTargetBlankToFullyQualifiedLinks) && + len(cleanAttrs) > 0 { + + // Add rel="nofollow" if a "href" exists + switch elementName { + case "a", "area", "link": + var hrefFound bool + var externalLink bool + for _, htmlAttr := range cleanAttrs { + if htmlAttr.Key == "href" { + hrefFound = true + + u, err := url.Parse(htmlAttr.Val) + if err != nil { + continue + } + if u.Host != "" { + externalLink = true + } + + continue + } + } + + if hrefFound { + var ( + noFollowFound bool + targetBlankFound bool + ) + + addNoFollow := (p.requireNoFollow || + externalLink && p.requireNoFollowFullyQualifiedLinks) + + addTargetBlank := (externalLink && + p.addTargetBlankToFullyQualifiedLinks) + + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + + var appended bool + if htmlAttr.Key == "rel" && addNoFollow { + + if strings.Contains(htmlAttr.Val, "nofollow") { + noFollowFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } else { + htmlAttr.Val += " nofollow" + noFollowFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } + } + + if elementName == "a" && htmlAttr.Key == "target" { + if htmlAttr.Val == "_blank" { + targetBlankFound = true + } + if addTargetBlank && !targetBlankFound { + htmlAttr.Val = "_blank" + targetBlankFound = true + tmpAttrs = append(tmpAttrs, htmlAttr) + appended = true + } + } + + if !appended { + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + if noFollowFound || targetBlankFound { + cleanAttrs = tmpAttrs + } + + if addNoFollow && !noFollowFound { + rel := html.Attribute{} + rel.Key = "rel" + rel.Val = "nofollow" + cleanAttrs = append(cleanAttrs, rel) + } + + if elementName == "a" && addTargetBlank && !targetBlankFound { + rel := html.Attribute{} + rel.Key = "target" + rel.Val = "_blank" + targetBlankFound = true + cleanAttrs = append(cleanAttrs, rel) + } + + if targetBlankFound { + // target="_blank" has a security risk that allows the + // opened window/tab to issue JavaScript calls against + // window.opener, which in effect allow the destination + // of the link to control the source: + // https://dev.to/ben/the-targetblank-vulnerability-by-example + // + // To mitigate this risk, we need to add a specific rel + // attribute if it is not already present. + // rel="noopener" + // + // Unfortunately this is processing the rel twice (we + // already looked at it earlier ^^) as we cannot be sure + // of the ordering of the href and rel, and whether we + // have fully satisfied that we need to do this. This + // double processing only happens *if* target="_blank" + // is true. + var noOpenerAdded bool + tmpAttrs := []html.Attribute{} + for _, htmlAttr := range cleanAttrs { + var appended bool + if htmlAttr.Key == "rel" { + if strings.Contains(htmlAttr.Val, "noopener") { + noOpenerAdded = true + tmpAttrs = append(tmpAttrs, htmlAttr) + } else { + htmlAttr.Val += " noopener" + noOpenerAdded = true + tmpAttrs = append(tmpAttrs, htmlAttr) + } + + appended = true + } + if !appended { + tmpAttrs = append(tmpAttrs, htmlAttr) + } + } + if noOpenerAdded { + cleanAttrs = tmpAttrs + } else { + // rel attr was not found, or else noopener would + // have been added already + rel := html.Attribute{} + rel.Key = "rel" + rel.Val = "noopener" + cleanAttrs = append(cleanAttrs, rel) + } + + } + } + default: + } + } + } + + return cleanAttrs +} + +func (p *Policy) allowNoAttrs(elementName string) bool { + _, ok := p.setOfElementsAllowedWithoutAttrs[elementName] + return ok +} + +func (p *Policy) validURL(rawurl string) (string, bool) { + if p.requireParseableURLs { + // URLs are valid if when space is trimmed the URL is valid + rawurl = strings.TrimSpace(rawurl) + + // URLs cannot contain whitespace, unless it is a data-uri + if (strings.Contains(rawurl, " ") || + strings.Contains(rawurl, "\t") || + strings.Contains(rawurl, "\n")) && + !strings.HasPrefix(rawurl, `data:`) { + return "", false + } + + // URLs are valid if they parse + u, err := url.Parse(rawurl) + if err != nil { + return "", false + } + + if u.Scheme != "" { + + urlPolicy, ok := p.allowURLSchemes[u.Scheme] + if !ok { + return "", false + + } + + if urlPolicy == nil || urlPolicy(u) == true { + return u.String(), true + } + + return "", false + } + + if p.allowRelativeURLs { + if u.String() != "" { + return u.String(), true + } + } + + return "", false + } + + return rawurl, true +} + +func linkable(elementName string) bool { + switch elementName { + case "a", "area", "blockquote", "img", "link", "script": + return true + default: + return false + } +} + +func isDataAttribute(val string) bool { + if !dataAttribute.MatchString(val) { + return false + } + rest := strings.Split(val, "data-") + if len(rest) == 1 { + return false + } + // data-xml* is invalid. + if dataAttributeXMLPrefix.MatchString(rest[1]) { + return false + } + // no uppercase or semi-colons allowed. + if dataAttributeInvalidChars.MatchString(rest[1]) { + return false + } + return true +} diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 0000000..f9c841a --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md new file mode 100644 index 0000000..d70706d --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/README.md @@ -0,0 +1,14 @@ +# go-homedir + +This is a Go library for detecting the user's home directory without +the use of cgo, so the library can be used in cross-compilation environments. + +Usage is incredibly simple, just call `homedir.Dir()` to get the home directory +for a user, and `homedir.Expand()` to expand the `~` in a path to the home +directory. + +**Why not just use `os/user`?** The built-in `os/user` package requires +cgo on Darwin systems. This means that any Go code that uses that package +cannot cross compile. But 99% of the time the use for `os/user` is just to +retrieve the home directory, which we can do for the current user without +cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/go.mod b/vendor/github.com/mitchellh/go-homedir/go.mod new file mode 100644 index 0000000..7efa09a --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/go.mod @@ -0,0 +1 @@ +module github.com/mitchellh/go-homedir diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go new file mode 100644 index 0000000..fb87bef --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir.go @@ -0,0 +1,157 @@ +package homedir + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" +) + +// DisableCache will disable caching of the home directory. Caching is enabled +// by default. +var DisableCache bool + +var homedirCache string +var cacheLock sync.RWMutex + +// Dir returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Dir() (string, error) { + if !DisableCache { + cacheLock.RLock() + cached := homedirCache + cacheLock.RUnlock() + if cached != "" { + return cached, nil + } + } + + cacheLock.Lock() + defer cacheLock.Unlock() + + var result string + var err error + if runtime.GOOS == "windows" { + result, err = dirWindows() + } else { + // Unix-like system, so just assume Unix + result, err = dirUnix() + } + + if err != nil { + return "", err + } + homedirCache = result + return result, nil +} + +// Expand expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~`, the path is +// returned as-is. +func Expand(path string) (string, error) { + if len(path) == 0 { + return path, nil + } + + if path[0] != '~' { + return path, nil + } + + if len(path) > 1 && path[1] != '/' && path[1] != '\\' { + return "", errors.New("cannot expand user-specific home dir") + } + + dir, err := Dir() + if err != nil { + return "", err + } + + return filepath.Join(dir, path[1:]), nil +} + +func dirUnix() (string, error) { + homeEnv := "HOME" + if runtime.GOOS == "plan9" { + // On plan9, env vars are lowercase. + homeEnv = "home" + } + + // First prefer the HOME environmental variable + if home := os.Getenv(homeEnv); home != "" { + return home, nil + } + + var stdout bytes.Buffer + + // If that fails, try OS specific commands + if runtime.GOOS == "darwin" { + cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) + cmd.Stdout = &stdout + if err := cmd.Run(); err == nil { + result := strings.TrimSpace(stdout.String()) + if result != "" { + return result, nil + } + } + } else { + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if err != exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd := exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func dirWindows() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + // Prefer standard environment variable USERPROFILE + if home := os.Getenv("USERPROFILE"); home != "" { + return home, nil + } + + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") + } + + return home, nil +} diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml new file mode 100644 index 0000000..93b1fcd --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml @@ -0,0 +1,16 @@ +sudo: false +language: go +go: + - 1.x + - master +matrix: + allow_failures: + - go: master + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE new file mode 100644 index 0000000..c35c17a --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 Dmitri Shuralyov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md new file mode 100644 index 0000000..670bf0f --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md @@ -0,0 +1,36 @@ +sanitized_anchor_name +===================== + +[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) + +Package sanitized_anchor_name provides a func to create sanitized anchor names. + +Its logic can be reused by multiple packages to create interoperable anchor names +and links to those anchors. + +At this time, it does not try to ensure that generated anchor names +are unique, that responsibility falls on the caller. + +Installation +------------ + +```bash +go get -u github.com/shurcooL/sanitized_anchor_name +``` + +Example +------- + +```Go +anchorName := sanitized_anchor_name.Create("This is a header") + +fmt.Println(anchorName) + +// Output: +// this-is-a-header +``` + +License +------- + +- [MIT License](LICENSE) diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go new file mode 100644 index 0000000..6a77d12 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go @@ -0,0 +1,29 @@ +// Package sanitized_anchor_name provides a func to create sanitized anchor names. +// +// Its logic can be reused by multiple packages to create interoperable anchor names +// and links to those anchors. +// +// At this time, it does not try to ensure that generated anchor names +// are unique, that responsibility falls on the caller. +package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" + +import "unicode" + +// Create returns a sanitized anchor name for the given text. +func Create(text string) string { + var anchorName []rune + var futureDash = false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } + } + return string(anchorName) +} diff --git a/vendor/github.com/writeas/go-writeas/v2/.gitignore b/vendor/github.com/writeas/go-writeas/v2/.gitignore new file mode 100644 index 0000000..87ae607 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/.gitignore @@ -0,0 +1,3 @@ +*~ +*.swp +writeas diff --git a/vendor/github.com/writeas/go-writeas/v2/LICENSE b/vendor/github.com/writeas/go-writeas/v2/LICENSE new file mode 100644 index 0000000..134a5b8 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Write.as + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/writeas/go-writeas/v2/README.md b/vendor/github.com/writeas/go-writeas/v2/README.md new file mode 100644 index 0000000..5288001 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/README.md @@ -0,0 +1,71 @@ +# go-writeas + +[![godoc](https://godoc.org/go.code.as/writeas.v2?status.svg)](https://godoc.org/go.code.as/writeas.v2) + +Official Write.as Go client library. + +## Installation + +**Warning**: the `v2` branch is under heavy development and its API will change without notice. + +For a stable API, use `go.code.as/writeas.v1` and upgrade to `v2` once everything is merged into `master`. + +```bash +go get go.code.as/writeas.v2 +``` + +## Documentation + +See all functionality and usages in the [API documentation](https://developer.write.as/docs/api/). + +### Example usage + +```go +import "go.code.as/writeas.v2" + +func main() { + // Create the client + c := writeas.NewClient() + + // Publish a post + p, err := c.CreatePost(&writeas.PostParams{ + Title: "Title!", + Content: "This is a post.", + Font: "sans", + }) + if err != nil { + // Perhaps show err.Error() + } + + // Save token for later, since it won't ever be returned again + token := p.Token + + // Update a published post + p, err = c.UpdatePost(p.ID, token, &writeas.PostParams{ + Content: "Now it's been updated!", + }) + if err != nil { + // handle + } + + // Get a published post + p, err = c.GetPost(p.ID) + if err != nil { + // handle + } + + // Delete a post + err = c.DeletePost(p.ID, token) +} +``` + +## Contributing + +The library covers our usage, but might not be comprehensive of the API. So we always welcome contributions and improvements from the community. Before sending pull requests, make sure you've done the following: + +* Run `goimports` on all updated .go files. +* Document all exported structs and funcs. + +## License + +MIT diff --git a/vendor/github.com/writeas/go-writeas/v2/auth.go b/vendor/github.com/writeas/go-writeas/v2/auth.go new file mode 100644 index 0000000..3cf4249 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/auth.go @@ -0,0 +1,75 @@ +package writeas + +import ( + "fmt" + "net/http" +) + +// LogIn authenticates a user with Write.as. +// See https://developer.write.as/docs/api/#authenticate-a-user +func (c *Client) LogIn(username, pass string) (*AuthUser, error) { + u := &AuthUser{} + up := struct { + Alias string `json:"alias"` + Pass string `json:"pass"` + }{ + Alias: username, + Pass: pass, + } + + env, err := c.post("/auth/login", up, u) + if err != nil { + return nil, err + } + + var ok bool + if u, ok = env.Data.(*AuthUser); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + status := env.Code + if status != http.StatusOK { + if status == http.StatusBadRequest { + return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) + } else if status == http.StatusUnauthorized { + return nil, fmt.Errorf("Incorrect password.") + } else if status == http.StatusNotFound { + return nil, fmt.Errorf("User does not exist.") + } else if status == http.StatusTooManyRequests { + return nil, fmt.Errorf("Too many log in attempts in a short period of time.") + } + return nil, fmt.Errorf("Problem authenticating: %d. %v\n", status, err) + } + + c.SetToken(u.AccessToken) + return u, nil +} + +// LogOut logs the current user out, making the Client's current access token +// invalid. +func (c *Client) LogOut() error { + env, err := c.delete("/auth/me", nil) + if err != nil { + return err + } + + status := env.Code + if status != http.StatusNoContent { + if status == http.StatusNotFound { + return fmt.Errorf("Access token is invalid or doesn't exist") + } + return fmt.Errorf("Unable to log out: %v", env.ErrorMessage) + } + + // Logout successful, so update the Client + c.token = "" + + return nil +} + +func (c *Client) isNotLoggedIn(code int) bool { + if c.token == "" { + return false + } + return code == http.StatusUnauthorized +} diff --git a/vendor/github.com/writeas/go-writeas/v2/collection.go b/vendor/github.com/writeas/go-writeas/v2/collection.go new file mode 100644 index 0000000..9b4a925 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/collection.go @@ -0,0 +1,186 @@ +package writeas + +import ( + "fmt" + "net/http" +) + +type ( + // Collection represents a collection of posts. Blogs are a type of collection + // on Write.as. + Collection struct { + Alias string `json:"alias"` + Title string `json:"title"` + Description string `json:"description"` + StyleSheet string `json:"style_sheet"` + Private bool `json:"private"` + Views int64 `json:"views"` + Domain string `json:"domain,omitempty"` + Email string `json:"email,omitempty"` + URL string `json:"url,omitempty"` + + TotalPosts int `json:"total_posts"` + + Posts *[]Post `json:"posts,omitempty"` + } + + // CollectionParams holds values for creating a collection. + CollectionParams struct { + Alias string `json:"alias"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + } +) + +// CreateCollection creates a new collection, returning a user-friendly error +// if one comes up. Requires a Write.as subscription. See +// https://developer.write.as/docs/api/#create-a-collection +func (c *Client) CreateCollection(sp *CollectionParams) (*Collection, error) { + p := &Collection{} + env, err := c.post("/collections", sp, p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*Collection); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + status := env.Code + if status != http.StatusCreated { + if status == http.StatusBadRequest { + return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) + } else if status == http.StatusForbidden { + return nil, fmt.Errorf("Casual or Pro user required.") + } else if status == http.StatusConflict { + return nil, fmt.Errorf("Collection name is already taken.") + } else if status == http.StatusPreconditionFailed { + return nil, fmt.Errorf("Reached max collection quota.") + } + return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err) + } + return p, nil +} + +// GetCollection retrieves a collection, returning the Collection and any error +// (in user-friendly form) that occurs. See +// https://developer.write.as/docs/api/#retrieve-a-collection +func (c *Client) GetCollection(alias string) (*Collection, error) { + coll := &Collection{} + env, err := c.get(fmt.Sprintf("/collections/%s", alias), coll) + if err != nil { + return nil, err + } + + var ok bool + if coll, ok = env.Data.(*Collection); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + status := env.Code + + if status == http.StatusOK { + return coll, nil + } else if status == http.StatusNotFound { + return nil, fmt.Errorf("Collection not found.") + } else { + return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) + } +} + +// GetCollectionPosts retrieves a collection's posts, returning the Posts +// and any error (in user-friendly form) that occurs. See +// https://developer.write.as/docs/api/#retrieve-collection-posts +func (c *Client) GetCollectionPosts(alias string) (*[]Post, error) { + coll := &Collection{} + env, err := c.get(fmt.Sprintf("/collections/%s/posts", alias), coll) + if err != nil { + return nil, err + } + + var ok bool + if coll, ok = env.Data.(*Collection); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + status := env.Code + + if status == http.StatusOK { + return coll.Posts, nil + } else if status == http.StatusNotFound { + return nil, fmt.Errorf("Collection not found.") + } else { + return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) + } +} + +// GetCollectionPost retrieves a post from a collection +// and any error (in user-friendly form) that occurs). See +// https://developers.write.as/docs/api/#retrieve-a-collection-post +func (c *Client) GetCollectionPost(alias, slug string) (*Post, error) { + post := Post{} + + env, err := c.get(fmt.Sprintf("/collections/%s/posts/%s", alias, slug), &post) + if err != nil { + return nil, err + } + + if _, ok := env.Data.(*Post); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + if env.Code == http.StatusOK { + return &post, nil + } else if env.Code == http.StatusNotFound { + return nil, fmt.Errorf("Post %s not found in collection %s", slug, alias) + } + + return nil, fmt.Errorf("Problem getting post %s from collection %s: %d. %v\n", slug, alias, env.Code, err) +} + +// GetUserCollections retrieves the authenticated user's collections. +// See https://developers.write.as/docs/api/#retrieve-user-39-s-collections +func (c *Client) GetUserCollections() (*[]Collection, error) { + colls := &[]Collection{} + env, err := c.get("/me/collections", colls) + if err != nil { + return nil, err + } + + var ok bool + if colls, ok = env.Data.(*[]Collection); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + status := env.Code + + if status != http.StatusOK { + if c.isNotLoggedIn(status) { + return nil, fmt.Errorf("Not authenticated.") + } + return nil, fmt.Errorf("Problem getting collections: %d. %v\n", status, err) + } + return colls, nil +} + +// DeleteCollection permanently deletes a collection and makes any posts on it +// anonymous. +// +// See https://developers.write.as/docs/api/#delete-a-collection. +func (c *Client) DeleteCollection(alias string) error { + endpoint := "/collections/" + alias + env, err := c.delete(endpoint, nil /* data */) + if err != nil { + return err + } + + status := env.Code + switch status { + case http.StatusNoContent: + return nil + case http.StatusUnauthorized: + return fmt.Errorf("Not authenticated.") + case http.StatusBadRequest: + return fmt.Errorf("Bad request: %s", env.ErrorMessage) + default: + return fmt.Errorf("Problem deleting collection: %d. %s\n", status, env.ErrorMessage) + } +} diff --git a/vendor/github.com/writeas/go-writeas/v2/go.mod b/vendor/github.com/writeas/go-writeas/v2/go.mod new file mode 100644 index 0000000..b88b28a --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/go.mod @@ -0,0 +1,8 @@ +module github.com/writeas/go-writeas/v2 + +go 1.9 + +require ( + code.as/core/socks v1.0.0 + github.com/writeas/impart v1.1.0 +) diff --git a/vendor/github.com/writeas/go-writeas/v2/go.sum b/vendor/github.com/writeas/go-writeas/v2/go.sum new file mode 100644 index 0000000..3e036d3 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/go.sum @@ -0,0 +1,4 @@ +code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= +code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= +github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= +github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= diff --git a/vendor/github.com/writeas/go-writeas/v2/post.go b/vendor/github.com/writeas/go-writeas/v2/post.go new file mode 100644 index 0000000..1f8a55b --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/post.go @@ -0,0 +1,330 @@ +package writeas + +import ( + "fmt" + "net/http" + "time" +) + +type ( + // Post represents a published Write.as post, whether anonymous, owned by a + // user, or part of a collection. + Post struct { + ID string `json:"id"` + Slug string `json:"slug"` + Token string `json:"token"` + Font string `json:"appearance"` + Language *string `json:"language"` + RTL *bool `json:"rtl"` + Listed bool `json:"listed"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Title string `json:"title"` + Content string `json:"body"` + Views int64 `json:"views"` + Tags []string `json:"tags"` + Images []string `json:"images"` + OwnerName string `json:"owner,omitempty"` + + Collection *Collection `json:"collection,omitempty"` + } + + // OwnedPostParams are, together, fields only the original post author knows. + OwnedPostParams struct { + ID string `json:"id"` + Token string `json:"token,omitempty"` + } + + // PostParams holds values for creating or updating a post. + PostParams struct { + // Parameters only for updating + ID string `json:"-"` + Token string `json:"token,omitempty"` + + // Parameters for creating or updating + Slug string `json:"slug"` + Created *time.Time `json:"created,omitempty"` + Updated *time.Time `json:"updated,omitempty"` + Title string `json:"title,omitempty"` + Content string `json:"body,omitempty"` + Font string `json:"font,omitempty"` + IsRTL *bool `json:"rtl,omitempty"` + Language *string `json:"lang,omitempty"` + + // Parameters only for creating + Crosspost []map[string]string `json:"crosspost,omitempty"` + + // Parameters for collection posts + Collection string `json:"-"` + } + + // PinnedPostParams holds values for pinning a post + PinnedPostParams struct { + ID string `json:"id"` + Position int `json:"position"` + } + + // BatchPostResult contains the post-specific result as part of a larger + // batch operation. + BatchPostResult struct { + ID string `json:"id,omitempty"` + Code int `json:"code,omitempty"` + ErrorMessage string `json:"error_msg,omitempty"` + } + + // ClaimPostResult contains the post-specific result for a request to + // associate a post to an account. + ClaimPostResult struct { + ID string `json:"id,omitempty"` + Code int `json:"code,omitempty"` + ErrorMessage string `json:"error_msg,omitempty"` + Post *Post `json:"post,omitempty"` + } +) + +// GetPost retrieves a published post, returning the Post and any error (in +// user-friendly form) that occurs. See +// https://developer.write.as/docs/api/#retrieve-a-post. +func (c *Client) GetPost(id string) (*Post, error) { + p := &Post{} + env, err := c.get(fmt.Sprintf("/posts/%s", id), p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*Post); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + status := env.Code + + if status == http.StatusOK { + return p, nil + } else if status == http.StatusNotFound { + return nil, fmt.Errorf("Post not found.") + } else if status == http.StatusGone { + return nil, fmt.Errorf("Post unpublished.") + } + return nil, fmt.Errorf("Problem getting post: %d. %s\n", status, env.ErrorMessage) +} + +// CreatePost publishes a new post, returning a user-friendly error if one comes +// up. See https://developer.write.as/docs/api/#publish-a-post. +func (c *Client) CreatePost(sp *PostParams) (*Post, error) { + p := &Post{} + endPre := "" + if sp.Collection != "" { + endPre = "/collections/" + sp.Collection + } + env, err := c.post(endPre+"/posts", sp, p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*Post); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + status := env.Code + if status != http.StatusCreated { + if status == http.StatusBadRequest { + return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) + } + return nil, fmt.Errorf("Problem creating post: %d. %s\n", status, env.ErrorMessage) + } + return p, nil +} + +// UpdatePost updates a published post with the given PostParams. See +// https://developer.write.as/docs/api/#update-a-post. +func (c *Client) UpdatePost(id, token string, sp *PostParams) (*Post, error) { + return c.updatePost("", id, token, sp) +} + +func (c *Client) updatePost(collection, identifier, token string, sp *PostParams) (*Post, error) { + p := &Post{} + endpoint := "/posts/" + identifier + /* + if collection != "" { + endpoint = "/collections/" + collection + endpoint + } else { + sp.Token = token + } + */ + sp.Token = token + env, err := c.put(endpoint, sp, p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*Post); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + status := env.Code + if status != http.StatusOK { + if c.isNotLoggedIn(status) { + return nil, fmt.Errorf("Not authenticated.") + } else if status == http.StatusBadRequest { + return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) + } + return nil, fmt.Errorf("Problem updating post: %d. %s\n", status, env.ErrorMessage) + } + return p, nil +} + +// DeletePost permanently deletes a published post. See +// https://developer.write.as/docs/api/#delete-a-post. +func (c *Client) DeletePost(id, token string) error { + return c.deletePost("", id, token) +} + +func (c *Client) deletePost(collection, identifier, token string) error { + p := map[string]string{} + endpoint := "/posts/" + identifier + /* + if collection != "" { + endpoint = "/collections/" + collection + endpoint + } else { + p["token"] = token + } + */ + p["token"] = token + env, err := c.delete(endpoint, p) + if err != nil { + return err + } + + status := env.Code + if status == http.StatusNoContent { + return nil + } else if c.isNotLoggedIn(status) { + return fmt.Errorf("Not authenticated.") + } else if status == http.StatusBadRequest { + return fmt.Errorf("Bad request: %s", env.ErrorMessage) + } + return fmt.Errorf("Problem deleting post: %d. %s\n", status, env.ErrorMessage) +} + +// ClaimPosts associates anonymous posts with a user / account. +// https://developer.write.as/docs/api/#claim-posts. +func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) { + p := &[]ClaimPostResult{} + env, err := c.post("/posts/claim", sp, p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*[]ClaimPostResult); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + + status := env.Code + if status == http.StatusOK { + return p, nil + } else if c.isNotLoggedIn(status) { + return nil, fmt.Errorf("Not authenticated.") + } else if status == http.StatusBadRequest { + return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) + } else { + return nil, fmt.Errorf("Problem claiming post: %d. %s\n", status, env.ErrorMessage) + } + // TODO: does this also happen with moving posts? +} + +// GetUserPosts retrieves the authenticated user's posts. +// See https://developers.write.as/docs/api/#retrieve-user-39-s-posts +func (c *Client) GetUserPosts() (*[]Post, error) { + p := &[]Post{} + env, err := c.get("/me/posts", p) + if err != nil { + return nil, err + } + + var ok bool + if p, ok = env.Data.(*[]Post); !ok { + return nil, fmt.Errorf("Wrong data returned from API.") + } + status := env.Code + + if status != http.StatusOK { + if c.isNotLoggedIn(status) { + return nil, fmt.Errorf("Not authenticated.") + } + return nil, fmt.Errorf("Problem getting user posts: %d. %s\n", status, env.ErrorMessage) + } + return p, nil +} + +// PinPost pins a post in the given collection. +// See https://developers.write.as/docs/api/#pin-a-post-to-a-collection +func (c *Client) PinPost(alias string, pp *PinnedPostParams) error { + res := &[]BatchPostResult{} + env, err := c.post(fmt.Sprintf("/collections/%s/pin", alias), []*PinnedPostParams{pp}, res) + if err != nil { + return err + } + + var ok bool + if res, ok = env.Data.(*[]BatchPostResult); !ok { + return fmt.Errorf("Wrong data returned from API.") + } + + // Check for basic request errors on top level response + status := env.Code + if status != http.StatusOK { + if c.isNotLoggedIn(status) { + return fmt.Errorf("Not authenticated.") + } + return fmt.Errorf("Problem pinning post: %d. %s\n", status, env.ErrorMessage) + } + + // Check the individual post result + if len(*res) == 0 || len(*res) > 1 { + return fmt.Errorf("Wrong data returned from API.") + } + if (*res)[0].Code != http.StatusOK { + return fmt.Errorf("Problem pinning post: %d", (*res)[0].Code) + // TODO: return ErrorMessage (right now it'll be empty) + // return fmt.Errorf("Problem pinning post: %s", res[0].ErrorMessage) + } + return nil +} + +// UnpinPost unpins a post from the given collection. +// See https://developers.write.as/docs/api/#unpin-a-post-from-a-collection +func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error { + res := &[]BatchPostResult{} + env, err := c.post(fmt.Sprintf("/collections/%s/unpin", alias), []*PinnedPostParams{pp}, res) + if err != nil { + return err + } + + var ok bool + if res, ok = env.Data.(*[]BatchPostResult); !ok { + return fmt.Errorf("Wrong data returned from API.") + } + + // Check for basic request errors on top level response + status := env.Code + if status != http.StatusOK { + if c.isNotLoggedIn(status) { + return fmt.Errorf("Not authenticated.") + } + return fmt.Errorf("Problem unpinning post: %d. %s\n", status, env.ErrorMessage) + } + + // Check the individual post result + if len(*res) == 0 || len(*res) > 1 { + return fmt.Errorf("Wrong data returned from API.") + } + if (*res)[0].Code != http.StatusOK { + return fmt.Errorf("Problem unpinning post: %d", (*res)[0].Code) + // TODO: return ErrorMessage (right now it'll be empty) + // return fmt.Errorf("Problem unpinning post: %s", res[0].ErrorMessage) + } + return nil +} diff --git a/vendor/github.com/writeas/go-writeas/v2/user.go b/vendor/github.com/writeas/go-writeas/v2/user.go new file mode 100644 index 0000000..5973d9c --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/user.go @@ -0,0 +1,34 @@ +package writeas + +import "time" + +type ( + // AuthUser represents a just-authenticated user. It contains information + // that'll only be returned once (now) per user session. + AuthUser struct { + AccessToken string `json:"access_token,omitempty"` + Password string `json:"password,omitempty"` + User *User `json:"user"` + } + + // User represents a registered Write.as user. + User struct { + Username string `json:"username"` + Email string `json:"email"` + Created time.Time `json:"created"` + + // Optional properties + Subscription *UserSubscription `json:"subscription"` + } + + // UserSubscription contains information about a user's Write.as + // subscription. + UserSubscription struct { + Name string `json:"name"` + Begin time.Time `json:"begin"` + End time.Time `json:"end"` + AutoRenew bool `json:"auto_renew"` + Active bool `json:"is_active"` + Delinquent bool `json:"is_delinquent"` + } +) diff --git a/vendor/github.com/writeas/go-writeas/v2/writeas.go b/vendor/github.com/writeas/go-writeas/v2/writeas.go new file mode 100644 index 0000000..fa87ae1 --- /dev/null +++ b/vendor/github.com/writeas/go-writeas/v2/writeas.go @@ -0,0 +1,199 @@ +// Package writeas provides the binding for the Write.as API +package writeas + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "code.as/core/socks" + "github.com/writeas/impart" +) + +const ( + apiURL = "https://write.as/api" + devAPIURL = "https://development.write.as/api" + torAPIURL = "http://writeas7pm7rcdqg.onion/api" + + // Current go-writeas version + Version = "2-dev" +) + +// Client is used to interact with the Write.as API. It can be used to make +// authenticated or unauthenticated calls. +type Client struct { + baseURL string + + // Access token for the user making requests. + token string + // Client making requests to the API + client *http.Client + + // UserAgent overrides the default User-Agent header + UserAgent string +} + +// defaultHTTPTimeout is the default http.Client timeout. +const defaultHTTPTimeout = 10 * time.Second + +// NewClient creates a new API client. By default, all requests are made +// unauthenticated. To optionally make authenticated requests, call `SetToken`. +// +// c := writeas.NewClient() +// c.SetToken("00000000-0000-0000-0000-000000000000") +func NewClient() *Client { + return NewClientWith(Config{URL: apiURL}) +} + +// NewTorClient creates a new API client for communicating with the Write.as +// Tor hidden service, using the given port to connect to the local SOCKS +// proxy. +func NewTorClient(port int) *Client { + return NewClientWith(Config{URL: torAPIURL, TorPort: port}) +} + +// NewDevClient creates a new API client for development and testing. It'll +// communicate with our development servers, and SHOULD NOT be used in +// production. +func NewDevClient() *Client { + return NewClientWith(Config{URL: devAPIURL}) +} + +// Config configures a Write.as client. +type Config struct { + // URL of the Write.as API service. Defaults to https://write.as/api. + URL string + + // If specified, the API client will communicate with the Write.as Tor + // hidden service using the provided port to connect to the local SOCKS + // proxy. + TorPort int + + // If specified, requests will be authenticated using this user token. + // This may be provided after making a few anonymous requests with + // SetToken. + Token string +} + +// NewClientWith builds a new API client with the provided configuration. +func NewClientWith(c Config) *Client { + if c.URL == "" { + c.URL = apiURL + } + + httpClient := &http.Client{Timeout: defaultHTTPTimeout} + if c.TorPort > 0 { + dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", c.TorPort)) + httpClient.Transport = &http.Transport{Dial: dialSocksProxy} + } + + return &Client{ + client: httpClient, + baseURL: c.URL, + token: c.Token, + } +} + +// SetToken sets the user token for all future Client requests. Setting this to +// an empty string will change back to unauthenticated requests. +func (c *Client) SetToken(token string) { + c.token = token +} + +// Token returns the user token currently set to the Client. +func (c *Client) Token() string { + return c.token +} + +func (c *Client) get(path string, r interface{}) (*impart.Envelope, error) { + method := "GET" + if method != "GET" && method != "HEAD" { + return nil, fmt.Errorf("Method %s not currently supported by library (only HEAD and GET).\n", method) + } + + return c.request(method, path, nil, r) +} + +func (c *Client) post(path string, data, r interface{}) (*impart.Envelope, error) { + b := new(bytes.Buffer) + json.NewEncoder(b).Encode(data) + return c.request("POST", path, b, r) +} + +func (c *Client) put(path string, data, r interface{}) (*impart.Envelope, error) { + b := new(bytes.Buffer) + json.NewEncoder(b).Encode(data) + return c.request("PUT", path, b, r) +} + +func (c *Client) delete(path string, data map[string]string) (*impart.Envelope, error) { + r, err := c.buildRequest("DELETE", path, nil) + if err != nil { + return nil, err + } + + q := r.URL.Query() + for k, v := range data { + q.Add(k, v) + } + r.URL.RawQuery = q.Encode() + + return c.doRequest(r, nil) +} + +func (c *Client) request(method, path string, data io.Reader, result interface{}) (*impart.Envelope, error) { + r, err := c.buildRequest(method, path, data) + if err != nil { + return nil, err + } + + return c.doRequest(r, result) +} + +func (c *Client) buildRequest(method, path string, data io.Reader) (*http.Request, error) { + url := fmt.Sprintf("%s%s", c.baseURL, path) + r, err := http.NewRequest(method, url, data) + if err != nil { + return nil, fmt.Errorf("Create request: %v", err) + } + c.prepareRequest(r) + + return r, nil +} + +func (c *Client) doRequest(r *http.Request, result interface{}) (*impart.Envelope, error) { + resp, err := c.client.Do(r) + if err != nil { + return nil, fmt.Errorf("Request: %v", err) + } + defer resp.Body.Close() + + env := &impart.Envelope{ + Code: resp.StatusCode, + } + if result != nil { + env.Data = result + + err = json.NewDecoder(resp.Body).Decode(&env) + if err != nil { + return nil, err + } + } + + return env, nil +} + +func (c *Client) prepareRequest(r *http.Request) { + ua := c.UserAgent + if ua == "" { + ua = "go-writeas v" + Version + } + r.Header.Set("User-Agent", ua) + r.Header.Add("Content-Type", "application/json") + if c.token != "" { + r.Header.Add("Authorization", "Token "+c.token) + } +} diff --git a/vendor/github.com/writeas/impart/.gitignore b/vendor/github.com/writeas/impart/.gitignore new file mode 100644 index 0000000..090febd --- /dev/null +++ b/vendor/github.com/writeas/impart/.gitignore @@ -0,0 +1,27 @@ +*~ +*.swp + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/writeas/impart/LICENSE b/vendor/github.com/writeas/impart/LICENSE new file mode 100644 index 0000000..7371932 --- /dev/null +++ b/vendor/github.com/writeas/impart/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Write.as + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/writeas/impart/README.md b/vendor/github.com/writeas/impart/README.md new file mode 100644 index 0000000..1d1fba6 --- /dev/null +++ b/vendor/github.com/writeas/impart/README.md @@ -0,0 +1,61 @@ +impart +====== + +![MIT license](https://img.shields.io/github/license/writeas/impart.svg) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) + +**impart** is a library for the final layer between the API and the consumer. It's used in the latest [Write.as](https://write.as) and [HTMLhouse](https://html.house) APIs. + +We're still in the early stages of development, so there may be breaking changes. + +## Example use + +```go +package main + +import ( + "fmt" + "github.com/writeas/impart" + "net/http" +) + +type handlerFunc func(w http.ResponseWriter, r *http.Request) error + +func main() { + http.HandleFunc("/", handle(index)) + http.ListenAndServe("127.0.0.1:8080", nil) +} + +func index(w http.ResponseWriter, r *http.Request) error { + fmt.Fprintf(w, "Hello world!") + + return nil +} + +func handle(f handlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + handleError(w, r, func() error { + // Do authentication... + + // Handle the request + err := f(w, r) + + // Log the request and result... + + return err + }()) + } +} + +func handleError(w http.ResponseWriter, r *http.Request, err error) { + if err == nil { + return + } + + if err, ok := err.(impart.HTTPError); ok { + impart.WriteError(w, err) + return + } + + impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Internal server error :("}) +} +``` diff --git a/vendor/github.com/writeas/impart/doc.go b/vendor/github.com/writeas/impart/doc.go new file mode 100644 index 0000000..a2e17d1 --- /dev/null +++ b/vendor/github.com/writeas/impart/doc.go @@ -0,0 +1,4 @@ +// Package impart provides a simple interface for a JSON-based API. It is +// designed for passing errors around a web application, sending back a status +// code and error message if needed, or a status code and some data on success. +package impart diff --git a/vendor/github.com/writeas/impart/errors.go b/vendor/github.com/writeas/impart/errors.go new file mode 100644 index 0000000..ce5e9b9 --- /dev/null +++ b/vendor/github.com/writeas/impart/errors.go @@ -0,0 +1,20 @@ +package impart + +import ( + "net/http" +) + +// HTTPError holds an HTTP status code and an error message. +type HTTPError struct { + Status int + Message string +} + +// Error displays the HTTPError's error message and satisfies the error +// interface. +func (h HTTPError) Error() string { + if h.Message == "" { + return http.StatusText(h.Status) + } + return h.Message +} diff --git a/vendor/github.com/writeas/impart/go.mod b/vendor/github.com/writeas/impart/go.mod new file mode 100644 index 0000000..5f2e65d --- /dev/null +++ b/vendor/github.com/writeas/impart/go.mod @@ -0,0 +1,3 @@ +module github.com/writeas/impart + +go 1.9 diff --git a/vendor/github.com/writeas/impart/request.go b/vendor/github.com/writeas/impart/request.go new file mode 100644 index 0000000..0f170a1 --- /dev/null +++ b/vendor/github.com/writeas/impart/request.go @@ -0,0 +1,13 @@ +package impart + +import ( + "mime" + "net/http" +) + +// ReqJSON returns whether or not the given Request is sending JSON, based on +// the Content-Type header being application/json. +func ReqJSON(r *http.Request) bool { + ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + return ct == "application/json" +} diff --git a/vendor/github.com/writeas/impart/response.go b/vendor/github.com/writeas/impart/response.go new file mode 100644 index 0000000..fe097bb --- /dev/null +++ b/vendor/github.com/writeas/impart/response.go @@ -0,0 +1,76 @@ +package impart + +import ( + "encoding/json" + "net/http" + "strconv" +) + +type ( + // Envelope contains metadata and optional data for a response object. + // Responses will always contain a status code and either: + // - response Data on a 2xx response, or + // - an ErrorMessage on non-2xx responses + // + // ErrorType is not currently used. + Envelope struct { + Code int `json:"code"` + ErrorType string `json:"error_type,omitempty"` + ErrorMessage string `json:"error_msg,omitempty"` + Data interface{} `json:"data,omitempty"` + } +) + +func writeBody(w http.ResponseWriter, body []byte, status int, contentType string) error { + w.Header().Set("Content-Type", contentType+"; charset=UTF-8") + w.Header().Set("Content-Length", strconv.Itoa(len(body))) + w.WriteHeader(status) + _, err := w.Write(body) + return err +} + +func RenderActivityJSON(w http.ResponseWriter, value interface{}, status int) error { + body, err := json.Marshal(value) + if err != nil { + return err + } + return writeBody(w, body, status, "application/activity+json") +} + +func renderJSON(w http.ResponseWriter, value interface{}, status int) error { + body, err := json.Marshal(value) + if err != nil { + return err + } + return writeBody(w, body, status, "application/json") +} + +func renderString(w http.ResponseWriter, status int, msg string) error { + return writeBody(w, []byte(msg), status, "text/plain") +} + +// WriteSuccess writes the successful data and metadata to the ResponseWriter as +// JSON. +func WriteSuccess(w http.ResponseWriter, data interface{}, status int) error { + env := &Envelope{ + Code: status, + Data: data, + } + return renderJSON(w, env, status) +} + +// WriteError writes the error to the ResponseWriter as JSON. +func WriteError(w http.ResponseWriter, e HTTPError) error { + env := &Envelope{ + Code: e.Status, + ErrorMessage: e.Message, + } + return renderJSON(w, env, e.Status) +} + +// WriteRedirect sends a redirect +func WriteRedirect(w http.ResponseWriter, e HTTPError) int { + w.Header().Set("Location", e.Message) + w.WriteHeader(e.Status) + return e.Status +} diff --git a/vendor/github.com/writeas/saturday/.gitignore b/vendor/github.com/writeas/saturday/.gitignore new file mode 100644 index 0000000..75623dc --- /dev/null +++ b/vendor/github.com/writeas/saturday/.gitignore @@ -0,0 +1,8 @@ +*.out +*.swp +*.8 +*.6 +_obj +_test* +markdown +tags diff --git a/vendor/github.com/writeas/saturday/.travis.yml b/vendor/github.com/writeas/saturday/.travis.yml new file mode 100644 index 0000000..a1687f1 --- /dev/null +++ b/vendor/github.com/writeas/saturday/.travis.yml @@ -0,0 +1,30 @@ +sudo: false +language: go +go: + - 1.5.4 + - 1.6.2 + - tip +matrix: + include: + - go: 1.2.2 + script: + - go get -t -v ./... + - go test -v -race ./... + - go: 1.3.3 + script: + - go get -t -v ./... + - go test -v -race ./... + - go: 1.4.3 + script: + - go get -t -v ./... + - go test -v -race ./... + allow_failures: + - go: tip + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/writeas/saturday/LICENSE.txt b/vendor/github.com/writeas/saturday/LICENSE.txt new file mode 100644 index 0000000..2885af3 --- /dev/null +++ b/vendor/github.com/writeas/saturday/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/writeas/saturday/README.md b/vendor/github.com/writeas/saturday/README.md new file mode 100644 index 0000000..8da1a94 --- /dev/null +++ b/vendor/github.com/writeas/saturday/README.md @@ -0,0 +1,284 @@ +Saturday +======== +Saturday is a fork of [Blackfriday](https://github.com/russross/blackfriday) used on [Write.as](https://write.as). + +We love Markdown, but aren't a Markdown-only platform. So we've stripped out and modified redundant or potentially frustrating syntax in this library. + +## Changes + +* Made images and links behave like standard Markdown (now they won't render when there are spaces between label/alt-text and URL) 12db6e2f7ebcc5d6d88e5b330e4c6d88b577bc95 +* Only support atx-style headings 32843b3dfc510153e76d8f535a9084fc8e22245a +* Removed smart periods, quotes, angles & backticks 72080d757965efc04255fd25ad97c76ef6f03ea9 +* Only support horizontal rules made of hyphens f75e5c8d41435593b7f24243e5c22b50f2b399b4 +* Only support fenced code blocks, not indented blocks 8223c01e430de7fd35f3c38ef75f802734cc0cfc +* Keep leading spaces in paragraphs 24845d212205e789fe24ec27ebc1c4cd121523c9 + +Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) [![GoDoc](https://godoc.org/github.com/russross/blackfriday?status.svg)](https://godoc.org/github.com/russross/blackfriday) +----------- + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. An experimental LaTeX output engine is also included. + +It started as a translation from C of [Sundown][3]. + + +### Installation + +Blackfriday is compatible with Go 1. If you are using an older +release of Go, consider using v1.1 of blackfriday, which was based +on the last stable release of Go prior to Go 1. You can find it as a +tagged commit on github. + +With Go 1 and git installed: + + go get github.com/russross/blackfriday + +will download, compile, and install the package into your `$GOPATH` +directory hierarchy. Alternatively, you can achieve the same if you +import it into a project: + + import "github.com/russross/blackfriday" + +and `go get` without parameters. + +### Usage + +For basic usage, it is as simple as getting your input into a byte +slice and calling: + + output := blackfriday.MarkdownBasic(input) + +This renders it with no extensions enabled. To get a more useful +feature set, use this instead: + + output := blackfriday.MarkdownCommon(input) + +#### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running blackfriday's output +through HTML sanitizer such as +[Bluemonday](https://github.com/microcosm-cc/bluemonday). + +Here's an example of simple usage of blackfriday together with bluemonday: + +``` go +import ( + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" +) + +// ... +unsafe := blackfriday.MarkdownCommon(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +#### Custom options + +If you want to customize the set of options, first get a renderer +(currently either the HTML or LaTeX output engines), then use it to +call the more general `Markdown` function. For examples, see the +implementations of `MarkdownBasic` and `MarkdownCommon` in +`markdown.go`. + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + + +### Features + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +### Extensions + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ``` go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + + To preserve classes of fenced code blocks while using the bluemonday + HTML sanitizer, use the following policy: + + ``` go + p := bluemonday.UGCPolicy() + p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") + html := p.SanitizeBytes(unsafe) + ``` + +* **Definition lists**. A simple definition list is made of a single-line + term followed by a colon and the definition for that term. + + Cat + : Fluffy animal everyone likes + + Internet + : Vector of transmission for pictures of cats + + Terms must be separated from the previous definition by a blank line. + +* **Footnotes**. A marker in the text that will become a superscript number; + a footnote definition that will be placed in a list of footnotes at the + end of the document. A footnote looks like this: + + This is a footnote.[^1] + + [^1]: the footnote text. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled (it is off by + default in the `MarkdownBasic` and `MarkdownCommon` convenience + functions), newlines in the input translate into line breaks in + the output. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `45`, which renders as + 45. + + +### Other renderers + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable header anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* LaTeX output: renders output as LaTeX. This is currently part of the + main Blackfriday repository, but may be split into its own project + in the future. If you are interested in owning and maintaining the + LaTeX output component, please be in touch. + + It renders some basic documents, but is only experimental at this + point. In particular, it does not do any inline escaping, so input + that happens to look like LaTeX code will be passed through without + modification. + +* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format. + + +### Todo + +* More unit testing +* Improve unicode support. It does not understand all unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all utf-8 input. + + +### License + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: http://daringfireball.net/projects/markdown/ "Markdown" + [2]: http://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" diff --git a/vendor/github.com/writeas/saturday/block.go b/vendor/github.com/writeas/saturday/block.go new file mode 100644 index 0000000..eafb67c --- /dev/null +++ b/vendor/github.com/writeas/saturday/block.go @@ -0,0 +1,1412 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + + "github.com/shurcooL/sanitized_anchor_name" +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *parser) block(out *bytes.Buffer, data []byte) { + if len(data) == 0 || data[len(data)-1] != '\n' { + panic("block input is missing terminating newline") + } + + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed header: + // + // # Header 1 + // ## Header 2 + // ... + // ###### Header 6 + if p.isPrefixHeader(data) { + data = data[p.prefixHeader(out, data):] + continue + } + + // block of preformatted HTML: + // + //
+ // ... + //
+ if data[0] == '<' { + if i := p.html(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.flags&EXTENSION_TITLEBLOCK != 0 { + if data[0] == '%' { + if i := p.titleBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // fenced code block: + // + // ``` go + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + if p.isHRule(data) { + p.r.HRule(out) + var i int + for i = 0; data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(out, data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.flags&EXTENSION_TABLES != 0 { + if i := p.table(out, data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(out, data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_ORDERED):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_DEFINITION):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headers, too + data = data[p.paragraph(out, data):] + } + + p.nesting-- +} + +func (p *parser) isPrefixHeader(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.flags&EXTENSION_SPACE_HEADERS != 0 { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + if data[level] != ' ' { + return false + } + } + return true +} + +func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.flags&EXTENSION_HEADER_IDS != 0 { + j, k := 0, 0 + // find start/end of header id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract header id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = sanitized_anchor_name.Create(string(data[i:end])) + } + work := func() bool { + p.inline(out, data[i:end]) + return true + } + p.r.Header(out, work, level, id) + } + return skip +} + +func (p *parser) isUnderlinedHeader(data []byte) int { + // test of level 1 header + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 1 + } else { + return 0 + } + } + + // test of level 2 header + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 2 + } else { + return 0 + } + } + + return 0 +} + +func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + p.r.TitleBlock(out, data) + + return len(data) +} + +func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(out, data, doRender); size > 0 { + return size + } + + // check for an
tag + if size := p.htmlHr(out, data, doRender); size > 0 { + return size + } + + // check for HTML CDATA + if size := p.htmlCDATA(out, data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + + return i +} + +func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { + // html block needs to end with a blank line + if i := p.isEmpty(data[start:]); i > 0 { + size := start + i + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + return size + } + return 0 +} + +// HTML comment, lax form +func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { + i := p.inlineHTMLComment(out, data) + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HTML CDATA section +func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { + const cdataTag = "') { + i++ + } + i++ + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HR, which is the only self-closing block tag considered +func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
tag after all; at least not a valid one + return 0 + } + + i := 3 + for data[i] != '>' && data[i] != '\n' { + i++ + } + + if data[i] == '>' { + return p.renderHTMLBlock(out, data, i+1, doRender) + } + + return 0 +} + +func (p *parser) htmlFindTag(data []byte) (string, bool) { + i := 0 + for isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if _, ok := blockTags[key]; ok { + return key, true + } + return "", false +} + +func (p *parser) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (*parser) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + return i + 1 +} + +func (*parser) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // character must be a hyphen, otherwise not HR + if data[i] != '-' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, +// and returns the end index if so, or 0 otherwise. It also returns the marker found. +// If syntax is not nil, it gets set to the syntax specified in the fence line. +// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. +func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) { + i, size := 0, 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + + // check for the marker characters: ~ or ` + if i >= len(data) { + return 0, "" + } + if data[i] != '~' && data[i] != '`' { + return 0, "" + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + // the marker char must occur at least 3 times + if size < 3 { + return 0, "" + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return 0, "" + } + + // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here + // into one, always get the syntax, and discard it if the caller doesn't care. + if syntax != nil { + syn := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + syntaxStart := i + + if data[i] == '{' { + i++ + syntaxStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + syn++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return 0, "" + } + + // strip all whitespace at the beginning and the end + // of the {} block + for syn > 0 && isspace(data[syntaxStart]) { + syntaxStart++ + syn-- + } + + for syn > 0 && isspace(data[syntaxStart+syn-1]) { + syn-- + } + + i++ + } else { + for i < len(data) && !isspace(data[i]) { + syn++ + i++ + } + } + + *syntax = string(data[syntaxStart : syntaxStart+syn]) + } + + i = skipChar(data, i, ' ') + if i >= len(data) || data[i] != '\n' { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + return i + 1, marker // Take newline into account. +} + +// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, +// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. +// If doRender is true, a final newline is mandatory to recognize the fenced code block. +func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { + var syntax string + beg, marker := isFenceLine(data, &syntax, "", false) + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + newlineOptional := !doRender + fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + if doRender { + p.r.BlockCode(out, work.Bytes(), syntax) + } + + return beg +} + +func (p *parser) table(out *bytes.Buffer, data []byte) int { + var header bytes.Buffer + i, columns := p.tableHeader(&header, data) + if i == 0 { + return 0 + } + + var body bytes.Buffer + + for i < len(data) { + pipes, rowStart := 0, i + for ; data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + i++ + p.tableRow(&body, data[rowStart:i], columns, false) + } + + p.r.Table(out, header.Bytes(), body.Bytes(), columns) + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { + i := 0 + colCount := 1 + for i = 0; data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + header := data[:i+1] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]int, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_LEFT + dashes++ + } + for data[i] == '-' { + i++ + dashes++ + } + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_RIGHT + dashes++ + } + for data[i] == ' ' { + i++ + } + + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.tableRow(out, header, columns, true) + size = i + 1 + return +} + +func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { + i, col := 0, 0 + var rowWork bytes.Buffer + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for data[i] == ' ' { + i++ + } + + cellStart := i + + for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && data[cellEnd-1] == ' ' { + cellEnd-- + } + + var cellWork bytes.Buffer + p.inline(&cellWork, data[cellStart:cellEnd]) + + if header { + p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) + } else { + p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) + } + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + if header { + p.r.TableHeaderCell(&rowWork, nil, columns[col]) + } else { + p.r.TableCell(&rowWork, nil, columns[col]) + } + } + + // silently ignore rows with too many cells + + p.r.TableRow(out, rowWork.Bytes()) +} + +// returns blockquote prefix length +func (p *parser) quotePrefix(data []byte) int { + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + if data[i] == '>' { + if data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// blockquote ends with at least one blank line +// followed by something without a blockquote prefix +func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { + if p.isEmpty(data[beg:]) <= 0 { + return false + } + if end >= len(data) { + return true + } + return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 +} + +// parse a blockquote fragment +func (p *parser) quote(out *bytes.Buffer, data []byte) int { + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + // Step over whole lines, collecting them. While doing that, check for + // fenced code and if one's found, incorporate it altogether, + // irregardless of any contents inside it + for data[end] != '\n' { + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { + // -1 to compensate for the extra end++ after the loop: + end += i - 1 + break + } + } + end++ + } + end++ + + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.terminateBlockquote(data, beg, end) { + break + } + + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + + var cooked bytes.Buffer + p.block(&cooked, raw.Bytes()) + p.r.BlockQuote(out, cooked.Bytes()) + return end +} + +// returns prefix length for block code +func (p *parser) codePrefix(data []byte) int { + if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *parser) code(out *bytes.Buffer, data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for data[i] != '\n' { + i++ + } + i++ + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffeu + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + p.r.BlockCode(out, work.Bytes(), "") + + return i +} + +// returns unordered list item prefix +func (p *parser) uliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // need a *, +, or - followed by a space + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *parser) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for data[i] >= '0' && data[i] <= '9' { + i++ + } + + // we need >= 1 digits followed by a dot and a space + if start == i || data[i] != '.' || data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *parser) dliPrefix(data []byte) int { + i := 0 + + // need a : followed by a spaces + if data[i] != ':' || data[i+1] != ' ' { + return 0 + } + for data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { + i := 0 + flags |= LIST_ITEM_BEGINNING_OF_LIST + work := func() bool { + for i < len(data) { + skip := p.listItem(out, data[i:], &flags) + i += skip + + if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { + break + } + flags &= ^LIST_ITEM_BEGINNING_OF_LIST + } + return true + } + + p.r.List(out, work, flags) + return i +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { + // keep track of the indentation of the first line + itemIndent := 0 + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^LIST_TYPE_TERM + } + } + if i == 0 { + // if in defnition list, set term flag and continue + if *flags&LIST_TYPE_DEFINITION != 0 { + *flags |= LIST_TYPE_TERM + } else { + return 0 + } + } + + // skip leading whitespace on first line + for data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + raw.Write(data[line:i]) + line = i + continue + } + + // calculate the indentation + indent := 0 + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + } + + chunk := data[line+indent : i] + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + if containsBlankLine { + // end the list if the type changed after a blank line + if indent <= itemIndent && + ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || + (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { + + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + // to be a nested list, it must be indented more + // if not, it is the next item in the same list + if indent <= itemIndent { + break gatherlines + } + + // is this the first item in the nested list? + if sublist == 0 { + sublist = raw.Len() + } + + // is this a nested prefix header? + case p.isPrefixHeader(chunk): + // if the header is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= LIST_ITEM_END_OF_LIST + } + } else { + *flags |= LIST_ITEM_END_OF_LIST + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + containsBlankLine = false + + // add the line into the working buffer without prefix + raw.Write(data[line+indent : i]) + + line = i + } + + // If reached end of data, the Renderer.ListItem call we're going to make below + // is definitely the last in the list. + if line >= len(data) { + *flags |= LIST_ITEM_END_OF_LIST + } + + rawBytes := raw.Bytes() + + // render the contents of the list item + var cooked bytes.Buffer + if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.block(&cooked, rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + p.inline(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.inline(&cooked, rawBytes) + } + } + + // render the actual list item + cookedBytes := cooked.Bytes() + parsedEnd := len(cookedBytes) + + // strip trailing newlines + for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { + parsedEnd-- + } + p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) + + return line +} + +// render a single paragraph that has already been parsed out +func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { + if len(data) == 0 { + return + } + + beg := 0 + + // trim trailing newline + end := len(data) - 1 + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + work := func() bool { + p.inline(out, data[beg:end]) + return true + } + p.r.Paragraph(out, work) +} + +func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + p.renderParagraph(out, data[:i]) + return i + n + } + + // an underline under some text marks a header, so our paragraph ended on prev line + // -- But we don't want Setext-style headers on Write.as. atx is great. + /* + if i > 0 { + if level := p.isUnderlinedHeader(current); level > 0 { + // render the paragraph + p.renderParagraph(out, data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + // render the header + // this ugly double closure avoids forcing variables onto the heap + work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { + return func() bool { + pp.inline(o, d) + return true + } + }(out, p, data[prev:eol]) + + id := "" + if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = sanitized_anchor_name.Create(string(data[prev:eol])) + } + + p.r.Header(out, work, level, id) + + // find the end of the underline + for data[i] != '\n' { + i++ + } + return i + } + } + */ + + // if the next line starts a block of HTML, then the paragraph ends here + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if data[i] == '<' && p.html(out, current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a prefixed header or a horizontal rule after this, paragraph is over + if p.isPrefixHeader(current) || p.isHRule(current) { + p.renderParagraph(out, data[:i]) + return i + } + + // if there's a fenced code block, paragraph is over + if p.flags&EXTENSION_FENCED_CODE != 0 { + if p.fencedCodeBlock(out, current, false) > 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a definition list item, prev line is a definition term + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(current) != 0 { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + // if there's a list after this, paragraph is over + if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + for data[i] != '\n' { + i++ + } + i++ + } + + p.renderParagraph(out, data[:i]) + return i +} diff --git a/vendor/github.com/writeas/saturday/html.go b/vendor/github.com/writeas/saturday/html.go new file mode 100644 index 0000000..74e67ee --- /dev/null +++ b/vendor/github.com/writeas/saturday/html.go @@ -0,0 +1,949 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// HTML rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Html renderer configuration options. +const ( + HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks + HTML_SKIP_STYLE // skip embedded
World -``` - -Into a harmless: -```html -Hello World -``` - -And it turns this: -```html -XSS -``` - -Into this: -```html -XSS -``` - -Whilst still allowing this: -```html - - - -``` - -To pass through mostly unaltered (it gained a rel="nofollow" which is a good thing for user generated content): -```html - - - -``` - -It protects sites from [XSS](http://en.wikipedia.org/wiki/Cross-site_scripting) attacks. There are many [vectors for an XSS attack](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) and the best way to mitigate the risk is to sanitize user input against a known safe list of HTML elements and attributes. - -You should **always** run bluemonday **after** any other processing. - -If you use [blackfriday](https://github.com/russross/blackfriday) or [Pandoc](http://johnmacfarlane.net/pandoc/) then bluemonday should be run after these steps. This ensures that no insecure HTML is introduced later in your process. - -bluemonday is heavily inspired by both the [OWASP Java HTML Sanitizer](https://code.google.com/p/owasp-java-html-sanitizer/) and the [HTML Purifier](http://htmlpurifier.org/). - -## Technical Summary - -Whitelist based, you need to either build a policy describing the HTML elements and attributes to permit (and the `regexp` patterns of attributes), or use one of the supplied policies representing good defaults. - -The policy containing the whitelist is applied using a fast non-validating, forward only, token-based parser implemented in the [Go net/html library](https://godoc.org/golang.org/x/net/html) by the core Go team. - -We expect to be supplied with well-formatted HTML (closing elements for every applicable open element, nested correctly) and so we do not focus on repairing badly nested or incomplete HTML. We focus on simply ensuring that whatever elements do exist are described in the policy whitelist and that attributes and links are safe for use on your web page. [GIGO](http://en.wikipedia.org/wiki/Garbage_in,_garbage_out) does apply and if you feed it bad HTML bluemonday is not tasked with figuring out how to make it good again. - -### Supported Go Versions - -bluemonday is tested against Go 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, and tip. - -We do not support Go 1.0 as we depend on `golang.org/x/net/html` which includes a reference to `io.ErrNoProgress` which did not exist in Go 1.0. - -## Is it production ready? - -*Yes* - -We are using bluemonday in production having migrated from the widely used and heavily field tested OWASP Java HTML Sanitizer. - -We are passing our extensive test suite (including AntiSamy tests as well as tests for any issues raised). Check for any [unresolved issues](https://github.com/microcosm-cc/bluemonday/issues?page=1&state=open) to see whether anything may be a blocker for you. - -We invite pull requests and issues to help us ensure we are offering comprehensive protection against various attacks via user generated content. - -## Usage - -Install in your `${GOPATH}` using `go get -u github.com/microcosm-cc/bluemonday` - -Then call it: -```go -package main - -import ( - "fmt" - - "github.com/microcosm-cc/bluemonday" -) - -func main() { - // Do this once for each unique policy, and use the policy for the life of the program - // Policy creation/editing is not safe to use in multiple goroutines - p := bluemonday.UGCPolicy() - - // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines - html := p.Sanitize( - `Google`, - ) - - // Output: - // Google - fmt.Println(html) -} -``` - -We offer three ways to call Sanitize: -```go -p.Sanitize(string) string -p.SanitizeBytes([]byte) []byte -p.SanitizeReader(io.Reader) bytes.Buffer -``` - -If you are obsessed about performance, `p.SanitizeReader(r).Bytes()` will return a `[]byte` without performing any unnecessary casting of the inputs or outputs. Though the difference is so negligible you should never need to care. - -You can build your own policies: -```go -package main - -import ( - "fmt" - - "github.com/microcosm-cc/bluemonday" -) - -func main() { - p := bluemonday.NewPolicy() - - // Require URLs to be parseable by net/url.Parse and either: - // mailto: http:// or https:// - p.AllowStandardURLs() - - // We only allow

and - p.AllowAttrs("href").OnElements("a") - p.AllowElements("p") - - html := p.Sanitize( - `Google`, - ) - - // Output: - // Google - fmt.Println(html) -} -``` - -We ship two default policies: - -1. `bluemonday.StrictPolicy()` which can be thought of as equivalent to stripping all HTML elements and their attributes as it has nothing on its whitelist. An example usage scenario would be blog post titles where HTML tags are not expected at all and if they are then the elements *and* the content of the elements should be stripped. This is a *very* strict policy. -2. `bluemonday.UGCPolicy()` which allows a broad selection of HTML elements and attributes that are safe for user generated content. Note that this policy does *not* whitelist iframes, object, embed, styles, script, etc. An example usage scenario would be blog post bodies where a variety of formatting is expected along with the potential for TABLEs and IMGs. - -## Policy Building - -The essence of building a policy is to determine which HTML elements and attributes are considered safe for your scenario. OWASP provide an [XSS prevention cheat sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) to help explain the risks, but essentially: - -1. Avoid anything other than the standard HTML elements -1. Avoid `script`, `style`, `iframe`, `object`, `embed`, `base` elements that allow code to be executed by the client or third party content to be included that can execute code -1. Avoid anything other than plain HTML attributes with values matched to a regexp - -Basically, you should be able to describe what HTML is fine for your scenario. If you do not have confidence that you can describe your policy please consider using one of the shipped policies such as `bluemonday.UGCPolicy()`. - -To create a new policy: -```go -p := bluemonday.NewPolicy() -``` - -To add elements to a policy either add just the elements: -```go -p.AllowElements("b", "strong") -``` - -Or add elements as a virtue of adding an attribute: -```go -// Not the recommended pattern, see the recommendation on using .Matching() below -p.AllowAttrs("nowrap").OnElements("td", "th") -``` - -Attributes can either be added to all elements: -```go -p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally() -``` - -Or attributes can be added to specific elements: -```go -// Not the recommended pattern, see the recommendation on using .Matching() below -p.AllowAttrs("value").OnElements("li") -``` - -It is **always** recommended that an attribute be made to match a pattern. XSS in HTML attributes is very easy otherwise: -```go -// \p{L} matches unicode letters, \p{N} matches unicode numbers -p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally() -``` - -You can stop at any time and call .Sanitize(): -```go -// string htmlIn passed in from a HTTP POST -htmlOut := p.Sanitize(htmlIn) -``` - -And you can take any existing policy and extend it: -```go -p := bluemonday.UGCPolicy() -p.AllowElements("fieldset", "select", "option") -``` - -### Links - -Links are difficult beasts to sanitise safely and also one of the biggest attack vectors for malicious content. - -It is possible to do this: -```go -p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a") -``` - -But that will not protect you as the regular expression is insufficient in this case to have prevented a malformed value doing something unexpected. - -We provide some additional global options for safely working with links. - -`RequireParseableURLs` will ensure that URLs are parseable by Go's `net/url` package: -```go -p.RequireParseableURLs(true) -``` - -If you have enabled parseable URLs then the following option will `AllowRelativeURLs`. By default this is disabled (bluemonday is a whitelist tool... you need to explicitly tell us to permit things) and when disabled it will prevent all local and scheme relative URLs (i.e. `href="localpage.html"`, `href="../home.html"` and even `href="//www.google.com"` are relative): -```go -p.AllowRelativeURLs(true) -``` - -If you have enabled parseable URLs then you can whitelist the schemes (commonly called protocol when thinking of `http` and `https`) that are permitted. Bear in mind that allowing relative URLs in the above option will allow for a blank scheme: -```go -p.AllowURLSchemes("mailto", "http", "https") -``` - -Regardless of whether you have enabled parseable URLs, you can force all URLs to have a rel="nofollow" attribute. This will be added if it does not exist, but only when the `href` is valid: -```go -// This applies to "a" "area" "link" elements that have a "href" attribute -p.RequireNoFollowOnLinks(true) -``` - -We provide a convenience method that applies all of the above, but you will still need to whitelist the linkable elements for the URL rules to be applied to: -```go -p.AllowStandardURLs() -p.AllowAttrs("cite").OnElements("blockquote", "q") -p.AllowAttrs("href").OnElements("a", "area") -p.AllowAttrs("src").OnElements("img") -``` - -An additional complexity regarding links is the data URI as defined in [RFC2397](http://tools.ietf.org/html/rfc2397). The data URI allows for images to be served inline using this format: - -```html - -``` - -We have provided a helper to verify the mimetype followed by base64 content of data URIs links: - -```go -p.AllowDataURIImages() -``` - -That helper will enable GIF, JPEG, PNG and WEBP images. - -It should be noted that there is a potential [security](http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/) [risk](https://capec.mitre.org/data/definitions/244.html) with the use of data URI links. You should only enable data URI links if you already trust the content. - -We also have some features to help deal with user generated content: -```go -p.AddTargetBlankToFullyQualifiedLinks(true) -``` - -This will ensure that anchor `` links that are fully qualified (the href destination includes a host name) will get `target="_blank"` added to them. - -Additionally any link that has `target="_blank"` after the policy has been applied will also have the `rel` attribute adjusted to add `noopener`. This means a link may start like `` and will end up as ``. It is important to note that the addition of `noopener` is a security feature and not an issue. There is an unfortunate feature to browsers that a browser window opened as a result of `target="_blank"` can still control the opener (your web page) and this protects against that. The background to this can be found here: [https://dev.to/ben/the-targetblank-vulnerability-by-example](https://dev.to/ben/the-targetblank-vulnerability-by-example) - -### Policy Building Helpers - -We also bundle some helpers to simplify policy building: -```go - -// Permits the "dir", "id", "lang", "title" attributes globally -p.AllowStandardAttributes() - -// Permits the "img" element and its standard attributes -p.AllowImages() - -// Permits ordered and unordered lists, and also definition lists -p.AllowLists() - -// Permits HTML tables and all applicable elements and non-styling attributes -p.AllowTables() -``` - -### Invalid Instructions - -The following are invalid: -```go -// This does not say where the attributes are allowed, you need to add -// .Globally() or .OnElements(...) -// This will be ignored without error. -p.AllowAttrs("value") - -// This does not say where the attributes are allowed, you need to add -// .Globally() or .OnElements(...) -// This will be ignored without error. -p.AllowAttrs( - "type", -).Matching( - regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"), -) -``` - -Both examples exhibit the same issue, they declare attributes but do not then specify whether they are whitelisted globally or only on specific elements (and which elements). Attributes belong to one or more elements, and the policy needs to declare this. - -## Limitations - -We are not yet including any tools to help whitelist and sanitize CSS. Which means that unless you wish to do the heavy lifting in a single regular expression (inadvisable), **you should not allow the "style" attribute anywhere**. - -It is not the job of bluemonday to fix your bad HTML, it is merely the job of bluemonday to prevent malicious HTML getting through. If you have mismatched HTML elements, or non-conforming nesting of elements, those will remain. But if you have well-structured HTML bluemonday will not break it. - -## TODO - -* Add support for CSS sanitisation to allow some CSS properties based on a whitelist, possibly using the [Gorilla CSS3 scanner](http://www.gorillatoolkit.org/pkg/css/scanner) - PRs welcome so long as testing covers XSS and demonstrates safety first -* Investigate whether devs want to blacklist elements and attributes. This would allow devs to take an existing policy (such as the `bluemonday.UGCPolicy()` ) that encapsulates 90% of what they're looking for but does more than they need, and to remove the extra things they do not want to make it 100% what they want -* Investigate whether devs want a validating HTML mode, in which the HTML elements are not just transformed into a balanced tree (every start tag has a closing tag at the correct depth) but also that elements and character data appear only in their allowed context (i.e. that a `table` element isn't a descendent of a `caption`, that `colgroup`, `thead`, `tbody`, `tfoot` and `tr` are permitted, and that character data is not permitted) - -## Development - -If you have cloned this repo you will probably need the dependency: - -`go get golang.org/x/net/html` - -Gophers can use their familiar tools: - -`go build` - -`go test` - -I personally use a Makefile as it spares typing the same args over and over whilst providing consistency for those of us who jump from language to language and enjoy just typing `make` in a project directory and watch magic happen. - -`make` will build, vet, test and install the library. - -`make clean` will remove the library from a *single* `${GOPATH}/pkg` directory tree - -`make test` will run the tests - -`make cover` will run the tests and *open a browser window* with the coverage report - -`make lint` will run golint (install via `go get github.com/golang/lint/golint`) - -## Long term goals - -1. Open the code to adversarial peer review similar to the [Attack Review Ground Rules](https://code.google.com/p/owasp-java-html-sanitizer/wiki/AttackReviewGroundRules) -1. Raise funds and pay for an external security review diff --git a/vendor/github.com/microcosm-cc/bluemonday/doc.go b/vendor/github.com/microcosm-cc/bluemonday/doc.go deleted file mode 100644 index 71dab60..0000000 --- a/vendor/github.com/microcosm-cc/bluemonday/doc.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2014, David Kitchen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of the organisation (Microcosm) nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Package bluemonday provides a way of describing a whitelist of HTML elements -and attributes as a policy, and for that policy to be applied to untrusted -strings from users that may contain markup. All elements and attributes not on -the whitelist will be stripped. - -The default bluemonday.UGCPolicy().Sanitize() turns this: - - Hello World - -Into the more harmless: - - Hello World - -And it turns this: - - XSS - -Into this: - - XSS - -Whilst still allowing this: - - - - - -To pass through mostly unaltered (it gained a rel="nofollow"): - - - - - -The primary purpose of bluemonday is to take potentially unsafe user generated -content (from things like Markdown, HTML WYSIWYG tools, etc) and make it safe -for you to put on your website. - -It protects sites against XSS (http://en.wikipedia.org/wiki/Cross-site_scripting) -and other malicious content that a user interface may deliver. There are many -vectors for an XSS attack (https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet) -and the safest thing to do is to sanitize user input against a known safe list -of HTML elements and attributes. - -Note: You should always run bluemonday after any other processing. - -If you use blackfriday (https://github.com/russross/blackfriday) or -Pandoc (http://johnmacfarlane.net/pandoc/) then bluemonday should be run after -these steps. This ensures that no insecure HTML is introduced later in your -process. - -bluemonday is heavily inspired by both the OWASP Java HTML Sanitizer -(https://code.google.com/p/owasp-java-html-sanitizer/) and the HTML Purifier -(http://htmlpurifier.org/). - -We ship two default policies, one is bluemonday.StrictPolicy() and can be -thought of as equivalent to stripping all HTML elements and their attributes as -it has nothing on its whitelist. - -The other is bluemonday.UGCPolicy() and allows a broad selection of HTML -elements and attributes that are safe for user generated content. Note that -this policy does not whitelist iframes, object, embed, styles, script, etc. - -The essence of building a policy is to determine which HTML elements and -attributes are considered safe for your scenario. OWASP provide an XSS -prevention cheat sheet ( https://www.google.com/search?q=xss+prevention+cheat+sheet ) -to help explain the risks, but essentially: - - 1. Avoid whitelisting anything other than plain HTML elements - 2. Avoid whitelisting `script`, `style`, `iframe`, `object`, `embed`, `base` - elements - 3. Avoid whitelisting anything other than plain HTML elements with simple - values that you can match to a regexp -*/ -package bluemonday diff --git a/vendor/github.com/microcosm-cc/bluemonday/helpers.go b/vendor/github.com/microcosm-cc/bluemonday/helpers.go deleted file mode 100644 index dfa5868..0000000 --- a/vendor/github.com/microcosm-cc/bluemonday/helpers.go +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) 2014, David Kitchen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of the organisation (Microcosm) nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package bluemonday - -import ( - "encoding/base64" - "net/url" - "regexp" -) - -// A selection of regular expressions that can be used as .Matching() rules on -// HTML attributes. -var ( - // CellAlign handles the `align` attribute - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-align - CellAlign = regexp.MustCompile(`(?i)^(center|justify|left|right|char)$`) - - // CellVerticalAlign handles the `valign` attribute - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-valign - CellVerticalAlign = regexp.MustCompile(`(?i)^(baseline|bottom|middle|top)$`) - - // Direction handles the `dir` attribute - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo#attr-dir - Direction = regexp.MustCompile(`(?i)^(rtl|ltr)$`) - - // ImageAlign handles the `align` attribute on the `image` tag - // http://www.w3.org/MarkUp/Test/Img/imgtest.html - ImageAlign = regexp.MustCompile( - `(?i)^(left|right|top|texttop|middle|absmiddle|baseline|bottom|absbottom)$`, - ) - - // Integer describes whole positive integers (including 0) used in places - // like td.colspan - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#attr-colspan - Integer = regexp.MustCompile(`^[0-9]+$`) - - // ISO8601 according to the W3 group is only a subset of the ISO8601 - // standard: http://www.w3.org/TR/NOTE-datetime - // - // Used in places like time.datetime - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time#attr-datetime - // - // Matches patterns: - // Year: - // YYYY (eg 1997) - // Year and month: - // YYYY-MM (eg 1997-07) - // Complete date: - // YYYY-MM-DD (eg 1997-07-16) - // Complete date plus hours and minutes: - // YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) - // Complete date plus hours, minutes and seconds: - // YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) - // Complete date plus hours, minutes, seconds and a decimal fraction of a - // second - // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) - ISO8601 = regexp.MustCompile( - `^[0-9]{4}(-[0-9]{2}(-[0-9]{2}([ T][0-9]{2}(:[0-9]{2}){1,2}(.[0-9]{1,6})` + - `?Z?([\+-][0-9]{2}:[0-9]{2})?)?)?)?$`, - ) - - // ListType encapsulates the common value as well as the latest spec - // values for lists - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol#attr-type - ListType = regexp.MustCompile(`(?i)^(circle|disc|square|a|A|i|I|1)$`) - - // SpaceSeparatedTokens is used in places like `a.rel` and the common attribute - // `class` which both contain space delimited lists of data tokens - // http://www.w3.org/TR/html-markup/datatypes.html#common.data.tokens-def - // Regexp: \p{L} matches unicode letters, \p{N} matches unicode numbers - SpaceSeparatedTokens = regexp.MustCompile(`^([\s\p{L}\p{N}_-]+)$`) - - // Number is a double value used on HTML5 meter and progress elements - // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-button-element.html#the-meter-element - Number = regexp.MustCompile(`^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$`) - - // NumberOrPercent is used predominantly as units of measurement in width - // and height attributes - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-height - NumberOrPercent = regexp.MustCompile(`^[0-9]+[%]?$`) - - // Paragraph of text in an attribute such as *.'title', img.alt, etc - // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-title - // Note that we are not allowing chars that could close tags like '>' - Paragraph = regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$`) - - // dataURIImagePrefix is used by AllowDataURIImages to define the acceptable - // prefix of data URIs that contain common web image formats. - // - // This is not exported as it's not useful by itself, and only has value - // within the AllowDataURIImages func - dataURIImagePrefix = regexp.MustCompile( - `^image/(gif|jpeg|png|webp);base64,`, - ) -) - -// AllowStandardURLs is a convenience function that will enable rel="nofollow" -// on "a", "area" and "link" (if you have allowed those elements) and will -// ensure that the URL values are parseable and either relative or belong to the -// "mailto", "http", or "https" schemes -func (p *Policy) AllowStandardURLs() { - // URLs must be parseable by net/url.Parse() - p.RequireParseableURLs(true) - - // !url.IsAbs() is permitted - p.AllowRelativeURLs(true) - - // Most common URL schemes only - p.AllowURLSchemes("mailto", "http", "https") - - // For all anchors we will add rel="nofollow" if it does not already exist - // This applies to "a" "area" "link" - p.RequireNoFollowOnLinks(true) -} - -// AllowStandardAttributes will enable "id", "title" and the language specific -// attributes "dir" and "lang" on all elements that are whitelisted -func (p *Policy) AllowStandardAttributes() { - // "dir" "lang" are permitted as both language attributes affect charsets - // and direction of text. - p.AllowAttrs("dir").Matching(Direction).Globally() - p.AllowAttrs( - "lang", - ).Matching(regexp.MustCompile(`[a-zA-Z]{2,20}`)).Globally() - - // "id" is permitted. This is pretty much as some HTML elements require this - // to work well ("dfn" is an example of a "id" being value) - // This does create a risk that JavaScript and CSS within your web page - // might identify the wrong elements. Ensure that you select things - // accurately - p.AllowAttrs("id").Matching( - regexp.MustCompile(`[a-zA-Z0-9\:\-_\.]+`), - ).Globally() - - // "title" is permitted as it improves accessibility. - p.AllowAttrs("title").Matching(Paragraph).Globally() -} - -// AllowStyling presently enables the class attribute globally. -// -// Note: When bluemonday ships a CSS parser and we can safely sanitise that, -// this will also allow sanitized styling of elements via the style attribute. -func (p *Policy) AllowStyling() { - - // "class" is permitted globally - p.AllowAttrs("class").Matching(SpaceSeparatedTokens).Globally() -} - -// AllowImages enables the img element and some popular attributes. It will also -// ensure that URL values are parseable. This helper does not enable data URI -// images, for that you should also use the AllowDataURIImages() helper. -func (p *Policy) AllowImages() { - - // "img" is permitted - p.AllowAttrs("align").Matching(ImageAlign).OnElements("img") - p.AllowAttrs("alt").Matching(Paragraph).OnElements("img") - p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("img") - - // Standard URLs enabled - p.AllowStandardURLs() - p.AllowAttrs("src").OnElements("img") -} - -// AllowDataURIImages permits the use of inline images defined in RFC2397 -// http://tools.ietf.org/html/rfc2397 -// http://en.wikipedia.org/wiki/Data_URI_scheme -// -// Images must have a mimetype matching: -// image/gif -// image/jpeg -// image/png -// image/webp -// -// NOTE: There is a potential security risk to allowing data URIs and you should -// only permit them on content you already trust. -// http://palizine.plynt.com/issues/2010Oct/bypass-xss-filters/ -// https://capec.mitre.org/data/definitions/244.html -func (p *Policy) AllowDataURIImages() { - - // URLs must be parseable by net/url.Parse() - p.RequireParseableURLs(true) - - // Supply a function to validate images contained within data URI - p.AllowURLSchemeWithCustomPolicy( - "data", - func(url *url.URL) (allowUrl bool) { - if url.RawQuery != "" || url.Fragment != "" { - return false - } - - matched := dataURIImagePrefix.FindString(url.Opaque) - if matched == "" { - return false - } - - _, err := base64.StdEncoding.DecodeString(url.Opaque[len(matched):]) - if err != nil { - return false - } - - return true - }, - ) -} - -// AllowLists will enabled ordered and unordered lists, as well as definition -// lists -func (p *Policy) AllowLists() { - // "ol" "ul" are permitted - p.AllowAttrs("type").Matching(ListType).OnElements("ol", "ul") - - // "li" is permitted - p.AllowAttrs("type").Matching(ListType).OnElements("li") - p.AllowAttrs("value").Matching(Integer).OnElements("li") - - // "dl" "dt" "dd" are permitted - p.AllowElements("dl", "dt", "dd") -} - -// AllowTables will enable a rich set of elements and attributes to describe -// HTML tables -func (p *Policy) AllowTables() { - - // "table" is permitted - p.AllowAttrs("height", "width").Matching(NumberOrPercent).OnElements("table") - p.AllowAttrs("summary").Matching(Paragraph).OnElements("table") - - // "caption" is permitted - p.AllowElements("caption") - - // "col" "colgroup" are permitted - p.AllowAttrs("align").Matching(CellAlign).OnElements("col", "colgroup") - p.AllowAttrs("height", "width").Matching( - NumberOrPercent, - ).OnElements("col", "colgroup") - p.AllowAttrs("span").Matching(Integer).OnElements("colgroup", "col") - p.AllowAttrs("valign").Matching( - CellVerticalAlign, - ).OnElements("col", "colgroup") - - // "thead" "tr" are permitted - p.AllowAttrs("align").Matching(CellAlign).OnElements("thead", "tr") - p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("thead", "tr") - - // "td" "th" are permitted - p.AllowAttrs("abbr").Matching(Paragraph).OnElements("td", "th") - p.AllowAttrs("align").Matching(CellAlign).OnElements("td", "th") - p.AllowAttrs("colspan", "rowspan").Matching(Integer).OnElements("td", "th") - p.AllowAttrs("headers").Matching( - SpaceSeparatedTokens, - ).OnElements("td", "th") - p.AllowAttrs("height", "width").Matching( - NumberOrPercent, - ).OnElements("td", "th") - p.AllowAttrs( - "scope", - ).Matching( - regexp.MustCompile(`(?i)(?:row|col)(?:group)?`), - ).OnElements("td", "th") - p.AllowAttrs("valign").Matching(CellVerticalAlign).OnElements("td", "th") - p.AllowAttrs("nowrap").Matching( - regexp.MustCompile(`(?i)|nowrap`), - ).OnElements("td", "th") - - // "tbody" "tfoot" - p.AllowAttrs("align").Matching(CellAlign).OnElements("tbody", "tfoot") - p.AllowAttrs("valign").Matching( - CellVerticalAlign, - ).OnElements("tbody", "tfoot") -} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policies.go b/vendor/github.com/microcosm-cc/bluemonday/policies.go deleted file mode 100644 index 570bba8..0000000 --- a/vendor/github.com/microcosm-cc/bluemonday/policies.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) 2014, David Kitchen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of the organisation (Microcosm) nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package bluemonday - -import ( - "regexp" -) - -// StrictPolicy returns an empty policy, which will effectively strip all HTML -// elements and their attributes from a document. -func StrictPolicy() *Policy { - return NewPolicy() -} - -// StripTagsPolicy is DEPRECATED. Use StrictPolicy instead. -func StripTagsPolicy() *Policy { - return StrictPolicy() -} - -// UGCPolicy returns a policy aimed at user generated content that is a result -// of HTML WYSIWYG tools and Markdown conversions. -// -// This is expected to be a fairly rich document where as much markup as -// possible should be retained. Markdown permits raw HTML so we are basically -// providing a policy to sanitise HTML5 documents safely but with the -// least intrusion on the formatting expectations of the user. -func UGCPolicy() *Policy { - - p := NewPolicy() - - /////////////////////// - // Global attributes // - /////////////////////// - - // "class" is not permitted as we are not allowing users to style their own - // content - - p.AllowStandardAttributes() - - ////////////////////////////// - // Global URL format policy // - ////////////////////////////// - - p.AllowStandardURLs() - - //////////////////////////////// - // Declarations and structure // - //////////////////////////////// - - // "xml" "xslt" "DOCTYPE" "html" "head" are not permitted as we are - // expecting user generated content to be a fragment of HTML and not a full - // document. - - ////////////////////////// - // Sectioning root tags // - ////////////////////////// - - // "article" and "aside" are permitted and takes no attributes - p.AllowElements("article", "aside") - - // "body" is not permitted as we are expecting user generated content to be a fragment - // of HTML and not a full document. - - // "details" is permitted, including the "open" attribute which can either - // be blank or the value "open". - p.AllowAttrs( - "open", - ).Matching(regexp.MustCompile(`(?i)^(|open)$`)).OnElements("details") - - // "fieldset" is not permitted as we are not allowing forms to be created. - - // "figure" is permitted and takes no attributes - p.AllowElements("figure") - - // "nav" is not permitted as it is assumed that the site (and not the user) - // has defined navigation elements - - // "section" is permitted and takes no attributes - p.AllowElements("section") - - // "summary" is permitted and takes no attributes - p.AllowElements("summary") - - ////////////////////////// - // Headings and footers // - ////////////////////////// - - // "footer" is not permitted as we expect user content to be a fragment and - // not structural to this extent - - // "h1" through "h6" are permitted and take no attributes - p.AllowElements("h1", "h2", "h3", "h4", "h5", "h6") - - // "header" is not permitted as we expect user content to be a fragment and - // not structural to this extent - - // "hgroup" is permitted and takes no attributes - p.AllowElements("hgroup") - - ///////////////////////////////////// - // Content grouping and separating // - ///////////////////////////////////// - - // "blockquote" is permitted, including the "cite" attribute which must be - // a standard URL. - p.AllowAttrs("cite").OnElements("blockquote") - - // "br" "div" "hr" "p" "span" "wbr" are permitted and take no attributes - p.AllowElements("br", "div", "hr", "p", "span", "wbr") - - /////////// - // Links // - /////////// - - // "a" is permitted - p.AllowAttrs("href").OnElements("a") - - // "area" is permitted along with the attributes that map image maps work - p.AllowAttrs("name").Matching( - regexp.MustCompile(`^([\p{L}\p{N}_-]+)$`), - ).OnElements("map") - p.AllowAttrs("alt").Matching(Paragraph).OnElements("area") - p.AllowAttrs("coords").Matching( - regexp.MustCompile(`^([0-9]+,)+[0-9]+$`), - ).OnElements("area") - p.AllowAttrs("href").OnElements("area") - p.AllowAttrs("rel").Matching(SpaceSeparatedTokens).OnElements("area") - p.AllowAttrs("shape").Matching( - regexp.MustCompile(`(?i)^(default|circle|rect|poly)$`), - ).OnElements("area") - p.AllowAttrs("usemap").Matching( - regexp.MustCompile(`(?i)^#[\p{L}\p{N}_-]+$`), - ).OnElements("img") - - // "link" is not permitted - - ///////////////////// - // Phrase elements // - ///////////////////// - - // The following are all inline phrasing elements - p.AllowElements("abbr", "acronym", "cite", "code", "dfn", "em", - "figcaption", "mark", "s", "samp", "strong", "sub", "sup", "var") - - // "q" is permitted and "cite" is a URL and handled by URL policies - p.AllowAttrs("cite").OnElements("q") - - // "time" is permitted - p.AllowAttrs("datetime").Matching(ISO8601).OnElements("time") - - //////////////////// - // Style elements // - //////////////////// - - // block and inline elements that impart no semantic meaning but style the - // document - p.AllowElements("b", "i", "pre", "small", "strike", "tt", "u") - - // "style" is not permitted as we are not yet sanitising CSS and it is an - // XSS attack vector - - ////////////////////// - // HTML5 Formatting // - ////////////////////// - - // "bdi" "bdo" are permitted - p.AllowAttrs("dir").Matching(Direction).OnElements("bdi", "bdo") - - // "rp" "rt" "ruby" are permitted - p.AllowElements("rp", "rt", "ruby") - - /////////////////////////// - // HTML5 Change tracking // - /////////////////////////// - - // "del" "ins" are permitted - p.AllowAttrs("cite").Matching(Paragraph).OnElements("del", "ins") - p.AllowAttrs("datetime").Matching(ISO8601).OnElements("del", "ins") - - /////////// - // Lists // - /////////// - - p.AllowLists() - - //////////// - // Tables // - //////////// - - p.AllowTables() - - /////////// - // Forms // - /////////// - - // By and large, forms are not permitted. However there are some form - // elements that can be used to present data, and we do permit those - // - // "button" "fieldset" "input" "keygen" "label" "output" "select" "datalist" - // "textarea" "optgroup" "option" are all not permitted - - // "meter" is permitted - p.AllowAttrs( - "value", - "min", - "max", - "low", - "high", - "optimum", - ).Matching(Number).OnElements("meter") - - // "progress" is permitted - p.AllowAttrs("value", "max").Matching(Number).OnElements("progress") - - ////////////////////// - // Embedded content // - ////////////////////// - - // Vast majority not permitted - // "audio" "canvas" "embed" "iframe" "object" "param" "source" "svg" "track" - // "video" are all not permitted - - p.AllowImages() - - return p -} diff --git a/vendor/github.com/microcosm-cc/bluemonday/policy.go b/vendor/github.com/microcosm-cc/bluemonday/policy.go deleted file mode 100644 index f61d98f..0000000 --- a/vendor/github.com/microcosm-cc/bluemonday/policy.go +++ /dev/null @@ -1,552 +0,0 @@ -// Copyright (c) 2014, David Kitchen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of the organisation (Microcosm) nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package bluemonday - -import ( - "net/url" - "regexp" - "strings" -) - -// Policy encapsulates the whitelist of HTML elements and attributes that will -// be applied to the sanitised HTML. -// -// You should use bluemonday.NewPolicy() to create a blank policy as the -// unexported fields contain maps that need to be initialized. -type Policy struct { - - // Declares whether the maps have been initialized, used as a cheap check to - // ensure that those using Policy{} directly won't cause nil pointer - // exceptions - initialized bool - - // If true then we add spaces when stripping tags, specifically the closing - // tag is replaced by a space character. - addSpaces bool - - // When true, add rel="nofollow" to HTML anchors - requireNoFollow bool - - // When true, add rel="nofollow" to HTML anchors - // Will add for href="http://foo" - // Will skip for href="/foo" or href="foo" - requireNoFollowFullyQualifiedLinks bool - - // When true add target="_blank" to fully qualified links - // Will add for href="http://foo" - // Will skip for href="/foo" or href="foo" - addTargetBlankToFullyQualifiedLinks bool - - // When true, URLs must be parseable by "net/url" url.Parse() - requireParseableURLs bool - - // When true, u, _ := url.Parse("url"); !u.IsAbs() is permitted - allowRelativeURLs bool - - // When true, allow data attributes. - allowDataAttributes bool - - // map[htmlElementName]map[htmlAttributeName]attrPolicy - elsAndAttrs map[string]map[string]attrPolicy - - // map[htmlAttributeName]attrPolicy - globalAttrs map[string]attrPolicy - - // If urlPolicy is nil, all URLs with matching schema are allowed. - // Otherwise, only the URLs with matching schema and urlPolicy(url) - // returning true are allowed. - allowURLSchemes map[string]urlPolicy - - // If an element has had all attributes removed as a result of a policy - // being applied, then the element would be removed from the output. - // - // However some elements are valid and have strong layout meaning without - // any attributes, i.e.

. To prevent those being removed we maintain - // a list of elements that are allowed to have no attributes and that will - // be maintained in the output HTML. - setOfElementsAllowedWithoutAttrs map[string]struct{} - - setOfElementsToSkipContent map[string]struct{} -} - -type attrPolicy struct { - - // optional pattern to match, when not nil the regexp needs to match - // otherwise the attribute is removed - regexp *regexp.Regexp -} - -type attrPolicyBuilder struct { - p *Policy - - attrNames []string - regexp *regexp.Regexp - allowEmpty bool -} - -type urlPolicy func(url *url.URL) (allowUrl bool) - -// init initializes the maps if this has not been done already -func (p *Policy) init() { - if !p.initialized { - p.elsAndAttrs = make(map[string]map[string]attrPolicy) - p.globalAttrs = make(map[string]attrPolicy) - p.allowURLSchemes = make(map[string]urlPolicy) - p.setOfElementsAllowedWithoutAttrs = make(map[string]struct{}) - p.setOfElementsToSkipContent = make(map[string]struct{}) - p.initialized = true - } -} - -// NewPolicy returns a blank policy with nothing whitelisted or permitted. This -// is the recommended way to start building a policy and you should now use -// AllowAttrs() and/or AllowElements() to construct the whitelist of HTML -// elements and attributes. -func NewPolicy() *Policy { - - p := Policy{} - - p.addDefaultElementsWithoutAttrs() - p.addDefaultSkipElementContent() - - return &p -} - -// AllowAttrs takes a range of HTML attribute names and returns an -// attribute policy builder that allows you to specify the pattern and scope of -// the whitelisted attribute. -// -// The attribute policy is only added to the core policy when either Globally() -// or OnElements(...) are called. -func (p *Policy) AllowAttrs(attrNames ...string) *attrPolicyBuilder { - - p.init() - - abp := attrPolicyBuilder{ - p: p, - allowEmpty: false, - } - - for _, attrName := range attrNames { - abp.attrNames = append(abp.attrNames, strings.ToLower(attrName)) - } - - return &abp -} - -// AllowDataAttributes whitelists all data attributes. We can't specify the name -// of each attribute exactly as they are customized. -// -// NOTE: These values are not sanitized and applications that evaluate or process -// them without checking and verification of the input may be at risk if this option -// is enabled. This is a 'caveat emptor' option and the person enabling this option -// needs to fully understand the potential impact with regards to whatever application -// will be consuming the sanitized HTML afterwards, i.e. if you know you put a link in a -// data attribute and use that to automatically load some new window then you're giving -// the author of a HTML fragment the means to open a malicious destination automatically. -// Use with care! -func (p *Policy) AllowDataAttributes() { - p.allowDataAttributes = true -} - -// AllowNoAttrs says that attributes on element are optional. -// -// The attribute policy is only added to the core policy when OnElements(...) -// are called. -func (p *Policy) AllowNoAttrs() *attrPolicyBuilder { - - p.init() - - abp := attrPolicyBuilder{ - p: p, - allowEmpty: true, - } - return &abp -} - -// AllowNoAttrs says that attributes on element are optional. -// -// The attribute policy is only added to the core policy when OnElements(...) -// are called. -func (abp *attrPolicyBuilder) AllowNoAttrs() *attrPolicyBuilder { - - abp.allowEmpty = true - - return abp -} - -// Matching allows a regular expression to be applied to a nascent attribute -// policy, and returns the attribute policy. Calling this more than once will -// replace the existing regexp. -func (abp *attrPolicyBuilder) Matching(regex *regexp.Regexp) *attrPolicyBuilder { - - abp.regexp = regex - - return abp -} - -// OnElements will bind an attribute policy to a given range of HTML elements -// and return the updated policy -func (abp *attrPolicyBuilder) OnElements(elements ...string) *Policy { - - for _, element := range elements { - element = strings.ToLower(element) - - for _, attr := range abp.attrNames { - - if _, ok := abp.p.elsAndAttrs[element]; !ok { - abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) - } - - ap := attrPolicy{} - if abp.regexp != nil { - ap.regexp = abp.regexp - } - - abp.p.elsAndAttrs[element][attr] = ap - } - - if abp.allowEmpty { - abp.p.setOfElementsAllowedWithoutAttrs[element] = struct{}{} - - if _, ok := abp.p.elsAndAttrs[element]; !ok { - abp.p.elsAndAttrs[element] = make(map[string]attrPolicy) - } - } - } - - return abp.p -} - -// Globally will bind an attribute policy to all HTML elements and return the -// updated policy -func (abp *attrPolicyBuilder) Globally() *Policy { - - for _, attr := range abp.attrNames { - if _, ok := abp.p.globalAttrs[attr]; !ok { - abp.p.globalAttrs[attr] = attrPolicy{} - } - - ap := attrPolicy{} - if abp.regexp != nil { - ap.regexp = abp.regexp - } - - abp.p.globalAttrs[attr] = ap - } - - return abp.p -} - -// AllowElements will append HTML elements to the whitelist without applying an -// attribute policy to those elements (the elements are permitted -// sans-attributes) -func (p *Policy) AllowElements(names ...string) *Policy { - p.init() - - for _, element := range names { - element = strings.ToLower(element) - - if _, ok := p.elsAndAttrs[element]; !ok { - p.elsAndAttrs[element] = make(map[string]attrPolicy) - } - } - - return p -} - -// RequireNoFollowOnLinks will result in all tags having a rel="nofollow" -// added to them if one does not already exist -// -// Note: This requires p.RequireParseableURLs(true) and will enable it. -func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy { - - p.requireNoFollow = require - p.requireParseableURLs = true - - return p -} - -// RequireNoFollowOnFullyQualifiedLinks will result in all tags that point -// to a non-local destination (i.e. starts with a protocol and has a host) -// having a rel="nofollow" added to them if one does not already exist -// -// Note: This requires p.RequireParseableURLs(true) and will enable it. -func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy { - - p.requireNoFollowFullyQualifiedLinks = require - p.requireParseableURLs = true - - return p -} - -// AddTargetBlankToFullyQualifiedLinks will result in all tags that point -// to a non-local destination (i.e. starts with a protocol and has a host) -// having a target="_blank" added to them if one does not already exist -// -// Note: This requires p.RequireParseableURLs(true) and will enable it. -func (p *Policy) AddTargetBlankToFullyQualifiedLinks(require bool) *Policy { - - p.addTargetBlankToFullyQualifiedLinks = require - p.requireParseableURLs = true - - return p -} - -// RequireParseableURLs will result in all URLs requiring that they be parseable -// by "net/url" url.Parse() -// This applies to: -// - a.href -// - area.href -// - blockquote.cite -// - img.src -// - link.href -// - script.src -func (p *Policy) RequireParseableURLs(require bool) *Policy { - - p.requireParseableURLs = require - - return p -} - -// AllowRelativeURLs enables RequireParseableURLs and then permits URLs that -// are parseable, have no schema information and url.IsAbs() returns false -// This permits local URLs -func (p *Policy) AllowRelativeURLs(require bool) *Policy { - - p.RequireParseableURLs(true) - p.allowRelativeURLs = require - - return p -} - -// AllowURLSchemes will append URL schemes to the whitelist -// Example: p.AllowURLSchemes("mailto", "http", "https") -func (p *Policy) AllowURLSchemes(schemes ...string) *Policy { - p.init() - - p.RequireParseableURLs(true) - - for _, scheme := range schemes { - scheme = strings.ToLower(scheme) - - // Allow all URLs with matching scheme. - p.allowURLSchemes[scheme] = nil - } - - return p -} - -// AllowURLSchemeWithCustomPolicy will append URL schemes with -// a custom URL policy to the whitelist. -// Only the URLs with matching schema and urlPolicy(url) -// returning true will be allowed. -func (p *Policy) AllowURLSchemeWithCustomPolicy( - scheme string, - urlPolicy func(url *url.URL) (allowUrl bool), -) *Policy { - - p.init() - - p.RequireParseableURLs(true) - - scheme = strings.ToLower(scheme) - - p.allowURLSchemes[scheme] = urlPolicy - - return p -} - -// AddSpaceWhenStrippingTag states whether to add a single space " " when -// removing tags that are not whitelisted by the policy. -// -// This is useful if you expect to strip tags in dense markup and may lose the -// value of whitespace. -// -// For example: "

Hello

World

"" would be sanitized to "HelloWorld" -// with the default value of false, but you may wish to sanitize this to -// " Hello World " by setting AddSpaceWhenStrippingTag to true as this would -// retain the intent of the text. -func (p *Policy) AddSpaceWhenStrippingTag(allow bool) *Policy { - - p.addSpaces = allow - - return p -} - -// SkipElementsContent adds the HTML elements whose tags is needed to be removed -// with its content. -func (p *Policy) SkipElementsContent(names ...string) *Policy { - - p.init() - - for _, element := range names { - element = strings.ToLower(element) - - if _, ok := p.setOfElementsToSkipContent[element]; !ok { - p.setOfElementsToSkipContent[element] = struct{}{} - } - } - - return p -} - -// AllowElementsContent marks the HTML elements whose content should be -// retained after removing the tag. -func (p *Policy) AllowElementsContent(names ...string) *Policy { - - p.init() - - for _, element := range names { - delete(p.setOfElementsToSkipContent, strings.ToLower(element)) - } - - return p -} - -// addDefaultElementsWithoutAttrs adds the HTML elements that we know are valid -// without any attributes to an internal map. -// i.e. we know that
is valid, but isn't valid as the "dir" attr -// is mandatory -func (p *Policy) addDefaultElementsWithoutAttrs() { - p.init() - - p.setOfElementsAllowedWithoutAttrs["abbr"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["acronym"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["address"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["article"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["aside"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["audio"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["b"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["bdi"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["blockquote"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["body"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["br"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["button"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["canvas"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["caption"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["center"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["cite"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["code"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["col"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["colgroup"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["datalist"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["dd"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["del"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["details"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["dfn"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["div"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["dl"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["dt"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["em"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["fieldset"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["figcaption"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["figure"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["footer"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h1"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h2"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h3"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h4"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h5"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["h6"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["head"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["header"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["hgroup"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["hr"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["html"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["i"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["ins"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["kbd"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["li"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["mark"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["marquee"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["nav"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["ol"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["optgroup"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["option"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["p"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["pre"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["q"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["rp"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["rt"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["ruby"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["s"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["samp"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["script"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["section"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["select"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["small"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["span"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["strike"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["strong"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["style"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["sub"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["summary"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["sup"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["svg"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["table"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["tbody"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["td"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["textarea"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["tfoot"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["th"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["thead"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["title"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["time"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["tr"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["tt"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["u"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["ul"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["var"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["video"] = struct{}{} - p.setOfElementsAllowedWithoutAttrs["wbr"] = struct{}{} - -} - -// addDefaultSkipElementContent adds the HTML elements that we should skip -// rendering the character content of, if the element itself is not allowed. -// This is all character data that the end user would not normally see. -// i.e. if we exclude a tag. -func (p *Policy) addDefaultSkipElementContent() { - p.init() - - p.setOfElementsToSkipContent["frame"] = struct{}{} - p.setOfElementsToSkipContent["frameset"] = struct{}{} - p.setOfElementsToSkipContent["iframe"] = struct{}{} - p.setOfElementsToSkipContent["noembed"] = struct{}{} - p.setOfElementsToSkipContent["noframes"] = struct{}{} - p.setOfElementsToSkipContent["noscript"] = struct{}{} - p.setOfElementsToSkipContent["nostyle"] = struct{}{} - p.setOfElementsToSkipContent["object"] = struct{}{} - p.setOfElementsToSkipContent["script"] = struct{}{} - p.setOfElementsToSkipContent["style"] = struct{}{} - p.setOfElementsToSkipContent["title"] = struct{}{} -} diff --git a/vendor/github.com/microcosm-cc/bluemonday/sanitize.go b/vendor/github.com/microcosm-cc/bluemonday/sanitize.go deleted file mode 100644 index 65ed89b..0000000 --- a/vendor/github.com/microcosm-cc/bluemonday/sanitize.go +++ /dev/null @@ -1,581 +0,0 @@ -// Copyright (c) 2014, David Kitchen -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// -// * Neither the name of the organisation (Microcosm) nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package bluemonday - -import ( - "bytes" - "io" - "net/url" - "regexp" - "strings" - - "golang.org/x/net/html" -) - -var ( - dataAttribute = regexp.MustCompile("^data-.+") - dataAttributeXMLPrefix = regexp.MustCompile("^xml.+") - dataAttributeInvalidChars = regexp.MustCompile("[A-Z;]+") -) - -// Sanitize takes a string that contains a HTML fragment or document and applies -// the given policy whitelist. -// -// It returns a HTML string that has been sanitized by the policy or an empty -// string if an error has occurred (most likely as a consequence of extremely -// malformed input) -func (p *Policy) Sanitize(s string) string { - if strings.TrimSpace(s) == "" { - return s - } - - return p.sanitize(strings.NewReader(s)).String() -} - -// SanitizeBytes takes a []byte that contains a HTML fragment or document and applies -// the given policy whitelist. -// -// It returns a []byte containing the HTML that has been sanitized by the policy -// or an empty []byte if an error has occurred (most likely as a consequence of -// extremely malformed input) -func (p *Policy) SanitizeBytes(b []byte) []byte { - if len(bytes.TrimSpace(b)) == 0 { - return b - } - - return p.sanitize(bytes.NewReader(b)).Bytes() -} - -// SanitizeReader takes an io.Reader that contains a HTML fragment or document -// and applies the given policy whitelist. -// -// It returns a bytes.Buffer containing the HTML that has been sanitized by the -// policy. Errors during sanitization will merely return an empty result. -func (p *Policy) SanitizeReader(r io.Reader) *bytes.Buffer { - return p.sanitize(r) -} - -// Performs the actual sanitization process. -func (p *Policy) sanitize(r io.Reader) *bytes.Buffer { - - // It is possible that the developer has created the policy via: - // p := bluemonday.Policy{} - // rather than: - // p := bluemonday.NewPolicy() - // If this is the case, and if they haven't yet triggered an action that - // would initiliaze the maps, then we need to do that. - p.init() - - var ( - buff bytes.Buffer - skipElementContent bool - skippingElementsCount int64 - skipClosingTag bool - closingTagToSkipStack []string - mostRecentlyStartedToken string - ) - - tokenizer := html.NewTokenizer(r) - for { - if tokenizer.Next() == html.ErrorToken { - err := tokenizer.Err() - if err == io.EOF { - // End of input means end of processing - return &buff - } - - // Raw tokenizer error - return &bytes.Buffer{} - } - - token := tokenizer.Token() - switch token.Type { - case html.DoctypeToken: - - // DocType is not handled as there is no safe parsing mechanism - // provided by golang.org/x/net/html for the content, and this can - // be misused to insert HTML tags that are not then sanitized - // - // One might wish to recursively sanitize here using the same policy - // but I will need to do some further testing before considering - // this. - - case html.CommentToken: - - // Comments are ignored by default - - case html.StartTagToken: - - mostRecentlyStartedToken = token.Data - - aps, ok := p.elsAndAttrs[token.Data] - if !ok { - if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { - skipElementContent = true - skippingElementsCount++ - } - if p.addSpaces { - buff.WriteString(" ") - } - break - } - - if len(token.Attr) != 0 { - token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) - } - - if len(token.Attr) == 0 { - if !p.allowNoAttrs(token.Data) { - skipClosingTag = true - closingTagToSkipStack = append(closingTagToSkipStack, token.Data) - if p.addSpaces { - buff.WriteString(" ") - } - break - } - } - - if !skipElementContent { - buff.WriteString(token.String()) - } - - case html.EndTagToken: - - if mostRecentlyStartedToken == token.Data { - mostRecentlyStartedToken = "" - } - - if skipClosingTag && closingTagToSkipStack[len(closingTagToSkipStack)-1] == token.Data { - closingTagToSkipStack = closingTagToSkipStack[:len(closingTagToSkipStack)-1] - if len(closingTagToSkipStack) == 0 { - skipClosingTag = false - } - if p.addSpaces { - buff.WriteString(" ") - } - break - } - - if _, ok := p.elsAndAttrs[token.Data]; !ok { - if _, ok := p.setOfElementsToSkipContent[token.Data]; ok { - skippingElementsCount-- - if skippingElementsCount == 0 { - skipElementContent = false - } - } - if p.addSpaces { - buff.WriteString(" ") - } - break - } - - if !skipElementContent { - buff.WriteString(token.String()) - } - - case html.SelfClosingTagToken: - - aps, ok := p.elsAndAttrs[token.Data] - if !ok { - if p.addSpaces { - buff.WriteString(" ") - } - break - } - - if len(token.Attr) != 0 { - token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps) - } - - if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) { - if p.addSpaces { - buff.WriteString(" ") - } - break - } - - if !skipElementContent { - buff.WriteString(token.String()) - } - - case html.TextToken: - - if !skipElementContent { - switch mostRecentlyStartedToken { - case "script": - // not encouraged, but if a policy allows JavaScript we - // should not HTML escape it as that would break the output - buff.WriteString(token.Data) - case "style": - // not encouraged, but if a policy allows CSS styles we - // should not HTML escape it as that would break the output - buff.WriteString(token.Data) - default: - // HTML escape the text - buff.WriteString(token.String()) - } - } - default: - // A token that didn't exist in the html package when we wrote this - return &bytes.Buffer{} - } - } -} - -// sanitizeAttrs takes a set of element attribute policies and the global -// attribute policies and applies them to the []html.Attribute returning a set -// of html.Attributes that match the policies -func (p *Policy) sanitizeAttrs( - elementName string, - attrs []html.Attribute, - aps map[string]attrPolicy, -) []html.Attribute { - - if len(attrs) == 0 { - return attrs - } - - // Builds a new attribute slice based on the whether the attribute has been - // whitelisted explicitly or globally. - cleanAttrs := []html.Attribute{} - for _, htmlAttr := range attrs { - if p.allowDataAttributes { - // If we see a data attribute, let it through. - if isDataAttribute(htmlAttr.Key) { - cleanAttrs = append(cleanAttrs, htmlAttr) - continue - } - } - // Is there an element specific attribute policy that applies? - if ap, ok := aps[htmlAttr.Key]; ok { - if ap.regexp != nil { - if ap.regexp.MatchString(htmlAttr.Val) { - cleanAttrs = append(cleanAttrs, htmlAttr) - continue - } - } else { - cleanAttrs = append(cleanAttrs, htmlAttr) - continue - } - } - - // Is there a global attribute policy that applies? - if ap, ok := p.globalAttrs[htmlAttr.Key]; ok { - - if ap.regexp != nil { - if ap.regexp.MatchString(htmlAttr.Val) { - cleanAttrs = append(cleanAttrs, htmlAttr) - } - } else { - cleanAttrs = append(cleanAttrs, htmlAttr) - } - } - } - - if len(cleanAttrs) == 0 { - // If nothing was allowed, let's get out of here - return cleanAttrs - } - // cleanAttrs now contains the attributes that are permitted - - if linkable(elementName) { - if p.requireParseableURLs { - // Ensure URLs are parseable: - // - a.href - // - area.href - // - link.href - // - blockquote.cite - // - q.cite - // - img.src - // - script.src - tmpAttrs := []html.Attribute{} - for _, htmlAttr := range cleanAttrs { - switch elementName { - case "a", "area", "link": - if htmlAttr.Key == "href" { - if u, ok := p.validURL(htmlAttr.Val); ok { - htmlAttr.Val = u - tmpAttrs = append(tmpAttrs, htmlAttr) - } - break - } - tmpAttrs = append(tmpAttrs, htmlAttr) - case "blockquote", "q": - if htmlAttr.Key == "cite" { - if u, ok := p.validURL(htmlAttr.Val); ok { - htmlAttr.Val = u - tmpAttrs = append(tmpAttrs, htmlAttr) - } - break - } - tmpAttrs = append(tmpAttrs, htmlAttr) - case "img", "script": - if htmlAttr.Key == "src" { - if u, ok := p.validURL(htmlAttr.Val); ok { - htmlAttr.Val = u - tmpAttrs = append(tmpAttrs, htmlAttr) - } - break - } - tmpAttrs = append(tmpAttrs, htmlAttr) - default: - tmpAttrs = append(tmpAttrs, htmlAttr) - } - } - cleanAttrs = tmpAttrs - } - - if (p.requireNoFollow || - p.requireNoFollowFullyQualifiedLinks || - p.addTargetBlankToFullyQualifiedLinks) && - len(cleanAttrs) > 0 { - - // Add rel="nofollow" if a "href" exists - switch elementName { - case "a", "area", "link": - var hrefFound bool - var externalLink bool - for _, htmlAttr := range cleanAttrs { - if htmlAttr.Key == "href" { - hrefFound = true - - u, err := url.Parse(htmlAttr.Val) - if err != nil { - continue - } - if u.Host != "" { - externalLink = true - } - - continue - } - } - - if hrefFound { - var ( - noFollowFound bool - targetBlankFound bool - ) - - addNoFollow := (p.requireNoFollow || - externalLink && p.requireNoFollowFullyQualifiedLinks) - - addTargetBlank := (externalLink && - p.addTargetBlankToFullyQualifiedLinks) - - tmpAttrs := []html.Attribute{} - for _, htmlAttr := range cleanAttrs { - - var appended bool - if htmlAttr.Key == "rel" && addNoFollow { - - if strings.Contains(htmlAttr.Val, "nofollow") { - noFollowFound = true - tmpAttrs = append(tmpAttrs, htmlAttr) - appended = true - } else { - htmlAttr.Val += " nofollow" - noFollowFound = true - tmpAttrs = append(tmpAttrs, htmlAttr) - appended = true - } - } - - if elementName == "a" && htmlAttr.Key == "target" { - if htmlAttr.Val == "_blank" { - targetBlankFound = true - } - if addTargetBlank && !targetBlankFound { - htmlAttr.Val = "_blank" - targetBlankFound = true - tmpAttrs = append(tmpAttrs, htmlAttr) - appended = true - } - } - - if !appended { - tmpAttrs = append(tmpAttrs, htmlAttr) - } - } - if noFollowFound || targetBlankFound { - cleanAttrs = tmpAttrs - } - - if addNoFollow && !noFollowFound { - rel := html.Attribute{} - rel.Key = "rel" - rel.Val = "nofollow" - cleanAttrs = append(cleanAttrs, rel) - } - - if elementName == "a" && addTargetBlank && !targetBlankFound { - rel := html.Attribute{} - rel.Key = "target" - rel.Val = "_blank" - targetBlankFound = true - cleanAttrs = append(cleanAttrs, rel) - } - - if targetBlankFound { - // target="_blank" has a security risk that allows the - // opened window/tab to issue JavaScript calls against - // window.opener, which in effect allow the destination - // of the link to control the source: - // https://dev.to/ben/the-targetblank-vulnerability-by-example - // - // To mitigate this risk, we need to add a specific rel - // attribute if it is not already present. - // rel="noopener" - // - // Unfortunately this is processing the rel twice (we - // already looked at it earlier ^^) as we cannot be sure - // of the ordering of the href and rel, and whether we - // have fully satisfied that we need to do this. This - // double processing only happens *if* target="_blank" - // is true. - var noOpenerAdded bool - tmpAttrs := []html.Attribute{} - for _, htmlAttr := range cleanAttrs { - var appended bool - if htmlAttr.Key == "rel" { - if strings.Contains(htmlAttr.Val, "noopener") { - noOpenerAdded = true - tmpAttrs = append(tmpAttrs, htmlAttr) - } else { - htmlAttr.Val += " noopener" - noOpenerAdded = true - tmpAttrs = append(tmpAttrs, htmlAttr) - } - - appended = true - } - if !appended { - tmpAttrs = append(tmpAttrs, htmlAttr) - } - } - if noOpenerAdded { - cleanAttrs = tmpAttrs - } else { - // rel attr was not found, or else noopener would - // have been added already - rel := html.Attribute{} - rel.Key = "rel" - rel.Val = "noopener" - cleanAttrs = append(cleanAttrs, rel) - } - - } - } - default: - } - } - } - - return cleanAttrs -} - -func (p *Policy) allowNoAttrs(elementName string) bool { - _, ok := p.setOfElementsAllowedWithoutAttrs[elementName] - return ok -} - -func (p *Policy) validURL(rawurl string) (string, bool) { - if p.requireParseableURLs { - // URLs are valid if when space is trimmed the URL is valid - rawurl = strings.TrimSpace(rawurl) - - // URLs cannot contain whitespace, unless it is a data-uri - if (strings.Contains(rawurl, " ") || - strings.Contains(rawurl, "\t") || - strings.Contains(rawurl, "\n")) && - !strings.HasPrefix(rawurl, `data:`) { - return "", false - } - - // URLs are valid if they parse - u, err := url.Parse(rawurl) - if err != nil { - return "", false - } - - if u.Scheme != "" { - - urlPolicy, ok := p.allowURLSchemes[u.Scheme] - if !ok { - return "", false - - } - - if urlPolicy == nil || urlPolicy(u) == true { - return u.String(), true - } - - return "", false - } - - if p.allowRelativeURLs { - if u.String() != "" { - return u.String(), true - } - } - - return "", false - } - - return rawurl, true -} - -func linkable(elementName string) bool { - switch elementName { - case "a", "area", "blockquote", "img", "link", "script": - return true - default: - return false - } -} - -func isDataAttribute(val string) bool { - if !dataAttribute.MatchString(val) { - return false - } - rest := strings.Split(val, "data-") - if len(rest) == 1 { - return false - } - // data-xml* is invalid. - if dataAttributeXMLPrefix.MatchString(rest[1]) { - return false - } - // no uppercase or semi-colons allowed. - if dataAttributeInvalidChars.MatchString(rest[1]) { - return false - } - return true -} diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE deleted file mode 100644 index f9c841a..0000000 --- a/vendor/github.com/mitchellh/go-homedir/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Mitchell Hashimoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md deleted file mode 100644 index d70706d..0000000 --- a/vendor/github.com/mitchellh/go-homedir/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# go-homedir - -This is a Go library for detecting the user's home directory without -the use of cgo, so the library can be used in cross-compilation environments. - -Usage is incredibly simple, just call `homedir.Dir()` to get the home directory -for a user, and `homedir.Expand()` to expand the `~` in a path to the home -directory. - -**Why not just use `os/user`?** The built-in `os/user` package requires -cgo on Darwin systems. This means that any Go code that uses that package -cannot cross compile. But 99% of the time the use for `os/user` is just to -retrieve the home directory, which we can do for the current user without -cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/go.mod b/vendor/github.com/mitchellh/go-homedir/go.mod deleted file mode 100644 index 7efa09a..0000000 --- a/vendor/github.com/mitchellh/go-homedir/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/mitchellh/go-homedir diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go deleted file mode 100644 index fb87bef..0000000 --- a/vendor/github.com/mitchellh/go-homedir/homedir.go +++ /dev/null @@ -1,157 +0,0 @@ -package homedir - -import ( - "bytes" - "errors" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" -) - -// DisableCache will disable caching of the home directory. Caching is enabled -// by default. -var DisableCache bool - -var homedirCache string -var cacheLock sync.RWMutex - -// Dir returns the home directory for the executing user. -// -// This uses an OS-specific method for discovering the home directory. -// An error is returned if a home directory cannot be detected. -func Dir() (string, error) { - if !DisableCache { - cacheLock.RLock() - cached := homedirCache - cacheLock.RUnlock() - if cached != "" { - return cached, nil - } - } - - cacheLock.Lock() - defer cacheLock.Unlock() - - var result string - var err error - if runtime.GOOS == "windows" { - result, err = dirWindows() - } else { - // Unix-like system, so just assume Unix - result, err = dirUnix() - } - - if err != nil { - return "", err - } - homedirCache = result - return result, nil -} - -// Expand expands the path to include the home directory if the path -// is prefixed with `~`. If it isn't prefixed with `~`, the path is -// returned as-is. -func Expand(path string) (string, error) { - if len(path) == 0 { - return path, nil - } - - if path[0] != '~' { - return path, nil - } - - if len(path) > 1 && path[1] != '/' && path[1] != '\\' { - return "", errors.New("cannot expand user-specific home dir") - } - - dir, err := Dir() - if err != nil { - return "", err - } - - return filepath.Join(dir, path[1:]), nil -} - -func dirUnix() (string, error) { - homeEnv := "HOME" - if runtime.GOOS == "plan9" { - // On plan9, env vars are lowercase. - homeEnv = "home" - } - - // First prefer the HOME environmental variable - if home := os.Getenv(homeEnv); home != "" { - return home, nil - } - - var stdout bytes.Buffer - - // If that fails, try OS specific commands - if runtime.GOOS == "darwin" { - cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) - cmd.Stdout = &stdout - if err := cmd.Run(); err == nil { - result := strings.TrimSpace(stdout.String()) - if result != "" { - return result, nil - } - } - } else { - cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) - cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - // If the error is ErrNotFound, we ignore it. Otherwise, return it. - if err != exec.ErrNotFound { - return "", err - } - } else { - if passwd := strings.TrimSpace(stdout.String()); passwd != "" { - // username:password:uid:gid:gecos:home:shell - passwdParts := strings.SplitN(passwd, ":", 7) - if len(passwdParts) > 5 { - return passwdParts[5], nil - } - } - } - } - - // If all else fails, try the shell - stdout.Reset() - cmd := exec.Command("sh", "-c", "cd && pwd") - cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - return "", err - } - - result := strings.TrimSpace(stdout.String()) - if result == "" { - return "", errors.New("blank output when reading home directory") - } - - return result, nil -} - -func dirWindows() (string, error) { - // First prefer the HOME environmental variable - if home := os.Getenv("HOME"); home != "" { - return home, nil - } - - // Prefer standard environment variable USERPROFILE - if home := os.Getenv("USERPROFILE"); home != "" { - return home, nil - } - - drive := os.Getenv("HOMEDRIVE") - path := os.Getenv("HOMEPATH") - home := drive + path - if drive == "" || path == "" { - return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") - } - - return home, nil -} diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml deleted file mode 100644 index 93b1fcd..0000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: go -go: - - 1.x - - master -matrix: - allow_failures: - - go: master - fast_finish: true -install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . - - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE deleted file mode 100644 index c35c17a..0000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2015 Dmitri Shuralyov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md deleted file mode 100644 index 670bf0f..0000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md +++ /dev/null @@ -1,36 +0,0 @@ -sanitized_anchor_name -===================== - -[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) - -Package sanitized_anchor_name provides a func to create sanitized anchor names. - -Its logic can be reused by multiple packages to create interoperable anchor names -and links to those anchors. - -At this time, it does not try to ensure that generated anchor names -are unique, that responsibility falls on the caller. - -Installation ------------- - -```bash -go get -u github.com/shurcooL/sanitized_anchor_name -``` - -Example -------- - -```Go -anchorName := sanitized_anchor_name.Create("This is a header") - -fmt.Println(anchorName) - -// Output: -// this-is-a-header -``` - -License -------- - -- [MIT License](LICENSE) diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go deleted file mode 100644 index 6a77d12..0000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go +++ /dev/null @@ -1,29 +0,0 @@ -// Package sanitized_anchor_name provides a func to create sanitized anchor names. -// -// Its logic can be reused by multiple packages to create interoperable anchor names -// and links to those anchors. -// -// At this time, it does not try to ensure that generated anchor names -// are unique, that responsibility falls on the caller. -package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" - -import "unicode" - -// Create returns a sanitized anchor name for the given text. -func Create(text string) string { - var anchorName []rune - var futureDash = false - for _, r := range text { - switch { - case unicode.IsLetter(r) || unicode.IsNumber(r): - if futureDash && len(anchorName) > 0 { - anchorName = append(anchorName, '-') - } - futureDash = false - anchorName = append(anchorName, unicode.ToLower(r)) - default: - futureDash = true - } - } - return string(anchorName) -} diff --git a/vendor/github.com/writeas/go-writeas/v2/.gitignore b/vendor/github.com/writeas/go-writeas/v2/.gitignore deleted file mode 100644 index 87ae607..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*~ -*.swp -writeas diff --git a/vendor/github.com/writeas/go-writeas/v2/LICENSE b/vendor/github.com/writeas/go-writeas/v2/LICENSE deleted file mode 100644 index 134a5b8..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Write.as - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/writeas/go-writeas/v2/README.md b/vendor/github.com/writeas/go-writeas/v2/README.md deleted file mode 100644 index 5288001..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# go-writeas - -[![godoc](https://godoc.org/go.code.as/writeas.v2?status.svg)](https://godoc.org/go.code.as/writeas.v2) - -Official Write.as Go client library. - -## Installation - -**Warning**: the `v2` branch is under heavy development and its API will change without notice. - -For a stable API, use `go.code.as/writeas.v1` and upgrade to `v2` once everything is merged into `master`. - -```bash -go get go.code.as/writeas.v2 -``` - -## Documentation - -See all functionality and usages in the [API documentation](https://developer.write.as/docs/api/). - -### Example usage - -```go -import "go.code.as/writeas.v2" - -func main() { - // Create the client - c := writeas.NewClient() - - // Publish a post - p, err := c.CreatePost(&writeas.PostParams{ - Title: "Title!", - Content: "This is a post.", - Font: "sans", - }) - if err != nil { - // Perhaps show err.Error() - } - - // Save token for later, since it won't ever be returned again - token := p.Token - - // Update a published post - p, err = c.UpdatePost(p.ID, token, &writeas.PostParams{ - Content: "Now it's been updated!", - }) - if err != nil { - // handle - } - - // Get a published post - p, err = c.GetPost(p.ID) - if err != nil { - // handle - } - - // Delete a post - err = c.DeletePost(p.ID, token) -} -``` - -## Contributing - -The library covers our usage, but might not be comprehensive of the API. So we always welcome contributions and improvements from the community. Before sending pull requests, make sure you've done the following: - -* Run `goimports` on all updated .go files. -* Document all exported structs and funcs. - -## License - -MIT diff --git a/vendor/github.com/writeas/go-writeas/v2/auth.go b/vendor/github.com/writeas/go-writeas/v2/auth.go deleted file mode 100644 index 3cf4249..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/auth.go +++ /dev/null @@ -1,75 +0,0 @@ -package writeas - -import ( - "fmt" - "net/http" -) - -// LogIn authenticates a user with Write.as. -// See https://developer.write.as/docs/api/#authenticate-a-user -func (c *Client) LogIn(username, pass string) (*AuthUser, error) { - u := &AuthUser{} - up := struct { - Alias string `json:"alias"` - Pass string `json:"pass"` - }{ - Alias: username, - Pass: pass, - } - - env, err := c.post("/auth/login", up, u) - if err != nil { - return nil, err - } - - var ok bool - if u, ok = env.Data.(*AuthUser); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - status := env.Code - if status != http.StatusOK { - if status == http.StatusBadRequest { - return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) - } else if status == http.StatusUnauthorized { - return nil, fmt.Errorf("Incorrect password.") - } else if status == http.StatusNotFound { - return nil, fmt.Errorf("User does not exist.") - } else if status == http.StatusTooManyRequests { - return nil, fmt.Errorf("Too many log in attempts in a short period of time.") - } - return nil, fmt.Errorf("Problem authenticating: %d. %v\n", status, err) - } - - c.SetToken(u.AccessToken) - return u, nil -} - -// LogOut logs the current user out, making the Client's current access token -// invalid. -func (c *Client) LogOut() error { - env, err := c.delete("/auth/me", nil) - if err != nil { - return err - } - - status := env.Code - if status != http.StatusNoContent { - if status == http.StatusNotFound { - return fmt.Errorf("Access token is invalid or doesn't exist") - } - return fmt.Errorf("Unable to log out: %v", env.ErrorMessage) - } - - // Logout successful, so update the Client - c.token = "" - - return nil -} - -func (c *Client) isNotLoggedIn(code int) bool { - if c.token == "" { - return false - } - return code == http.StatusUnauthorized -} diff --git a/vendor/github.com/writeas/go-writeas/v2/collection.go b/vendor/github.com/writeas/go-writeas/v2/collection.go deleted file mode 100644 index 9b4a925..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/collection.go +++ /dev/null @@ -1,186 +0,0 @@ -package writeas - -import ( - "fmt" - "net/http" -) - -type ( - // Collection represents a collection of posts. Blogs are a type of collection - // on Write.as. - Collection struct { - Alias string `json:"alias"` - Title string `json:"title"` - Description string `json:"description"` - StyleSheet string `json:"style_sheet"` - Private bool `json:"private"` - Views int64 `json:"views"` - Domain string `json:"domain,omitempty"` - Email string `json:"email,omitempty"` - URL string `json:"url,omitempty"` - - TotalPosts int `json:"total_posts"` - - Posts *[]Post `json:"posts,omitempty"` - } - - // CollectionParams holds values for creating a collection. - CollectionParams struct { - Alias string `json:"alias"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - } -) - -// CreateCollection creates a new collection, returning a user-friendly error -// if one comes up. Requires a Write.as subscription. See -// https://developer.write.as/docs/api/#create-a-collection -func (c *Client) CreateCollection(sp *CollectionParams) (*Collection, error) { - p := &Collection{} - env, err := c.post("/collections", sp, p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*Collection); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - status := env.Code - if status != http.StatusCreated { - if status == http.StatusBadRequest { - return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) - } else if status == http.StatusForbidden { - return nil, fmt.Errorf("Casual or Pro user required.") - } else if status == http.StatusConflict { - return nil, fmt.Errorf("Collection name is already taken.") - } else if status == http.StatusPreconditionFailed { - return nil, fmt.Errorf("Reached max collection quota.") - } - return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err) - } - return p, nil -} - -// GetCollection retrieves a collection, returning the Collection and any error -// (in user-friendly form) that occurs. See -// https://developer.write.as/docs/api/#retrieve-a-collection -func (c *Client) GetCollection(alias string) (*Collection, error) { - coll := &Collection{} - env, err := c.get(fmt.Sprintf("/collections/%s", alias), coll) - if err != nil { - return nil, err - } - - var ok bool - if coll, ok = env.Data.(*Collection); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - status := env.Code - - if status == http.StatusOK { - return coll, nil - } else if status == http.StatusNotFound { - return nil, fmt.Errorf("Collection not found.") - } else { - return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) - } -} - -// GetCollectionPosts retrieves a collection's posts, returning the Posts -// and any error (in user-friendly form) that occurs. See -// https://developer.write.as/docs/api/#retrieve-collection-posts -func (c *Client) GetCollectionPosts(alias string) (*[]Post, error) { - coll := &Collection{} - env, err := c.get(fmt.Sprintf("/collections/%s/posts", alias), coll) - if err != nil { - return nil, err - } - - var ok bool - if coll, ok = env.Data.(*Collection); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - status := env.Code - - if status == http.StatusOK { - return coll.Posts, nil - } else if status == http.StatusNotFound { - return nil, fmt.Errorf("Collection not found.") - } else { - return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) - } -} - -// GetCollectionPost retrieves a post from a collection -// and any error (in user-friendly form) that occurs). See -// https://developers.write.as/docs/api/#retrieve-a-collection-post -func (c *Client) GetCollectionPost(alias, slug string) (*Post, error) { - post := Post{} - - env, err := c.get(fmt.Sprintf("/collections/%s/posts/%s", alias, slug), &post) - if err != nil { - return nil, err - } - - if _, ok := env.Data.(*Post); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - if env.Code == http.StatusOK { - return &post, nil - } else if env.Code == http.StatusNotFound { - return nil, fmt.Errorf("Post %s not found in collection %s", slug, alias) - } - - return nil, fmt.Errorf("Problem getting post %s from collection %s: %d. %v\n", slug, alias, env.Code, err) -} - -// GetUserCollections retrieves the authenticated user's collections. -// See https://developers.write.as/docs/api/#retrieve-user-39-s-collections -func (c *Client) GetUserCollections() (*[]Collection, error) { - colls := &[]Collection{} - env, err := c.get("/me/collections", colls) - if err != nil { - return nil, err - } - - var ok bool - if colls, ok = env.Data.(*[]Collection); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - status := env.Code - - if status != http.StatusOK { - if c.isNotLoggedIn(status) { - return nil, fmt.Errorf("Not authenticated.") - } - return nil, fmt.Errorf("Problem getting collections: %d. %v\n", status, err) - } - return colls, nil -} - -// DeleteCollection permanently deletes a collection and makes any posts on it -// anonymous. -// -// See https://developers.write.as/docs/api/#delete-a-collection. -func (c *Client) DeleteCollection(alias string) error { - endpoint := "/collections/" + alias - env, err := c.delete(endpoint, nil /* data */) - if err != nil { - return err - } - - status := env.Code - switch status { - case http.StatusNoContent: - return nil - case http.StatusUnauthorized: - return fmt.Errorf("Not authenticated.") - case http.StatusBadRequest: - return fmt.Errorf("Bad request: %s", env.ErrorMessage) - default: - return fmt.Errorf("Problem deleting collection: %d. %s\n", status, env.ErrorMessage) - } -} diff --git a/vendor/github.com/writeas/go-writeas/v2/go.mod b/vendor/github.com/writeas/go-writeas/v2/go.mod deleted file mode 100644 index b88b28a..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/writeas/go-writeas/v2 - -go 1.9 - -require ( - code.as/core/socks v1.0.0 - github.com/writeas/impart v1.1.0 -) diff --git a/vendor/github.com/writeas/go-writeas/v2/go.sum b/vendor/github.com/writeas/go-writeas/v2/go.sum deleted file mode 100644 index 3e036d3..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -code.as/core/socks v1.0.0 h1:SPQXNp4SbEwjOAP9VzUahLHak8SDqy5n+9cm9tpjZOs= -code.as/core/socks v1.0.0/go.mod h1:BAXBy5O9s2gmw6UxLqNJcVbWY7C/UPs+801CcSsfWOY= -github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE= -github.com/writeas/impart v1.1.0/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= diff --git a/vendor/github.com/writeas/go-writeas/v2/post.go b/vendor/github.com/writeas/go-writeas/v2/post.go deleted file mode 100644 index 1f8a55b..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/post.go +++ /dev/null @@ -1,330 +0,0 @@ -package writeas - -import ( - "fmt" - "net/http" - "time" -) - -type ( - // Post represents a published Write.as post, whether anonymous, owned by a - // user, or part of a collection. - Post struct { - ID string `json:"id"` - Slug string `json:"slug"` - Token string `json:"token"` - Font string `json:"appearance"` - Language *string `json:"language"` - RTL *bool `json:"rtl"` - Listed bool `json:"listed"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Title string `json:"title"` - Content string `json:"body"` - Views int64 `json:"views"` - Tags []string `json:"tags"` - Images []string `json:"images"` - OwnerName string `json:"owner,omitempty"` - - Collection *Collection `json:"collection,omitempty"` - } - - // OwnedPostParams are, together, fields only the original post author knows. - OwnedPostParams struct { - ID string `json:"id"` - Token string `json:"token,omitempty"` - } - - // PostParams holds values for creating or updating a post. - PostParams struct { - // Parameters only for updating - ID string `json:"-"` - Token string `json:"token,omitempty"` - - // Parameters for creating or updating - Slug string `json:"slug"` - Created *time.Time `json:"created,omitempty"` - Updated *time.Time `json:"updated,omitempty"` - Title string `json:"title,omitempty"` - Content string `json:"body,omitempty"` - Font string `json:"font,omitempty"` - IsRTL *bool `json:"rtl,omitempty"` - Language *string `json:"lang,omitempty"` - - // Parameters only for creating - Crosspost []map[string]string `json:"crosspost,omitempty"` - - // Parameters for collection posts - Collection string `json:"-"` - } - - // PinnedPostParams holds values for pinning a post - PinnedPostParams struct { - ID string `json:"id"` - Position int `json:"position"` - } - - // BatchPostResult contains the post-specific result as part of a larger - // batch operation. - BatchPostResult struct { - ID string `json:"id,omitempty"` - Code int `json:"code,omitempty"` - ErrorMessage string `json:"error_msg,omitempty"` - } - - // ClaimPostResult contains the post-specific result for a request to - // associate a post to an account. - ClaimPostResult struct { - ID string `json:"id,omitempty"` - Code int `json:"code,omitempty"` - ErrorMessage string `json:"error_msg,omitempty"` - Post *Post `json:"post,omitempty"` - } -) - -// GetPost retrieves a published post, returning the Post and any error (in -// user-friendly form) that occurs. See -// https://developer.write.as/docs/api/#retrieve-a-post. -func (c *Client) GetPost(id string) (*Post, error) { - p := &Post{} - env, err := c.get(fmt.Sprintf("/posts/%s", id), p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*Post); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - status := env.Code - - if status == http.StatusOK { - return p, nil - } else if status == http.StatusNotFound { - return nil, fmt.Errorf("Post not found.") - } else if status == http.StatusGone { - return nil, fmt.Errorf("Post unpublished.") - } - return nil, fmt.Errorf("Problem getting post: %d. %s\n", status, env.ErrorMessage) -} - -// CreatePost publishes a new post, returning a user-friendly error if one comes -// up. See https://developer.write.as/docs/api/#publish-a-post. -func (c *Client) CreatePost(sp *PostParams) (*Post, error) { - p := &Post{} - endPre := "" - if sp.Collection != "" { - endPre = "/collections/" + sp.Collection - } - env, err := c.post(endPre+"/posts", sp, p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*Post); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - status := env.Code - if status != http.StatusCreated { - if status == http.StatusBadRequest { - return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) - } - return nil, fmt.Errorf("Problem creating post: %d. %s\n", status, env.ErrorMessage) - } - return p, nil -} - -// UpdatePost updates a published post with the given PostParams. See -// https://developer.write.as/docs/api/#update-a-post. -func (c *Client) UpdatePost(id, token string, sp *PostParams) (*Post, error) { - return c.updatePost("", id, token, sp) -} - -func (c *Client) updatePost(collection, identifier, token string, sp *PostParams) (*Post, error) { - p := &Post{} - endpoint := "/posts/" + identifier - /* - if collection != "" { - endpoint = "/collections/" + collection + endpoint - } else { - sp.Token = token - } - */ - sp.Token = token - env, err := c.put(endpoint, sp, p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*Post); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - status := env.Code - if status != http.StatusOK { - if c.isNotLoggedIn(status) { - return nil, fmt.Errorf("Not authenticated.") - } else if status == http.StatusBadRequest { - return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) - } - return nil, fmt.Errorf("Problem updating post: %d. %s\n", status, env.ErrorMessage) - } - return p, nil -} - -// DeletePost permanently deletes a published post. See -// https://developer.write.as/docs/api/#delete-a-post. -func (c *Client) DeletePost(id, token string) error { - return c.deletePost("", id, token) -} - -func (c *Client) deletePost(collection, identifier, token string) error { - p := map[string]string{} - endpoint := "/posts/" + identifier - /* - if collection != "" { - endpoint = "/collections/" + collection + endpoint - } else { - p["token"] = token - } - */ - p["token"] = token - env, err := c.delete(endpoint, p) - if err != nil { - return err - } - - status := env.Code - if status == http.StatusNoContent { - return nil - } else if c.isNotLoggedIn(status) { - return fmt.Errorf("Not authenticated.") - } else if status == http.StatusBadRequest { - return fmt.Errorf("Bad request: %s", env.ErrorMessage) - } - return fmt.Errorf("Problem deleting post: %d. %s\n", status, env.ErrorMessage) -} - -// ClaimPosts associates anonymous posts with a user / account. -// https://developer.write.as/docs/api/#claim-posts. -func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) { - p := &[]ClaimPostResult{} - env, err := c.post("/posts/claim", sp, p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*[]ClaimPostResult); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - - status := env.Code - if status == http.StatusOK { - return p, nil - } else if c.isNotLoggedIn(status) { - return nil, fmt.Errorf("Not authenticated.") - } else if status == http.StatusBadRequest { - return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) - } else { - return nil, fmt.Errorf("Problem claiming post: %d. %s\n", status, env.ErrorMessage) - } - // TODO: does this also happen with moving posts? -} - -// GetUserPosts retrieves the authenticated user's posts. -// See https://developers.write.as/docs/api/#retrieve-user-39-s-posts -func (c *Client) GetUserPosts() (*[]Post, error) { - p := &[]Post{} - env, err := c.get("/me/posts", p) - if err != nil { - return nil, err - } - - var ok bool - if p, ok = env.Data.(*[]Post); !ok { - return nil, fmt.Errorf("Wrong data returned from API.") - } - status := env.Code - - if status != http.StatusOK { - if c.isNotLoggedIn(status) { - return nil, fmt.Errorf("Not authenticated.") - } - return nil, fmt.Errorf("Problem getting user posts: %d. %s\n", status, env.ErrorMessage) - } - return p, nil -} - -// PinPost pins a post in the given collection. -// See https://developers.write.as/docs/api/#pin-a-post-to-a-collection -func (c *Client) PinPost(alias string, pp *PinnedPostParams) error { - res := &[]BatchPostResult{} - env, err := c.post(fmt.Sprintf("/collections/%s/pin", alias), []*PinnedPostParams{pp}, res) - if err != nil { - return err - } - - var ok bool - if res, ok = env.Data.(*[]BatchPostResult); !ok { - return fmt.Errorf("Wrong data returned from API.") - } - - // Check for basic request errors on top level response - status := env.Code - if status != http.StatusOK { - if c.isNotLoggedIn(status) { - return fmt.Errorf("Not authenticated.") - } - return fmt.Errorf("Problem pinning post: %d. %s\n", status, env.ErrorMessage) - } - - // Check the individual post result - if len(*res) == 0 || len(*res) > 1 { - return fmt.Errorf("Wrong data returned from API.") - } - if (*res)[0].Code != http.StatusOK { - return fmt.Errorf("Problem pinning post: %d", (*res)[0].Code) - // TODO: return ErrorMessage (right now it'll be empty) - // return fmt.Errorf("Problem pinning post: %s", res[0].ErrorMessage) - } - return nil -} - -// UnpinPost unpins a post from the given collection. -// See https://developers.write.as/docs/api/#unpin-a-post-from-a-collection -func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error { - res := &[]BatchPostResult{} - env, err := c.post(fmt.Sprintf("/collections/%s/unpin", alias), []*PinnedPostParams{pp}, res) - if err != nil { - return err - } - - var ok bool - if res, ok = env.Data.(*[]BatchPostResult); !ok { - return fmt.Errorf("Wrong data returned from API.") - } - - // Check for basic request errors on top level response - status := env.Code - if status != http.StatusOK { - if c.isNotLoggedIn(status) { - return fmt.Errorf("Not authenticated.") - } - return fmt.Errorf("Problem unpinning post: %d. %s\n", status, env.ErrorMessage) - } - - // Check the individual post result - if len(*res) == 0 || len(*res) > 1 { - return fmt.Errorf("Wrong data returned from API.") - } - if (*res)[0].Code != http.StatusOK { - return fmt.Errorf("Problem unpinning post: %d", (*res)[0].Code) - // TODO: return ErrorMessage (right now it'll be empty) - // return fmt.Errorf("Problem unpinning post: %s", res[0].ErrorMessage) - } - return nil -} diff --git a/vendor/github.com/writeas/go-writeas/v2/user.go b/vendor/github.com/writeas/go-writeas/v2/user.go deleted file mode 100644 index 5973d9c..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/user.go +++ /dev/null @@ -1,34 +0,0 @@ -package writeas - -import "time" - -type ( - // AuthUser represents a just-authenticated user. It contains information - // that'll only be returned once (now) per user session. - AuthUser struct { - AccessToken string `json:"access_token,omitempty"` - Password string `json:"password,omitempty"` - User *User `json:"user"` - } - - // User represents a registered Write.as user. - User struct { - Username string `json:"username"` - Email string `json:"email"` - Created time.Time `json:"created"` - - // Optional properties - Subscription *UserSubscription `json:"subscription"` - } - - // UserSubscription contains information about a user's Write.as - // subscription. - UserSubscription struct { - Name string `json:"name"` - Begin time.Time `json:"begin"` - End time.Time `json:"end"` - AutoRenew bool `json:"auto_renew"` - Active bool `json:"is_active"` - Delinquent bool `json:"is_delinquent"` - } -) diff --git a/vendor/github.com/writeas/go-writeas/v2/writeas.go b/vendor/github.com/writeas/go-writeas/v2/writeas.go deleted file mode 100644 index fa87ae1..0000000 --- a/vendor/github.com/writeas/go-writeas/v2/writeas.go +++ /dev/null @@ -1,199 +0,0 @@ -// Package writeas provides the binding for the Write.as API -package writeas - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "code.as/core/socks" - "github.com/writeas/impart" -) - -const ( - apiURL = "https://write.as/api" - devAPIURL = "https://development.write.as/api" - torAPIURL = "http://writeas7pm7rcdqg.onion/api" - - // Current go-writeas version - Version = "2-dev" -) - -// Client is used to interact with the Write.as API. It can be used to make -// authenticated or unauthenticated calls. -type Client struct { - baseURL string - - // Access token for the user making requests. - token string - // Client making requests to the API - client *http.Client - - // UserAgent overrides the default User-Agent header - UserAgent string -} - -// defaultHTTPTimeout is the default http.Client timeout. -const defaultHTTPTimeout = 10 * time.Second - -// NewClient creates a new API client. By default, all requests are made -// unauthenticated. To optionally make authenticated requests, call `SetToken`. -// -// c := writeas.NewClient() -// c.SetToken("00000000-0000-0000-0000-000000000000") -func NewClient() *Client { - return NewClientWith(Config{URL: apiURL}) -} - -// NewTorClient creates a new API client for communicating with the Write.as -// Tor hidden service, using the given port to connect to the local SOCKS -// proxy. -func NewTorClient(port int) *Client { - return NewClientWith(Config{URL: torAPIURL, TorPort: port}) -} - -// NewDevClient creates a new API client for development and testing. It'll -// communicate with our development servers, and SHOULD NOT be used in -// production. -func NewDevClient() *Client { - return NewClientWith(Config{URL: devAPIURL}) -} - -// Config configures a Write.as client. -type Config struct { - // URL of the Write.as API service. Defaults to https://write.as/api. - URL string - - // If specified, the API client will communicate with the Write.as Tor - // hidden service using the provided port to connect to the local SOCKS - // proxy. - TorPort int - - // If specified, requests will be authenticated using this user token. - // This may be provided after making a few anonymous requests with - // SetToken. - Token string -} - -// NewClientWith builds a new API client with the provided configuration. -func NewClientWith(c Config) *Client { - if c.URL == "" { - c.URL = apiURL - } - - httpClient := &http.Client{Timeout: defaultHTTPTimeout} - if c.TorPort > 0 { - dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", c.TorPort)) - httpClient.Transport = &http.Transport{Dial: dialSocksProxy} - } - - return &Client{ - client: httpClient, - baseURL: c.URL, - token: c.Token, - } -} - -// SetToken sets the user token for all future Client requests. Setting this to -// an empty string will change back to unauthenticated requests. -func (c *Client) SetToken(token string) { - c.token = token -} - -// Token returns the user token currently set to the Client. -func (c *Client) Token() string { - return c.token -} - -func (c *Client) get(path string, r interface{}) (*impart.Envelope, error) { - method := "GET" - if method != "GET" && method != "HEAD" { - return nil, fmt.Errorf("Method %s not currently supported by library (only HEAD and GET).\n", method) - } - - return c.request(method, path, nil, r) -} - -func (c *Client) post(path string, data, r interface{}) (*impart.Envelope, error) { - b := new(bytes.Buffer) - json.NewEncoder(b).Encode(data) - return c.request("POST", path, b, r) -} - -func (c *Client) put(path string, data, r interface{}) (*impart.Envelope, error) { - b := new(bytes.Buffer) - json.NewEncoder(b).Encode(data) - return c.request("PUT", path, b, r) -} - -func (c *Client) delete(path string, data map[string]string) (*impart.Envelope, error) { - r, err := c.buildRequest("DELETE", path, nil) - if err != nil { - return nil, err - } - - q := r.URL.Query() - for k, v := range data { - q.Add(k, v) - } - r.URL.RawQuery = q.Encode() - - return c.doRequest(r, nil) -} - -func (c *Client) request(method, path string, data io.Reader, result interface{}) (*impart.Envelope, error) { - r, err := c.buildRequest(method, path, data) - if err != nil { - return nil, err - } - - return c.doRequest(r, result) -} - -func (c *Client) buildRequest(method, path string, data io.Reader) (*http.Request, error) { - url := fmt.Sprintf("%s%s", c.baseURL, path) - r, err := http.NewRequest(method, url, data) - if err != nil { - return nil, fmt.Errorf("Create request: %v", err) - } - c.prepareRequest(r) - - return r, nil -} - -func (c *Client) doRequest(r *http.Request, result interface{}) (*impart.Envelope, error) { - resp, err := c.client.Do(r) - if err != nil { - return nil, fmt.Errorf("Request: %v", err) - } - defer resp.Body.Close() - - env := &impart.Envelope{ - Code: resp.StatusCode, - } - if result != nil { - env.Data = result - - err = json.NewDecoder(resp.Body).Decode(&env) - if err != nil { - return nil, err - } - } - - return env, nil -} - -func (c *Client) prepareRequest(r *http.Request) { - ua := c.UserAgent - if ua == "" { - ua = "go-writeas v" + Version - } - r.Header.Set("User-Agent", ua) - r.Header.Add("Content-Type", "application/json") - if c.token != "" { - r.Header.Add("Authorization", "Token "+c.token) - } -} diff --git a/vendor/github.com/writeas/impart/.gitignore b/vendor/github.com/writeas/impart/.gitignore deleted file mode 100644 index 090febd..0000000 --- a/vendor/github.com/writeas/impart/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -*~ -*.swp - -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/vendor/github.com/writeas/impart/LICENSE b/vendor/github.com/writeas/impart/LICENSE deleted file mode 100644 index 7371932..0000000 --- a/vendor/github.com/writeas/impart/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Write.as - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/vendor/github.com/writeas/impart/README.md b/vendor/github.com/writeas/impart/README.md deleted file mode 100644 index 1d1fba6..0000000 --- a/vendor/github.com/writeas/impart/README.md +++ /dev/null @@ -1,61 +0,0 @@ -impart -====== - -![MIT license](https://img.shields.io/github/license/writeas/impart.svg) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Public Slack discussion](http://slack.write.as/badge.svg)](http://slack.write.as/) - -**impart** is a library for the final layer between the API and the consumer. It's used in the latest [Write.as](https://write.as) and [HTMLhouse](https://html.house) APIs. - -We're still in the early stages of development, so there may be breaking changes. - -## Example use - -```go -package main - -import ( - "fmt" - "github.com/writeas/impart" - "net/http" -) - -type handlerFunc func(w http.ResponseWriter, r *http.Request) error - -func main() { - http.HandleFunc("/", handle(index)) - http.ListenAndServe("127.0.0.1:8080", nil) -} - -func index(w http.ResponseWriter, r *http.Request) error { - fmt.Fprintf(w, "Hello world!") - - return nil -} - -func handle(f handlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - handleError(w, r, func() error { - // Do authentication... - - // Handle the request - err := f(w, r) - - // Log the request and result... - - return err - }()) - } -} - -func handleError(w http.ResponseWriter, r *http.Request, err error) { - if err == nil { - return - } - - if err, ok := err.(impart.HTTPError); ok { - impart.WriteError(w, err) - return - } - - impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Internal server error :("}) -} -``` diff --git a/vendor/github.com/writeas/impart/doc.go b/vendor/github.com/writeas/impart/doc.go deleted file mode 100644 index a2e17d1..0000000 --- a/vendor/github.com/writeas/impart/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package impart provides a simple interface for a JSON-based API. It is -// designed for passing errors around a web application, sending back a status -// code and error message if needed, or a status code and some data on success. -package impart diff --git a/vendor/github.com/writeas/impart/errors.go b/vendor/github.com/writeas/impart/errors.go deleted file mode 100644 index ce5e9b9..0000000 --- a/vendor/github.com/writeas/impart/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package impart - -import ( - "net/http" -) - -// HTTPError holds an HTTP status code and an error message. -type HTTPError struct { - Status int - Message string -} - -// Error displays the HTTPError's error message and satisfies the error -// interface. -func (h HTTPError) Error() string { - if h.Message == "" { - return http.StatusText(h.Status) - } - return h.Message -} diff --git a/vendor/github.com/writeas/impart/go.mod b/vendor/github.com/writeas/impart/go.mod deleted file mode 100644 index 5f2e65d..0000000 --- a/vendor/github.com/writeas/impart/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/writeas/impart - -go 1.9 diff --git a/vendor/github.com/writeas/impart/request.go b/vendor/github.com/writeas/impart/request.go deleted file mode 100644 index 0f170a1..0000000 --- a/vendor/github.com/writeas/impart/request.go +++ /dev/null @@ -1,13 +0,0 @@ -package impart - -import ( - "mime" - "net/http" -) - -// ReqJSON returns whether or not the given Request is sending JSON, based on -// the Content-Type header being application/json. -func ReqJSON(r *http.Request) bool { - ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) - return ct == "application/json" -} diff --git a/vendor/github.com/writeas/impart/response.go b/vendor/github.com/writeas/impart/response.go deleted file mode 100644 index fe097bb..0000000 --- a/vendor/github.com/writeas/impart/response.go +++ /dev/null @@ -1,76 +0,0 @@ -package impart - -import ( - "encoding/json" - "net/http" - "strconv" -) - -type ( - // Envelope contains metadata and optional data for a response object. - // Responses will always contain a status code and either: - // - response Data on a 2xx response, or - // - an ErrorMessage on non-2xx responses - // - // ErrorType is not currently used. - Envelope struct { - Code int `json:"code"` - ErrorType string `json:"error_type,omitempty"` - ErrorMessage string `json:"error_msg,omitempty"` - Data interface{} `json:"data,omitempty"` - } -) - -func writeBody(w http.ResponseWriter, body []byte, status int, contentType string) error { - w.Header().Set("Content-Type", contentType+"; charset=UTF-8") - w.Header().Set("Content-Length", strconv.Itoa(len(body))) - w.WriteHeader(status) - _, err := w.Write(body) - return err -} - -func RenderActivityJSON(w http.ResponseWriter, value interface{}, status int) error { - body, err := json.Marshal(value) - if err != nil { - return err - } - return writeBody(w, body, status, "application/activity+json") -} - -func renderJSON(w http.ResponseWriter, value interface{}, status int) error { - body, err := json.Marshal(value) - if err != nil { - return err - } - return writeBody(w, body, status, "application/json") -} - -func renderString(w http.ResponseWriter, status int, msg string) error { - return writeBody(w, []byte(msg), status, "text/plain") -} - -// WriteSuccess writes the successful data and metadata to the ResponseWriter as -// JSON. -func WriteSuccess(w http.ResponseWriter, data interface{}, status int) error { - env := &Envelope{ - Code: status, - Data: data, - } - return renderJSON(w, env, status) -} - -// WriteError writes the error to the ResponseWriter as JSON. -func WriteError(w http.ResponseWriter, e HTTPError) error { - env := &Envelope{ - Code: e.Status, - ErrorMessage: e.Message, - } - return renderJSON(w, env, e.Status) -} - -// WriteRedirect sends a redirect -func WriteRedirect(w http.ResponseWriter, e HTTPError) int { - w.Header().Set("Location", e.Message) - w.WriteHeader(e.Status) - return e.Status -} diff --git a/vendor/github.com/writeas/saturday/.gitignore b/vendor/github.com/writeas/saturday/.gitignore deleted file mode 100644 index 75623dc..0000000 --- a/vendor/github.com/writeas/saturday/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.out -*.swp -*.8 -*.6 -_obj -_test* -markdown -tags diff --git a/vendor/github.com/writeas/saturday/.travis.yml b/vendor/github.com/writeas/saturday/.travis.yml deleted file mode 100644 index a1687f1..0000000 --- a/vendor/github.com/writeas/saturday/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -sudo: false -language: go -go: - - 1.5.4 - - 1.6.2 - - tip -matrix: - include: - - go: 1.2.2 - script: - - go get -t -v ./... - - go test -v -race ./... - - go: 1.3.3 - script: - - go get -t -v ./... - - go test -v -race ./... - - go: 1.4.3 - script: - - go get -t -v ./... - - go test -v -race ./... - allow_failures: - - go: tip - fast_finish: true -install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . - - go test -v -race ./... diff --git a/vendor/github.com/writeas/saturday/LICENSE.txt b/vendor/github.com/writeas/saturday/LICENSE.txt deleted file mode 100644 index 2885af3..0000000 --- a/vendor/github.com/writeas/saturday/LICENSE.txt +++ /dev/null @@ -1,29 +0,0 @@ -Blackfriday is distributed under the Simplified BSD License: - -> Copyright © 2011 Russ Ross -> All rights reserved. -> -> Redistribution and use in source and binary forms, with or without -> modification, are permitted provided that the following conditions -> are met: -> -> 1. Redistributions of source code must retain the above copyright -> notice, this list of conditions and the following disclaimer. -> -> 2. Redistributions in binary form must reproduce the above -> copyright notice, this list of conditions and the following -> disclaimer in the documentation and/or other materials provided with -> the distribution. -> -> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/writeas/saturday/README.md b/vendor/github.com/writeas/saturday/README.md deleted file mode 100644 index 8da1a94..0000000 --- a/vendor/github.com/writeas/saturday/README.md +++ /dev/null @@ -1,284 +0,0 @@ -Saturday -======== -Saturday is a fork of [Blackfriday](https://github.com/russross/blackfriday) used on [Write.as](https://write.as). - -We love Markdown, but aren't a Markdown-only platform. So we've stripped out and modified redundant or potentially frustrating syntax in this library. - -## Changes - -* Made images and links behave like standard Markdown (now they won't render when there are spaces between label/alt-text and URL) 12db6e2f7ebcc5d6d88e5b330e4c6d88b577bc95 -* Only support atx-style headings 32843b3dfc510153e76d8f535a9084fc8e22245a -* Removed smart periods, quotes, angles & backticks 72080d757965efc04255fd25ad97c76ef6f03ea9 -* Only support horizontal rules made of hyphens f75e5c8d41435593b7f24243e5c22b50f2b399b4 -* Only support fenced code blocks, not indented blocks 8223c01e430de7fd35f3c38ef75f802734cc0cfc -* Keep leading spaces in paragraphs 24845d212205e789fe24ec27ebc1c4cd121523c9 - -Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) [![GoDoc](https://godoc.org/github.com/russross/blackfriday?status.svg)](https://godoc.org/github.com/russross/blackfriday) ------------ - -Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It -is paranoid about its input (so you can safely feed it user-supplied -data), it is fast, it supports common extensions (tables, smart -punctuation substitutions, etc.), and it is safe for all utf-8 -(unicode) input. - -HTML output is currently supported, along with Smartypants -extensions. An experimental LaTeX output engine is also included. - -It started as a translation from C of [Sundown][3]. - - -### Installation - -Blackfriday is compatible with Go 1. If you are using an older -release of Go, consider using v1.1 of blackfriday, which was based -on the last stable release of Go prior to Go 1. You can find it as a -tagged commit on github. - -With Go 1 and git installed: - - go get github.com/russross/blackfriday - -will download, compile, and install the package into your `$GOPATH` -directory hierarchy. Alternatively, you can achieve the same if you -import it into a project: - - import "github.com/russross/blackfriday" - -and `go get` without parameters. - -### Usage - -For basic usage, it is as simple as getting your input into a byte -slice and calling: - - output := blackfriday.MarkdownBasic(input) - -This renders it with no extensions enabled. To get a more useful -feature set, use this instead: - - output := blackfriday.MarkdownCommon(input) - -#### Sanitize untrusted content - -Blackfriday itself does nothing to protect against malicious content. If you are -dealing with user-supplied markdown, we recommend running blackfriday's output -through HTML sanitizer such as -[Bluemonday](https://github.com/microcosm-cc/bluemonday). - -Here's an example of simple usage of blackfriday together with bluemonday: - -``` go -import ( - "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday" -) - -// ... -unsafe := blackfriday.MarkdownCommon(input) -html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) -``` - -#### Custom options - -If you want to customize the set of options, first get a renderer -(currently either the HTML or LaTeX output engines), then use it to -call the more general `Markdown` function. For examples, see the -implementations of `MarkdownBasic` and `MarkdownCommon` in -`markdown.go`. - -You can also check out `blackfriday-tool` for a more complete example -of how to use it. Download and install it using: - - go get github.com/russross/blackfriday-tool - -This is a simple command-line tool that allows you to process a -markdown file using a standalone program. You can also browse the -source directly on github if you are just looking for some example -code: - -* - -Note that if you have not already done so, installing -`blackfriday-tool` will be sufficient to download and install -blackfriday in addition to the tool itself. The tool binary will be -installed in `$GOPATH/bin`. This is a statically-linked binary that -can be copied to wherever you need it without worrying about -dependencies and library versions. - - -### Features - -All features of Sundown are supported, including: - -* **Compatibility**. The Markdown v1.0.3 test suite passes with - the `--tidy` option. Without `--tidy`, the differences are - mostly in whitespace and entity escaping, where blackfriday is - more consistent and cleaner. - -* **Common extensions**, including table support, fenced code - blocks, autolinks, strikethroughs, non-strict emphasis, etc. - -* **Safety**. Blackfriday is paranoid when parsing, making it safe - to feed untrusted user input without fear of bad things - happening. The test suite stress tests this and there are no - known inputs that make it crash. If you find one, please let me - know and send me the input that does it. - - NOTE: "safety" in this context means *runtime safety only*. In order to - protect yourself against JavaScript injection in untrusted content, see - [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). - -* **Fast processing**. It is fast enough to render on-demand in - most web applications without having to cache the output. - -* **Thread safety**. You can run multiple parsers in different - goroutines without ill effect. There is no dependence on global - shared state. - -* **Minimal dependencies**. Blackfriday only depends on standard - library packages in Go. The source code is pretty - self-contained, so it is easy to add to any project, including - Google App Engine projects. - -* **Standards compliant**. Output successfully validates using the - W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. - - -### Extensions - -In addition to the standard markdown syntax, this package -implements the following extensions: - -* **Intra-word emphasis supression**. The `_` character is - commonly used inside words when discussing code, so having - markdown interpret it as an emphasis command is usually the - wrong thing. Blackfriday lets you treat all emphasis markers as - normal characters when they occur inside a word. - -* **Tables**. Tables can be created by drawing them in the input - using a simple syntax: - - ``` - Name | Age - --------|------ - Bob | 27 - Alice | 23 - ``` - -* **Fenced code blocks**. In addition to the normal 4-space - indentation to mark code blocks, you can explicitly mark them - and supply a language (to make syntax highlighting simple). Just - mark it like this: - - ``` go - func getTrue() bool { - return true - } - ``` - - You can use 3 or more backticks to mark the beginning of the - block, and the same number to mark the end of the block. - - To preserve classes of fenced code blocks while using the bluemonday - HTML sanitizer, use the following policy: - - ``` go - p := bluemonday.UGCPolicy() - p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") - html := p.SanitizeBytes(unsafe) - ``` - -* **Definition lists**. A simple definition list is made of a single-line - term followed by a colon and the definition for that term. - - Cat - : Fluffy animal everyone likes - - Internet - : Vector of transmission for pictures of cats - - Terms must be separated from the previous definition by a blank line. - -* **Footnotes**. A marker in the text that will become a superscript number; - a footnote definition that will be placed in a list of footnotes at the - end of the document. A footnote looks like this: - - This is a footnote.[^1] - - [^1]: the footnote text. - -* **Autolinking**. Blackfriday can find URLs that have not been - explicitly marked as links and turn them into links. - -* **Strikethrough**. Use two tildes (`~~`) to mark text that - should be crossed out. - -* **Hard line breaks**. With this extension enabled (it is off by - default in the `MarkdownBasic` and `MarkdownCommon` convenience - functions), newlines in the input translate into line breaks in - the output. - -* **Smart quotes**. Smartypants-style punctuation substitution is - supported, turning normal double- and single-quote marks into - curly quotes, etc. - -* **LaTeX-style dash parsing** is an additional option, where `--` - is translated into `–`, and `---` is translated into - `—`. This differs from most smartypants processors, which - turn a single hyphen into an ndash and a double hyphen into an - mdash. - -* **Smart fractions**, where anything that looks like a fraction - is translated into suitable HTML (instead of just a few special - cases like most smartypant processors). For example, `4/5` - becomes `45`, which renders as - 45. - - -### Other renderers - -Blackfriday is structured to allow alternative rendering engines. Here -are a few of note: - -* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): - provides a GitHub Flavored Markdown renderer with fenced code block - highlighting, clickable header anchor links. - - It's not customizable, and its goal is to produce HTML output - equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), - except the rendering is performed locally. - -* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, - but for markdown. - -* LaTeX output: renders output as LaTeX. This is currently part of the - main Blackfriday repository, but may be split into its own project - in the future. If you are interested in owning and maintaining the - LaTeX output component, please be in touch. - - It renders some basic documents, but is only experimental at this - point. In particular, it does not do any inline escaping, so input - that happens to look like LaTeX code will be passed through without - modification. - -* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format. - - -### Todo - -* More unit testing -* Improve unicode support. It does not understand all unicode - rules (about what constitutes a letter, a punctuation symbol, - etc.), so it may fail to detect word boundaries correctly in - some instances. It is safe on all utf-8 input. - - -### License - -[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) - - - [1]: http://daringfireball.net/projects/markdown/ "Markdown" - [2]: http://golang.org/ "Go Language" - [3]: https://github.com/vmg/sundown "Sundown" diff --git a/vendor/github.com/writeas/saturday/block.go b/vendor/github.com/writeas/saturday/block.go deleted file mode 100644 index eafb67c..0000000 --- a/vendor/github.com/writeas/saturday/block.go +++ /dev/null @@ -1,1412 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// Functions to parse block-level elements. -// - -package blackfriday - -import ( - "bytes" - - "github.com/shurcooL/sanitized_anchor_name" -) - -// Parse block-level data. -// Note: this function and many that it calls assume that -// the input buffer ends with a newline. -func (p *parser) block(out *bytes.Buffer, data []byte) { - if len(data) == 0 || data[len(data)-1] != '\n' { - panic("block input is missing terminating newline") - } - - // this is called recursively: enforce a maximum depth - if p.nesting >= p.maxNesting { - return - } - p.nesting++ - - // parse out one block-level construct at a time - for len(data) > 0 { - // prefixed header: - // - // # Header 1 - // ## Header 2 - // ... - // ###### Header 6 - if p.isPrefixHeader(data) { - data = data[p.prefixHeader(out, data):] - continue - } - - // block of preformatted HTML: - // - //
- // ... - //
- if data[0] == '<' { - if i := p.html(out, data, true); i > 0 { - data = data[i:] - continue - } - } - - // title block - // - // % stuff - // % more stuff - // % even more stuff - if p.flags&EXTENSION_TITLEBLOCK != 0 { - if data[0] == '%' { - if i := p.titleBlock(out, data, true); i > 0 { - data = data[i:] - continue - } - } - } - - // blank lines. note: returns the # of bytes to skip - if i := p.isEmpty(data); i > 0 { - data = data[i:] - continue - } - - // fenced code block: - // - // ``` go - // func fact(n int) int { - // if n <= 1 { - // return n - // } - // return n * fact(n-1) - // } - // ``` - if p.flags&EXTENSION_FENCED_CODE != 0 { - if i := p.fencedCodeBlock(out, data, true); i > 0 { - data = data[i:] - continue - } - } - - // horizontal rule: - // - // ------ - if p.isHRule(data) { - p.r.HRule(out) - var i int - for i = 0; data[i] != '\n'; i++ { - } - data = data[i:] - continue - } - - // block quote: - // - // > A big quote I found somewhere - // > on the web - if p.quotePrefix(data) > 0 { - data = data[p.quote(out, data):] - continue - } - - // table: - // - // Name | Age | Phone - // ------|-----|--------- - // Bob | 31 | 555-1234 - // Alice | 27 | 555-4321 - if p.flags&EXTENSION_TABLES != 0 { - if i := p.table(out, data); i > 0 { - data = data[i:] - continue - } - } - - // an itemized/unordered list: - // - // * Item 1 - // * Item 2 - // - // also works with + or - - if p.uliPrefix(data) > 0 { - data = data[p.list(out, data, 0):] - continue - } - - // a numbered/ordered list: - // - // 1. Item 1 - // 2. Item 2 - if p.oliPrefix(data) > 0 { - data = data[p.list(out, data, LIST_TYPE_ORDERED):] - continue - } - - // definition lists: - // - // Term 1 - // : Definition a - // : Definition b - // - // Term 2 - // : Definition c - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { - if p.dliPrefix(data) > 0 { - data = data[p.list(out, data, LIST_TYPE_DEFINITION):] - continue - } - } - - // anything else must look like a normal paragraph - // note: this finds underlined headers, too - data = data[p.paragraph(out, data):] - } - - p.nesting-- -} - -func (p *parser) isPrefixHeader(data []byte) bool { - if data[0] != '#' { - return false - } - - if p.flags&EXTENSION_SPACE_HEADERS != 0 { - level := 0 - for level < 6 && data[level] == '#' { - level++ - } - if data[level] != ' ' { - return false - } - } - return true -} - -func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { - level := 0 - for level < 6 && data[level] == '#' { - level++ - } - i := skipChar(data, level, ' ') - end := skipUntilChar(data, i, '\n') - skip := end - id := "" - if p.flags&EXTENSION_HEADER_IDS != 0 { - j, k := 0, 0 - // find start/end of header id - for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { - } - for k = j + 1; k < end && data[k] != '}'; k++ { - } - // extract header id iff found - if j < end && k < end { - id = string(data[j+2 : k]) - end = j - skip = k + 1 - for end > 0 && data[end-1] == ' ' { - end-- - } - } - } - for end > 0 && data[end-1] == '#' { - if isBackslashEscaped(data, end-1) { - break - } - end-- - } - for end > 0 && data[end-1] == ' ' { - end-- - } - if end > i { - if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { - id = sanitized_anchor_name.Create(string(data[i:end])) - } - work := func() bool { - p.inline(out, data[i:end]) - return true - } - p.r.Header(out, work, level, id) - } - return skip -} - -func (p *parser) isUnderlinedHeader(data []byte) int { - // test of level 1 header - if data[0] == '=' { - i := skipChar(data, 1, '=') - i = skipChar(data, i, ' ') - if data[i] == '\n' { - return 1 - } else { - return 0 - } - } - - // test of level 2 header - if data[0] == '-' { - i := skipChar(data, 1, '-') - i = skipChar(data, i, ' ') - if data[i] == '\n' { - return 2 - } else { - return 0 - } - } - - return 0 -} - -func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { - if data[0] != '%' { - return 0 - } - splitData := bytes.Split(data, []byte("\n")) - var i int - for idx, b := range splitData { - if !bytes.HasPrefix(b, []byte("%")) { - i = idx // - 1 - break - } - } - - data = bytes.Join(splitData[0:i], []byte("\n")) - p.r.TitleBlock(out, data) - - return len(data) -} - -func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { - var i, j int - - // identify the opening tag - if data[0] != '<' { - return 0 - } - curtag, tagfound := p.htmlFindTag(data[1:]) - - // handle special cases - if !tagfound { - // check for an HTML comment - if size := p.htmlComment(out, data, doRender); size > 0 { - return size - } - - // check for an
tag - if size := p.htmlHr(out, data, doRender); size > 0 { - return size - } - - // check for HTML CDATA - if size := p.htmlCDATA(out, data, doRender); size > 0 { - return size - } - - // no special case recognized - return 0 - } - - // look for an unindented matching closing tag - // followed by a blank line - found := false - /* - closetag := []byte("\n") - j = len(curtag) + 1 - for !found { - // scan for a closing tag at the beginning of a line - if skip := bytes.Index(data[j:], closetag); skip >= 0 { - j += skip + len(closetag) - } else { - break - } - - // see if it is the only thing on the line - if skip := p.isEmpty(data[j:]); skip > 0 { - // see if it is followed by a blank line/eof - j += skip - if j >= len(data) { - found = true - i = j - } else { - if skip := p.isEmpty(data[j:]); skip > 0 { - j += skip - found = true - i = j - } - } - } - } - */ - - // if not found, try a second pass looking for indented match - // but not if tag is "ins" or "del" (following original Markdown.pl) - if !found && curtag != "ins" && curtag != "del" { - i = 1 - for i < len(data) { - i++ - for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { - i++ - } - - if i+2+len(curtag) >= len(data) { - break - } - - j = p.htmlFindEnd(curtag, data[i-1:]) - - if j > 0 { - i += j - 1 - found = true - break - } - } - } - - if !found { - return 0 - } - - // the end of the block has been found - if doRender { - // trim newlines - end := i - for end > 0 && data[end-1] == '\n' { - end-- - } - p.r.BlockHtml(out, data[:end]) - } - - return i -} - -func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { - // html block needs to end with a blank line - if i := p.isEmpty(data[start:]); i > 0 { - size := start + i - if doRender { - // trim trailing newlines - end := size - for end > 0 && data[end-1] == '\n' { - end-- - } - p.r.BlockHtml(out, data[:end]) - } - return size - } - return 0 -} - -// HTML comment, lax form -func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { - i := p.inlineHTMLComment(out, data) - return p.renderHTMLBlock(out, data, i, doRender) -} - -// HTML CDATA section -func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { - const cdataTag = "') { - i++ - } - i++ - // no end-of-comment marker - if i >= len(data) { - return 0 - } - return p.renderHTMLBlock(out, data, i, doRender) -} - -// HR, which is the only self-closing block tag considered -func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { - if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { - return 0 - } - if data[3] != ' ' && data[3] != '/' && data[3] != '>' { - // not an
tag after all; at least not a valid one - return 0 - } - - i := 3 - for data[i] != '>' && data[i] != '\n' { - i++ - } - - if data[i] == '>' { - return p.renderHTMLBlock(out, data, i+1, doRender) - } - - return 0 -} - -func (p *parser) htmlFindTag(data []byte) (string, bool) { - i := 0 - for isalnum(data[i]) { - i++ - } - key := string(data[:i]) - if _, ok := blockTags[key]; ok { - return key, true - } - return "", false -} - -func (p *parser) htmlFindEnd(tag string, data []byte) int { - // assume data[0] == '<' && data[1] == '/' already tested - - // check if tag is a match - closetag := []byte("") - if !bytes.HasPrefix(data, closetag) { - return 0 - } - i := len(closetag) - - // check that the rest of the line is blank - skip := 0 - if skip = p.isEmpty(data[i:]); skip == 0 { - return 0 - } - i += skip - skip = 0 - - if i >= len(data) { - return i - } - - if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { - return i - } - if skip = p.isEmpty(data[i:]); skip == 0 { - // following line must be blank - return 0 - } - - return i + skip -} - -func (*parser) isEmpty(data []byte) int { - // it is okay to call isEmpty on an empty buffer - if len(data) == 0 { - return 0 - } - - var i int - for i = 0; i < len(data) && data[i] != '\n'; i++ { - if data[i] != ' ' && data[i] != '\t' { - return 0 - } - } - return i + 1 -} - -func (*parser) isHRule(data []byte) bool { - i := 0 - - // skip up to three spaces - for i < 3 && data[i] == ' ' { - i++ - } - - // character must be a hyphen, otherwise not HR - if data[i] != '-' { - return false - } - c := data[i] - - // the whole line must be the char or whitespace - n := 0 - for data[i] != '\n' { - switch { - case data[i] == c: - n++ - case data[i] != ' ': - return false - } - i++ - } - - return n >= 3 -} - -// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, -// and returns the end index if so, or 0 otherwise. It also returns the marker found. -// If syntax is not nil, it gets set to the syntax specified in the fence line. -// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. -func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) { - i, size := 0, 0 - - // skip up to three spaces - for i < len(data) && i < 3 && data[i] == ' ' { - i++ - } - - // check for the marker characters: ~ or ` - if i >= len(data) { - return 0, "" - } - if data[i] != '~' && data[i] != '`' { - return 0, "" - } - - c := data[i] - - // the whole line must be the same char or whitespace - for i < len(data) && data[i] == c { - size++ - i++ - } - - // the marker char must occur at least 3 times - if size < 3 { - return 0, "" - } - marker = string(data[i-size : i]) - - // if this is the end marker, it must match the beginning marker - if oldmarker != "" && marker != oldmarker { - return 0, "" - } - - // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 - i = skipChar(data, i, ' ') - - if i >= len(data) { - if newlineOptional && i == len(data) { - return i, marker - } - return 0, "" - } - - syntaxStart := i - - if data[i] == '{' { - i++ - syntaxStart++ - - for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ - i++ - } - - if i >= len(data) || data[i] != '}' { - return 0, "" - } - - // strip all whitespace at the beginning and the end - // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- - } - - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- - } - - i++ - } else { - for i < len(data) && !isspace(data[i]) { - syn++ - i++ - } - } - - *syntax = string(data[syntaxStart : syntaxStart+syn]) - } - - i = skipChar(data, i, ' ') - if i >= len(data) || data[i] != '\n' { - if newlineOptional && i == len(data) { - return i, marker - } - return 0, "" - } - - return i + 1, marker // Take newline into account. -} - -// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, -// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. -// If doRender is true, a final newline is mandatory to recognize the fenced code block. -func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "", false) - if beg == 0 || beg >= len(data) { - return 0 - } - - var work bytes.Buffer - - for { - // safe to assume beg < len(data) - - // check for the end of the code block - newlineOptional := !doRender - fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) - if fenceEnd != 0 { - beg += fenceEnd - break - } - - // copy the current line - end := skipUntilChar(data, beg, '\n') + 1 - - // did we reach the end of the buffer without a closing marker? - if end >= len(data) { - return 0 - } - - // verbatim copy to the working buffer - if doRender { - work.Write(data[beg:end]) - } - beg = end - } - - if doRender { - p.r.BlockCode(out, work.Bytes(), syntax) - } - - return beg -} - -func (p *parser) table(out *bytes.Buffer, data []byte) int { - var header bytes.Buffer - i, columns := p.tableHeader(&header, data) - if i == 0 { - return 0 - } - - var body bytes.Buffer - - for i < len(data) { - pipes, rowStart := 0, i - for ; data[i] != '\n'; i++ { - if data[i] == '|' { - pipes++ - } - } - - if pipes == 0 { - i = rowStart - break - } - - // include the newline in data sent to tableRow - i++ - p.tableRow(&body, data[rowStart:i], columns, false) - } - - p.r.Table(out, header.Bytes(), body.Bytes(), columns) - - return i -} - -// check if the specified position is preceded by an odd number of backslashes -func isBackslashEscaped(data []byte, i int) bool { - backslashes := 0 - for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { - backslashes++ - } - return backslashes&1 == 1 -} - -func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { - i := 0 - colCount := 1 - for i = 0; data[i] != '\n'; i++ { - if data[i] == '|' && !isBackslashEscaped(data, i) { - colCount++ - } - } - - // doesn't look like a table header - if colCount == 1 { - return - } - - // include the newline in the data sent to tableRow - header := data[:i+1] - - // column count ignores pipes at beginning or end of line - if data[0] == '|' { - colCount-- - } - if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { - colCount-- - } - - columns = make([]int, colCount) - - // move on to the header underline - i++ - if i >= len(data) { - return - } - - if data[i] == '|' && !isBackslashEscaped(data, i) { - i++ - } - i = skipChar(data, i, ' ') - - // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 - // and trailing | optional on last column - col := 0 - for data[i] != '\n' { - dashes := 0 - - if data[i] == ':' { - i++ - columns[col] |= TABLE_ALIGNMENT_LEFT - dashes++ - } - for data[i] == '-' { - i++ - dashes++ - } - if data[i] == ':' { - i++ - columns[col] |= TABLE_ALIGNMENT_RIGHT - dashes++ - } - for data[i] == ' ' { - i++ - } - - // end of column test is messy - switch { - case dashes < 3: - // not a valid column - return - - case data[i] == '|' && !isBackslashEscaped(data, i): - // marker found, now skip past trailing whitespace - col++ - i++ - for data[i] == ' ' { - i++ - } - - // trailing junk found after last column - if col >= colCount && data[i] != '\n' { - return - } - - case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: - // something else found where marker was required - return - - case data[i] == '\n': - // marker is optional for the last column - col++ - - default: - // trailing junk found after last column - return - } - } - if col != colCount { - return - } - - p.tableRow(out, header, columns, true) - size = i + 1 - return -} - -func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { - i, col := 0, 0 - var rowWork bytes.Buffer - - if data[i] == '|' && !isBackslashEscaped(data, i) { - i++ - } - - for col = 0; col < len(columns) && i < len(data); col++ { - for data[i] == ' ' { - i++ - } - - cellStart := i - - for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { - i++ - } - - cellEnd := i - - // skip the end-of-cell marker, possibly taking us past end of buffer - i++ - - for cellEnd > cellStart && data[cellEnd-1] == ' ' { - cellEnd-- - } - - var cellWork bytes.Buffer - p.inline(&cellWork, data[cellStart:cellEnd]) - - if header { - p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) - } else { - p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) - } - } - - // pad it out with empty columns to get the right number - for ; col < len(columns); col++ { - if header { - p.r.TableHeaderCell(&rowWork, nil, columns[col]) - } else { - p.r.TableCell(&rowWork, nil, columns[col]) - } - } - - // silently ignore rows with too many cells - - p.r.TableRow(out, rowWork.Bytes()) -} - -// returns blockquote prefix length -func (p *parser) quotePrefix(data []byte) int { - i := 0 - for i < 3 && data[i] == ' ' { - i++ - } - if data[i] == '>' { - if data[i+1] == ' ' { - return i + 2 - } - return i + 1 - } - return 0 -} - -// blockquote ends with at least one blank line -// followed by something without a blockquote prefix -func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { - if p.isEmpty(data[beg:]) <= 0 { - return false - } - if end >= len(data) { - return true - } - return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 -} - -// parse a blockquote fragment -func (p *parser) quote(out *bytes.Buffer, data []byte) int { - var raw bytes.Buffer - beg, end := 0, 0 - for beg < len(data) { - end = beg - // Step over whole lines, collecting them. While doing that, check for - // fenced code and if one's found, incorporate it altogether, - // irregardless of any contents inside it - for data[end] != '\n' { - if p.flags&EXTENSION_FENCED_CODE != 0 { - if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { - // -1 to compensate for the extra end++ after the loop: - end += i - 1 - break - } - } - end++ - } - end++ - - if pre := p.quotePrefix(data[beg:]); pre > 0 { - // skip the prefix - beg += pre - } else if p.terminateBlockquote(data, beg, end) { - break - } - - // this line is part of the blockquote - raw.Write(data[beg:end]) - beg = end - } - - var cooked bytes.Buffer - p.block(&cooked, raw.Bytes()) - p.r.BlockQuote(out, cooked.Bytes()) - return end -} - -// returns prefix length for block code -func (p *parser) codePrefix(data []byte) int { - if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { - return 4 - } - return 0 -} - -func (p *parser) code(out *bytes.Buffer, data []byte) int { - var work bytes.Buffer - - i := 0 - for i < len(data) { - beg := i - for data[i] != '\n' { - i++ - } - i++ - - blankline := p.isEmpty(data[beg:i]) > 0 - if pre := p.codePrefix(data[beg:i]); pre > 0 { - beg += pre - } else if !blankline { - // non-empty, non-prefixed line breaks the pre - i = beg - break - } - - // verbatim copy to the working buffeu - if blankline { - work.WriteByte('\n') - } else { - work.Write(data[beg:i]) - } - } - - // trim all the \n off the end of work - workbytes := work.Bytes() - eol := len(workbytes) - for eol > 0 && workbytes[eol-1] == '\n' { - eol-- - } - if eol != len(workbytes) { - work.Truncate(eol) - } - - work.WriteByte('\n') - - p.r.BlockCode(out, work.Bytes(), "") - - return i -} - -// returns unordered list item prefix -func (p *parser) uliPrefix(data []byte) int { - i := 0 - - // start with up to 3 spaces - for i < 3 && data[i] == ' ' { - i++ - } - - // need a *, +, or - followed by a space - if (data[i] != '*' && data[i] != '+' && data[i] != '-') || - data[i+1] != ' ' { - return 0 - } - return i + 2 -} - -// returns ordered list item prefix -func (p *parser) oliPrefix(data []byte) int { - i := 0 - - // start with up to 3 spaces - for i < 3 && data[i] == ' ' { - i++ - } - - // count the digits - start := i - for data[i] >= '0' && data[i] <= '9' { - i++ - } - - // we need >= 1 digits followed by a dot and a space - if start == i || data[i] != '.' || data[i+1] != ' ' { - return 0 - } - return i + 2 -} - -// returns definition list item prefix -func (p *parser) dliPrefix(data []byte) int { - i := 0 - - // need a : followed by a spaces - if data[i] != ':' || data[i+1] != ' ' { - return 0 - } - for data[i] == ' ' { - i++ - } - return i + 2 -} - -// parse ordered or unordered list block -func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { - i := 0 - flags |= LIST_ITEM_BEGINNING_OF_LIST - work := func() bool { - for i < len(data) { - skip := p.listItem(out, data[i:], &flags) - i += skip - - if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { - break - } - flags &= ^LIST_ITEM_BEGINNING_OF_LIST - } - return true - } - - p.r.List(out, work, flags) - return i -} - -// Parse a single list item. -// Assumes initial prefix is already removed if this is a sublist. -func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { - // keep track of the indentation of the first line - itemIndent := 0 - for itemIndent < 3 && data[itemIndent] == ' ' { - itemIndent++ - } - - i := p.uliPrefix(data) - if i == 0 { - i = p.oliPrefix(data) - } - if i == 0 { - i = p.dliPrefix(data) - // reset definition term flag - if i > 0 { - *flags &= ^LIST_TYPE_TERM - } - } - if i == 0 { - // if in defnition list, set term flag and continue - if *flags&LIST_TYPE_DEFINITION != 0 { - *flags |= LIST_TYPE_TERM - } else { - return 0 - } - } - - // skip leading whitespace on first line - for data[i] == ' ' { - i++ - } - - // find the end of the line - line := i - for i > 0 && data[i-1] != '\n' { - i++ - } - - // get working buffer - var raw bytes.Buffer - - // put the first line into the working buffer - raw.Write(data[line:i]) - line = i - - // process the following lines - containsBlankLine := false - sublist := 0 - -gatherlines: - for line < len(data) { - i++ - - // find the end of this line - for data[i-1] != '\n' { - i++ - } - - // if it is an empty line, guess that it is part of this item - // and move on to the next line - if p.isEmpty(data[line:i]) > 0 { - containsBlankLine = true - raw.Write(data[line:i]) - line = i - continue - } - - // calculate the indentation - indent := 0 - for indent < 4 && line+indent < i && data[line+indent] == ' ' { - indent++ - } - - chunk := data[line+indent : i] - - // evaluate how this line fits in - switch { - // is this a nested list item? - case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || - p.oliPrefix(chunk) > 0 || - p.dliPrefix(chunk) > 0: - - if containsBlankLine { - // end the list if the type changed after a blank line - if indent <= itemIndent && - ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || - (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { - - *flags |= LIST_ITEM_END_OF_LIST - break gatherlines - } - *flags |= LIST_ITEM_CONTAINS_BLOCK - } - - // to be a nested list, it must be indented more - // if not, it is the next item in the same list - if indent <= itemIndent { - break gatherlines - } - - // is this the first item in the nested list? - if sublist == 0 { - sublist = raw.Len() - } - - // is this a nested prefix header? - case p.isPrefixHeader(chunk): - // if the header is not indented, it is not nested in the list - // and thus ends the list - if containsBlankLine && indent < 4 { - *flags |= LIST_ITEM_END_OF_LIST - break gatherlines - } - *flags |= LIST_ITEM_CONTAINS_BLOCK - - // anything following an empty line is only part - // of this item if it is indented 4 spaces - // (regardless of the indentation of the beginning of the item) - case containsBlankLine && indent < 4: - if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { - // is the next item still a part of this list? - next := i - for data[next] != '\n' { - next++ - } - for next < len(data)-1 && data[next] == '\n' { - next++ - } - if i < len(data)-1 && data[i] != ':' && data[next] != ':' { - *flags |= LIST_ITEM_END_OF_LIST - } - } else { - *flags |= LIST_ITEM_END_OF_LIST - } - break gatherlines - - // a blank line means this should be parsed as a block - case containsBlankLine: - *flags |= LIST_ITEM_CONTAINS_BLOCK - } - - containsBlankLine = false - - // add the line into the working buffer without prefix - raw.Write(data[line+indent : i]) - - line = i - } - - // If reached end of data, the Renderer.ListItem call we're going to make below - // is definitely the last in the list. - if line >= len(data) { - *flags |= LIST_ITEM_END_OF_LIST - } - - rawBytes := raw.Bytes() - - // render the contents of the list item - var cooked bytes.Buffer - if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { - // intermediate render of block item, except for definition term - if sublist > 0 { - p.block(&cooked, rawBytes[:sublist]) - p.block(&cooked, rawBytes[sublist:]) - } else { - p.block(&cooked, rawBytes) - } - } else { - // intermediate render of inline item - if sublist > 0 { - p.inline(&cooked, rawBytes[:sublist]) - p.block(&cooked, rawBytes[sublist:]) - } else { - p.inline(&cooked, rawBytes) - } - } - - // render the actual list item - cookedBytes := cooked.Bytes() - parsedEnd := len(cookedBytes) - - // strip trailing newlines - for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { - parsedEnd-- - } - p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) - - return line -} - -// render a single paragraph that has already been parsed out -func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { - if len(data) == 0 { - return - } - - beg := 0 - - // trim trailing newline - end := len(data) - 1 - - // trim trailing spaces - for end > beg && data[end-1] == ' ' { - end-- - } - - work := func() bool { - p.inline(out, data[beg:end]) - return true - } - p.r.Paragraph(out, work) -} - -func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { - // prev: index of 1st char of previous line - // line: index of 1st char of current line - // i: index of cursor/end of current line - var prev, line, i int - - // keep going until we find something to mark the end of the paragraph - for i < len(data) { - // mark the beginning of the current line - prev = line - current := data[i:] - line = i - - // did we find a blank line marking the end of the paragraph? - if n := p.isEmpty(current); n > 0 { - // did this blank line followed by a definition list item? - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { - if i < len(data)-1 && data[i+1] == ':' { - return p.list(out, data[prev:], LIST_TYPE_DEFINITION) - } - } - - p.renderParagraph(out, data[:i]) - return i + n - } - - // an underline under some text marks a header, so our paragraph ended on prev line - // -- But we don't want Setext-style headers on Write.as. atx is great. - /* - if i > 0 { - if level := p.isUnderlinedHeader(current); level > 0 { - // render the paragraph - p.renderParagraph(out, data[:prev]) - - // ignore leading and trailing whitespace - eol := i - 1 - for prev < eol && data[prev] == ' ' { - prev++ - } - for eol > prev && data[eol-1] == ' ' { - eol-- - } - - // render the header - // this ugly double closure avoids forcing variables onto the heap - work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { - return func() bool { - pp.inline(o, d) - return true - } - }(out, p, data[prev:eol]) - - id := "" - if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { - id = sanitized_anchor_name.Create(string(data[prev:eol])) - } - - p.r.Header(out, work, level, id) - - // find the end of the underline - for data[i] != '\n' { - i++ - } - return i - } - } - */ - - // if the next line starts a block of HTML, then the paragraph ends here - if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { - if data[i] == '<' && p.html(out, current, false) > 0 { - // rewind to before the HTML block - p.renderParagraph(out, data[:i]) - return i - } - } - - // if there's a prefixed header or a horizontal rule after this, paragraph is over - if p.isPrefixHeader(current) || p.isHRule(current) { - p.renderParagraph(out, data[:i]) - return i - } - - // if there's a fenced code block, paragraph is over - if p.flags&EXTENSION_FENCED_CODE != 0 { - if p.fencedCodeBlock(out, current, false) > 0 { - p.renderParagraph(out, data[:i]) - return i - } - } - - // if there's a definition list item, prev line is a definition term - if p.flags&EXTENSION_DEFINITION_LISTS != 0 { - if p.dliPrefix(current) != 0 { - return p.list(out, data[prev:], LIST_TYPE_DEFINITION) - } - } - - // if there's a list after this, paragraph is over - if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { - if p.uliPrefix(current) != 0 || - p.oliPrefix(current) != 0 || - p.quotePrefix(current) != 0 || - p.codePrefix(current) != 0 { - p.renderParagraph(out, data[:i]) - return i - } - } - - // otherwise, scan to the beginning of the next line - for data[i] != '\n' { - i++ - } - i++ - } - - p.renderParagraph(out, data[:i]) - return i -} diff --git a/vendor/github.com/writeas/saturday/html.go b/vendor/github.com/writeas/saturday/html.go deleted file mode 100644 index 74e67ee..0000000 --- a/vendor/github.com/writeas/saturday/html.go +++ /dev/null @@ -1,949 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// -// HTML rendering backend -// -// - -package blackfriday - -import ( - "bytes" - "fmt" - "regexp" - "strconv" - "strings" -) - -// Html renderer configuration options. -const ( - HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks - HTML_SKIP_STYLE // skip embedded