From 910b09274ea3b657d6efafe0252e85d734e111f7 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sat, 29 Sep 2018 12:32:13 -0400 Subject: [PATCH 001/125] 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 002/125] 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 bf389561d6286c46d96d6aa415bb9cffe39efde3 Mon Sep 17 00:00:00 2001 From: Rob Loranger Date: Wed, 29 May 2019 13:55:44 -0700 Subject: [PATCH 003/125] 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 004/125] 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 005/125] 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 006/125] 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 007/125] 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 008/125] 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 009/125] 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 010/125] 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 011/125] 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 012/125] 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 013/125] 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 014/125] 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 015/125] 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 016/125] 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 017/125] 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 018/125] 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 019/125] 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 020/125] 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 021/125] 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 022/125] 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 023/125] 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 024/125] 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 025/125] 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 026/125] 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 027/125] 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 028/125] 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 029/125] 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 030/125] 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 031/125] 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 032/125] 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 033/125] 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 034/125] 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 035/125] 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 036/125] 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 037/125] 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 038/125] 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 039/125] 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 040/125] 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 041/125] 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 042/125] 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 043/125] 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 044/125] 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 045/125] 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 046/125] 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 047/125] 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 048/125] 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 049/125] 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 050/125] 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 051/125] 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 052/125] 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 053/125] 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 054/125] 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 055/125] 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 056/125] 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 057/125] 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 058/125] 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 059/125] 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 060/125] 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 061/125] 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 062/125] 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 063/125] 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 064/125] 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 065/125] 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 066/125] 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 067/125] 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 068/125] 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 069/125] 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 070/125] 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 071/125] 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 072/125] 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 073/125] 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 074/125] 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 075/125] 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 076/125] 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 077/125] 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 078/125] 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 079/125] 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 080/125] 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 081/125] 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 082/125] 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 083/125] 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 084/125] 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 085/125] 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 086/125] 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 087/125] 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 088/125] 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 089/125] 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 090/125] 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 091/125] 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 092/125] 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 093/125] 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 094/125] 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 095/125] 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 096/125] 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 097/125] 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 098/125] 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 099/125] 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 100/125] 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 101/125] 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 102/125] 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 103/125] 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 104/125] 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 105/125] 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 106/125] 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 107/125] 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 108/125] 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 109/125] 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 110/125] 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 111/125] 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 112/125] 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 113/125] 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 114/125] 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 115/125] 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 116/125] 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 117/125] 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 118/125] 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 `4⁄5`, which renders as + 4⁄5. + + +### 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 `4⁄5`, which renders as - 4⁄5. - - -### 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