├── .eslintignore ├── internal ├── testdata │ ├── disk │ │ ├── embed.go │ │ ├── file2.txt │ │ ├── file.txt │ │ └── under │ │ │ └── sub │ │ │ └── subfile │ ├── embedded │ │ ├── file.txt │ │ ├── under │ │ │ └── sub │ │ │ │ └── subfile │ │ └── embed.go │ └── panic.txt ├── defaults │ ├── defaults.go │ └── defaults_test.go ├── fakesmtp │ ├── connection.go │ └── server.go └── httpx │ ├── content_type.go │ └── content_type_test.go ├── plugins ├── plugdeps │ ├── plugin_test.go │ ├── pop.go │ ├── command.go │ ├── plugin.go │ ├── plugins_test.go │ ├── plugdeps_test.go │ ├── plugins.go │ └── plugdeps.go ├── log.go ├── events.go ├── log_debug.go ├── command.go ├── plugcmds │ ├── plug_map_test.go │ ├── available_test.go │ ├── plug_map.go │ └── available.go ├── plugins_test.go ├── cache.go └── decorate.go ├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── standard-stale.yml │ └── standard-go-test.yml ├── app_test.go ├── .codecov.yml ├── .csslintrc ├── runtime ├── version.go └── build.go ├── mail ├── body.go ├── internal │ └── mail │ │ ├── doc.go │ │ ├── .gitignore │ │ ├── mime.go │ │ ├── .travis.yml │ │ ├── errors.go │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── auth.go │ │ ├── send.go │ │ ├── CHANGELOG.md │ │ └── README.md ├── attachment.go ├── sender.go ├── mail.go ├── mail_test.go ├── smtp_sender_test.go ├── message.go ├── smtp_sender.go └── README.md ├── logger.go ├── binding ├── decoders │ ├── time.go │ ├── parse_time.go │ ├── null_time.go │ ├── decoders.go │ ├── null_time_test.go │ └── time_test.go ├── bindable.go ├── file.go ├── bindable_test.go ├── types.go ├── xml_request_type_binder.go ├── json_content_type_binder.go ├── html_content_type_binder.go ├── binding_test.go ├── request_binder.go ├── request_binder_test.go ├── file_request_type_binder.go ├── binding.go └── file_test.go ├── request_data_test.go ├── request_data.go ├── error_templates.go ├── Dockerfile ├── .golangci.yml ├── render ├── renderer.go ├── render_test.go ├── json_test.go ├── xml_test.go ├── func_test.go ├── string_test.go ├── json.go ├── plain_test.go ├── template_engine.go ├── xml.go ├── helpers.go ├── string_map_test.go ├── plain.go ├── string.go ├── func.go ├── markdown_test.go ├── js.go ├── options.go ├── template_helpers.go ├── string_map.go ├── sse.go ├── render.go ├── download_test.go ├── html.go ├── template_helpers_test.go ├── html_test.go ├── download.go └── js_test.go ├── .gitignore ├── response_test.go ├── .codeclimate.yml ├── worker ├── job.go └── worker.go ├── method_override_test.go ├── servers ├── tls.go ├── simple.go ├── servers.go └── listener.go ├── method_override.go ├── handler.go ├── Dockerfile.slim.build ├── buffalo.go ├── BACKERS.md ├── LICENSE.txt ├── route_info_test.go ├── Dockerfile.build ├── flash.go ├── home.go ├── options_test.go ├── not_found_test.go ├── plugins.go ├── response.go ├── session.go ├── session_test.go ├── notfound.prod.html ├── error.prod.html ├── routenamer.go ├── cookies_test.go ├── request_logger.go ├── cookies.go ├── app.go ├── events.go ├── go.mod ├── route.go ├── resource.go ├── fs.go ├── context.go ├── wrappers.go ├── wrappers_test.go ├── fs_test.go ├── grifts.go ├── route_info.go └── flash_test.go /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /internal/testdata/disk/embed.go: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/testdata/disk/file2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/testdata/disk/file.txt: -------------------------------------------------------------------------------- 1 | This file is on disk. -------------------------------------------------------------------------------- /plugins/plugdeps/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | -------------------------------------------------------------------------------- /internal/testdata/embedded/file.txt: -------------------------------------------------------------------------------- 1 | This file is embedded. -------------------------------------------------------------------------------- /internal/testdata/disk/under/sub/subfile: -------------------------------------------------------------------------------- 1 | This file is on disk/sub. -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owner 2 | * @gobuffalo/core-managers -------------------------------------------------------------------------------- /internal/testdata/embedded/under/sub/subfile: -------------------------------------------------------------------------------- 1 | This file is on embedded/sub. -------------------------------------------------------------------------------- /internal/testdata/panic.txt: -------------------------------------------------------------------------------- 1 | This file must not be accessible from buffalo.FS. -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | func voidHandler(c Context) error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "60...70" 3 | 4 | status: 5 | project: false 6 | patch: false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: markbates 4 | patreon: buffalo 5 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /runtime/version.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | // Version is the current version of the buffalo binary 4 | var Version = "v1.1.0" 5 | -------------------------------------------------------------------------------- /plugins/log.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | package plugins 4 | 5 | func log(_ string, fn func() error) error { 6 | return fn() 7 | } 8 | -------------------------------------------------------------------------------- /plugins/plugdeps/pop.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | var pop = Plugin{ 4 | Binary: "buffalo-pop", 5 | GoGet: "github.com/gobuffalo/buffalo-pop/v2", 6 | } 7 | -------------------------------------------------------------------------------- /mail/body.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | // Body represents one of the bodies in the Message could be main or alternative 4 | type Body struct { 5 | Content string 6 | ContentType string 7 | } 8 | -------------------------------------------------------------------------------- /internal/testdata/embedded/embed.go: -------------------------------------------------------------------------------- 1 | package embedded 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed * 8 | var files embed.FS 9 | 10 | func FS() embed.FS { 11 | return files 12 | } 13 | -------------------------------------------------------------------------------- /mail/internal/mail/doc.go: -------------------------------------------------------------------------------- 1 | // Package gomail provides a simple interface to compose emails and to mail them 2 | // efficiently. 3 | // 4 | // More info on Github: https://github.com/go-mail/mail 5 | // 6 | package mail 7 | -------------------------------------------------------------------------------- /plugins/events.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | const ( 4 | EvtSetupStarted = "buffalo-plugins:setup:started" 5 | EvtSetupErr = "buffalo-plugins:setup:err" 6 | EvtSetupFinished = "buffalo-plugins:setup:finished" 7 | ) 8 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "github.com/gobuffalo/logger" 5 | ) 6 | 7 | // Logger interface is used throughout Buffalo 8 | // apps to log a whole manner of things. 9 | type Logger = logger.FieldLogger 10 | -------------------------------------------------------------------------------- /mail/attachment.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import "io" 4 | 5 | // Attachment are files added into a email message 6 | type Attachment struct { 7 | Name string 8 | Reader io.Reader 9 | ContentType string 10 | Embedded bool 11 | } 12 | -------------------------------------------------------------------------------- /binding/decoders/time.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | // TimeDecoderFn is a custom type decoder func for Time fields 4 | func TimeDecoderFn() func([]string) (interface{}, error) { 5 | return func(vals []string) (interface{}, error) { 6 | return parseTime(vals) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugins/log_debug.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package plugins 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func log(name string, fn func() error) error { 11 | start := time.Now() 12 | defer fmt.Println(name, time.Now().Sub(start)) 13 | return fn() 14 | } 15 | -------------------------------------------------------------------------------- /binding/bindable.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import "net/http" 4 | 5 | // Bindable when implemented, on a type 6 | // will override any Binders that have been 7 | // configured when using buffalo#Context.Bind 8 | type Bindable interface { 9 | Bind(*http.Request) error 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/standard-stale.yml: -------------------------------------------------------------------------------- 1 | name: Standard Autocloser 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | call-standard-autocloser: 9 | name: Autocloser 10 | uses: gobuffalo/.github/.github/workflows/stale.yml@v1 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/standard-go-test.yml: -------------------------------------------------------------------------------- 1 | name: Standard Test 2 | 3 | on: 4 | push: 5 | branches: [main v1] 6 | pull_request: 7 | 8 | jobs: 9 | call-standard-test: 10 | name: Test 11 | uses: gobuffalo/.github/.github/workflows/go-test.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /mail/internal/mail/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | 16 | # IDE's 17 | .idea/ 18 | -------------------------------------------------------------------------------- /request_data_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_newRequestData(t *testing.T) { 10 | 11 | r := require.New(t) 12 | ts := newRequestData() 13 | r.NotNil(ts) 14 | r.NotNil(ts.moot) 15 | r.NotNil(ts.d) 16 | } 17 | -------------------------------------------------------------------------------- /request_data.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import "sync" 4 | 5 | type requestData struct { 6 | d map[string]interface{} 7 | moot *sync.RWMutex 8 | } 9 | 10 | func newRequestData() *requestData { 11 | return &requestData{ 12 | d: make(map[string]interface{}), 13 | moot: &sync.RWMutex{}, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /error_templates.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | _ "embed" // needed to embed the templates. 5 | ) 6 | 7 | var ( 8 | //go:embed error.dev.html 9 | devErrorTmpl string 10 | 11 | //go:embed error.prod.html 12 | prodErrorTmpl string 13 | 14 | //go:embed notfound.prod.html 15 | prodNotFoundTmpl string 16 | ) 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gobuffalo/buffalo:latest 2 | 3 | ARG CODECOV_TOKEN 4 | 5 | ENV GOPROXY https://proxy.golang.org 6 | ENV BP /src/buffalo 7 | 8 | RUN rm -rf $BP 9 | RUN mkdir -p $BP 10 | 11 | WORKDIR $BP 12 | COPY . . 13 | 14 | RUN go mod tidy 15 | RUN go test -tags "sqlite integration_test" -cover -race -v ./... 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | fast: true 2 | linters: 3 | enable-all: false 4 | disable-all: false 5 | enable: 6 | - govet 7 | - golint 8 | - goimports 9 | - deadcode 10 | - typecheck 11 | - ineffassign 12 | - misspell 13 | - nakedret 14 | - unconvert 15 | - megacheck 16 | - varcheck 17 | disable: 18 | - errcheck -------------------------------------------------------------------------------- /mail/sender.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | // Sender interface for any upcoming mailers. 4 | type Sender interface { 5 | Send(Message) error 6 | } 7 | 8 | // BatchSender interface for sending batch or single mail 9 | type BatchSender interface { 10 | Sender 11 | SendBatch(messages ...Message) (errorsByMessages []error, generalError error) 12 | } 13 | -------------------------------------------------------------------------------- /render/renderer.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "io" 4 | 5 | // Renderer interface that must be satisfied to be used with 6 | // buffalo.Context.Render 7 | type Renderer interface { 8 | ContentType() string 9 | Render(io.Writer, Data) error 10 | } 11 | 12 | // Data type to be provided to the Render function on the 13 | // Renderer interface. 14 | type Data map[string]interface{} 15 | -------------------------------------------------------------------------------- /mail/internal/mail/mime.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "mime" 5 | "mime/quotedprintable" 6 | "strings" 7 | ) 8 | 9 | var newQPWriter = quotedprintable.NewWriter 10 | 11 | type mimeEncoder struct { 12 | mime.WordEncoder 13 | } 14 | 15 | var ( 16 | bEncoding = mimeEncoder{mime.BEncoding} 17 | qEncoding = mimeEncoder{mime.QEncoding} 18 | lastIndexByte = strings.LastIndexByte 19 | ) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | *.pbxuser 11 | *.mode1v3 12 | .svn 13 | profile 14 | .console_history 15 | .sass-cache/* 16 | .rake_tasks~ 17 | *.log.lck 18 | solr/ 19 | .jhw-cache/ 20 | jhw.* 21 | *.sublime* 22 | node_modules/ 23 | dist/ 24 | generated/ 25 | .vendor/ 26 | bin/* 27 | gin-bin 28 | .idea/ 29 | .vscode/settings.json 30 | -------------------------------------------------------------------------------- /render/render_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "github.com/psanford/memfs" 5 | ) 6 | 7 | type Widget struct { 8 | Name string 9 | } 10 | 11 | func (w Widget) ToPath() string { 12 | return w.Name 13 | } 14 | 15 | func NewEngine() *Engine { 16 | return New(Options{ 17 | TemplatesFS: memfs.New(), 18 | AssetsFS: memfs.New(), 19 | }) 20 | } 21 | 22 | type rendFriend func(string, RendererFunc) Renderer 23 | -------------------------------------------------------------------------------- /mail/internal/mail/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - 1.4 7 | - 1.5 8 | - 1.6 9 | - 1.7 10 | - 1.8 11 | - 1.9 12 | - master 13 | 14 | # safelist 15 | branches: 16 | only: 17 | - master 18 | - v2 19 | 20 | notifications: 21 | email: false 22 | 23 | before_install: 24 | - mkdir -p $GOPATH/src/gopkg.in && 25 | ln -s ../github.com/go-mail/mail $GOPATH/src/gopkg.in/mail.v2 26 | -------------------------------------------------------------------------------- /binding/file.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "mime/multipart" 5 | ) 6 | 7 | // File holds information regarding an uploaded file 8 | type File struct { 9 | multipart.File 10 | *multipart.FileHeader 11 | } 12 | 13 | // Valid if there is an actual uploaded file 14 | func (f File) Valid() bool { 15 | return f.File != nil 16 | } 17 | 18 | func (f File) String() string { 19 | if f.File == nil { 20 | return "" 21 | } 22 | return f.Filename 23 | } 24 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Response_MultipleWrite(t *testing.T) { 12 | r := require.New(t) 13 | resWr := httptest.NewRecorder() 14 | res := Response{ 15 | ResponseWriter: resWr, 16 | } 17 | 18 | res.WriteHeader(http.StatusOK) 19 | res.WriteHeader(http.StatusInternalServerError) 20 | 21 | r.Equal(res.Status, http.StatusOK) 22 | } 23 | -------------------------------------------------------------------------------- /mail/internal/mail/errors.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import "fmt" 4 | 5 | // A SendError represents the failure to transmit a Message, detailing the cause 6 | // of the failure and index of the Message within a batch. 7 | type SendError struct { 8 | // Index specifies the index of the Message within a batch. 9 | Index uint 10 | Cause error 11 | } 12 | 13 | func (err *SendError) Error() string { 14 | return fmt.Sprintf("gomail: could not send email %d: %v", 15 | err.Index+1, err.Cause) 16 | } 17 | -------------------------------------------------------------------------------- /render/json_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_JSON(t *testing.T) { 12 | r := require.New(t) 13 | 14 | e := NewEngine() 15 | 16 | re := e.JSON(Data{"hello": "world"}) 17 | r.Equal("application/json; charset=utf-8", re.ContentType()) 18 | 19 | bb := &bytes.Buffer{} 20 | 21 | r.NoError(re.Render(bb, nil)) 22 | r.Equal(`{"hello":"world"}`, strings.TrimSpace(bb.String())) 23 | } 24 | -------------------------------------------------------------------------------- /plugins/plugdeps/command.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import "encoding/json" 4 | 5 | // Command is the plugin command you want to control further 6 | type Command struct { 7 | Name string `toml:"name" json:"name"` 8 | Flags []string `toml:"flags,omitempty" json:"flags,omitempty"` 9 | Commands []Command `toml:"command,omitempty" json:"commands,omitempty"` 10 | } 11 | 12 | // String implementation of fmt.Stringer 13 | func (p Command) String() string { 14 | b, _ := json.Marshal(p) 15 | return string(b) 16 | } 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | golint: 4 | enabled: true 5 | checks: 6 | GoLint/Naming/MixedCaps: 7 | enabled: false 8 | govet: 9 | enabled: true 10 | gofmt: 11 | enabled: true 12 | fixme: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "**.go" 17 | exclude_paths: 18 | - "generators/*/templates/**/*" 19 | - "buffalo/cmd/filetests/*" 20 | - "examples/**/*" 21 | - "vendor/**/*" 22 | - "**/*_test.go" 23 | - "*_test.go" 24 | - "**_test.go" 25 | - "middleware_test.go" 26 | -------------------------------------------------------------------------------- /binding/bindable_test.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type orbison struct { 12 | bound bool 13 | } 14 | 15 | func (o *orbison) Bind(req *http.Request) error { 16 | o.bound = true 17 | return nil 18 | } 19 | 20 | func Test_Bindable(t *testing.T) { 21 | r := require.New(t) 22 | 23 | req := httptest.NewRequest("GET", "/", nil) 24 | o := &orbison{} 25 | r.False(o.bound) 26 | r.NoError(Exec(req, o)) 27 | r.True(o.bound) 28 | } 29 | -------------------------------------------------------------------------------- /binding/decoders/parse_time.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func parseTime(vals []string) (time.Time, error) { 8 | var t time.Time 9 | var err error 10 | 11 | // don't try to parse empty time values, it will raise an error 12 | if len(vals) == 0 || vals[0] == "" { 13 | return t, nil 14 | } 15 | 16 | for _, layout := range timeFormats { 17 | t, err = time.Parse(layout, vals[0]) 18 | if err == nil { 19 | return t, nil 20 | } 21 | } 22 | 23 | if err != nil { 24 | return t, err 25 | } 26 | 27 | return t, nil 28 | } 29 | -------------------------------------------------------------------------------- /binding/types.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // ContenTypeBinder are those capable of handling a request type like JSON or XML 8 | type ContenTypeBinder interface { 9 | BinderFunc() Binder 10 | ContentTypes() []string 11 | } 12 | 13 | // Binder takes a request and binds it to an interface. 14 | // If there is a problem it should return an error. 15 | type Binder func(*http.Request, interface{}) error 16 | 17 | // CustomTypeDecoder converts a custom type from the request into its exact type. 18 | type CustomTypeDecoder func([]string) (interface{}, error) 19 | -------------------------------------------------------------------------------- /internal/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | func String(s1, s2 string) string { 4 | if s1 == "" { 5 | return s2 6 | } 7 | return s1 8 | } 9 | 10 | func Int(i1, i2 int) int { 11 | if i1 == 0 { 12 | return i2 13 | } 14 | return i1 15 | } 16 | 17 | func Int64(i1, i2 int64) int64 { 18 | if i1 == 0 { 19 | return i2 20 | } 21 | return i1 22 | } 23 | 24 | func Float32(i1, i2 float32) float32 { 25 | if i1 == 0.0 { 26 | return i2 27 | } 28 | return i1 29 | } 30 | 31 | func Float64(i1, i2 float64) float64 { 32 | if i1 == 0.0 { 33 | return i2 34 | } 35 | return i1 36 | } 37 | -------------------------------------------------------------------------------- /render/xml_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_XML(t *testing.T) { 12 | r := require.New(t) 13 | 14 | type user struct { 15 | Name string 16 | } 17 | 18 | e := NewEngine() 19 | 20 | re := e.XML(user{Name: "Mark"}) 21 | r.Equal("application/xml; charset=utf-8", re.ContentType()) 22 | 23 | bb := &bytes.Buffer{} 24 | 25 | r.NoError(re.Render(bb, nil)) 26 | r.Equal("\n\n Mark\n", strings.TrimSpace(bb.String())) 27 | } 28 | -------------------------------------------------------------------------------- /worker/job.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import "encoding/json" 4 | 5 | // Args are the arguments passed into a job 6 | type Args map[string]interface{} 7 | 8 | func (a Args) String() string { 9 | b, _ := json.Marshal(a) 10 | return string(b) 11 | } 12 | 13 | // Job to be processed by a Worker 14 | type Job struct { 15 | // Queue the job should be placed into 16 | Queue string 17 | // Args that will be passed to the Handler when run 18 | Args Args 19 | // Handler that will be run by the worker 20 | Handler string 21 | } 22 | 23 | func (j Job) String() string { 24 | b, _ := json.Marshal(j) 25 | return string(b) 26 | } 27 | -------------------------------------------------------------------------------- /method_override_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/gobuffalo/buffalo/render" 9 | "github.com/gobuffalo/httptest" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_MethodOverride(t *testing.T) { 14 | r := require.New(t) 15 | 16 | a := New(Options{}) 17 | a.PUT("/", func(c Context) error { 18 | return c.Render(http.StatusOK, render.String("you put me!")) 19 | }) 20 | 21 | w := httptest.New(a) 22 | res := w.HTML("/").Post(url.Values{"_method": []string{"PUT"}}) 23 | r.Equal(http.StatusOK, res.Code) 24 | r.Equal("you put me!", res.Body.String()) 25 | } 26 | -------------------------------------------------------------------------------- /render/func_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Func(t *testing.T) { 12 | r := require.New(t) 13 | 14 | table := []rendFriend{ 15 | Func, 16 | New(Options{}).Func, 17 | } 18 | 19 | for _, tt := range table { 20 | bb := &bytes.Buffer{} 21 | 22 | re := tt("foo/bar", func(w io.Writer, data Data) error { 23 | _, err := w.Write([]byte(data["name"].(string))) 24 | return err 25 | }) 26 | 27 | r.Equal("foo/bar", re.ContentType()) 28 | err := re.Render(bb, Data{"name": "Mark"}) 29 | r.NoError(err) 30 | r.Equal("Mark", bb.String()) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /render/string_test.go: -------------------------------------------------------------------------------- 1 | package render_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/gobuffalo/buffalo/render" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_String(t *testing.T) { 12 | r := require.New(t) 13 | 14 | j := render.New(render.Options{}).String 15 | 16 | re := j("<%= name %>") 17 | r.Equal("text/plain; charset=utf-8", re.ContentType()) 18 | 19 | var examples = []string{"Mark", "Jém"} 20 | for _, example := range examples { 21 | example := example 22 | bb := &bytes.Buffer{} 23 | err := re.Render(bb, map[string]interface{}{"name": example}) 24 | r.NoError(err) 25 | r.Equal(example, bb.String()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /binding/xml_request_type_binder.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "encoding/xml" 5 | "net/http" 6 | ) 7 | 8 | // XMLRequestTypeBinder is in charge of binding XML request types. 9 | type XMLRequestTypeBinder struct{} 10 | 11 | // BinderFunc returns the Binder for this RequestTypeBinder 12 | func (xm XMLRequestTypeBinder) BinderFunc() Binder { 13 | return func(req *http.Request, value interface{}) error { 14 | return xml.NewDecoder(req.Body).Decode(value) 15 | } 16 | } 17 | 18 | // ContentTypes that will be wired to this the XML Binder 19 | func (xm XMLRequestTypeBinder) ContentTypes() []string { 20 | return []string{ 21 | "application/xml", 22 | "text/xml", 23 | "xml", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/fakesmtp/connection.go: -------------------------------------------------------------------------------- 1 | package fakesmtp 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | //Connection of a client with our server 10 | type Connection struct { 11 | conn net.Conn 12 | address string 13 | time int64 14 | bufin *bufio.Reader 15 | bufout *bufio.Writer 16 | } 17 | 18 | //write something to the client on the connection 19 | func (c *Connection) write(s string) { 20 | c.bufout.WriteString(s + "\r\n") 21 | c.bufout.Flush() 22 | } 23 | 24 | //read a string from the connected client 25 | func (c *Connection) read() string { 26 | reply, err := c.bufin.ReadString('\n') 27 | 28 | if err != nil { 29 | fmt.Println("e ", err) 30 | } 31 | return reply 32 | } 33 | -------------------------------------------------------------------------------- /internal/httpx/content_type.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gobuffalo/buffalo/internal/defaults" 8 | ) 9 | 10 | func ContentType(req *http.Request) string { 11 | ct := defaults.String(req.Header.Get("Content-Type"), req.Header.Get("Accept")) 12 | ct = strings.TrimSpace(ct) 13 | var cts []string 14 | if strings.Contains(ct, ",") { 15 | cts = strings.Split(ct, ",") 16 | } else { 17 | cts = strings.Split(ct, ";") 18 | } 19 | for _, c := range cts { 20 | c = strings.TrimSpace(c) 21 | if strings.HasPrefix(c, "*/*") { 22 | continue 23 | } 24 | return strings.ToLower(c) 25 | } 26 | if ct == "*/*" { 27 | return "" 28 | } 29 | return ct 30 | } 31 | -------------------------------------------------------------------------------- /render/json.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | ) 7 | 8 | type jsonRenderer struct { 9 | value interface{} 10 | } 11 | 12 | func (s jsonRenderer) ContentType() string { 13 | return "application/json; charset=utf-8" 14 | } 15 | 16 | func (s jsonRenderer) Render(w io.Writer, data Data) error { 17 | return json.NewEncoder(w).Encode(s.value) 18 | } 19 | 20 | // JSON renders the value using the "application/json" 21 | // content type. 22 | func JSON(v interface{}) Renderer { 23 | return jsonRenderer{value: v} 24 | } 25 | 26 | // JSON renders the value using the "application/json" 27 | // content type. 28 | func (e *Engine) JSON(v interface{}) Renderer { 29 | return JSON(v) 30 | } 31 | -------------------------------------------------------------------------------- /binding/json_content_type_binder.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // JSONContentTypeBinder is in charge of binding JSON request types. 9 | type JSONContentTypeBinder struct{} 10 | 11 | // BinderFunc returns the Binder for this JSONRequestTypeBinder 12 | func (js JSONContentTypeBinder) BinderFunc() Binder { 13 | return func(req *http.Request, value interface{}) error { 14 | return json.NewDecoder(req.Body).Decode(value) 15 | } 16 | } 17 | 18 | // ContentTypes that will be wired to this the JSON Binder 19 | func (js JSONContentTypeBinder) ContentTypes() []string { 20 | return []string{ 21 | "application/json", 22 | "text/json", 23 | "json", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mail/internal/mail/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing to Gomail! Here are a few guidelines: 2 | 3 | ## Bugs 4 | 5 | If you think you found a bug, create an issue and supply the minimum amount 6 | of code triggering the bug so it can be reproduced. 7 | 8 | 9 | ## Fixing a bug 10 | 11 | If you want to fix a bug, you can send a pull request. It should contains a 12 | new test or update an existing one to cover that bug. 13 | 14 | 15 | ## New feature proposal 16 | 17 | If you think Gomail lacks a feature, you can open an issue or send a pull 18 | request. I want to keep Gomail code and API as simple as possible so please 19 | describe your needs so we can discuss whether this feature should be added to 20 | Gomail or not. 21 | -------------------------------------------------------------------------------- /plugins/command.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | // Command that the plugin supplies 4 | type Command struct { 5 | // Name "foo" 6 | Name string `json:"name"` 7 | // UseCommand "bar" 8 | UseCommand string `json:"use_command"` 9 | // BuffaloCommand "generate" 10 | BuffaloCommand string `json:"buffalo_command"` 11 | // Description "generates a foo" 12 | Description string `json:"description,omitempty"` 13 | Aliases []string `json:"aliases,omitempty"` 14 | Binary string `json:"-"` 15 | Flags []string `json:"flags,omitempty"` 16 | // Filters events to listen to ("" or "*") is all events 17 | ListenFor string `json:"listen_for,omitempty"` 18 | } 19 | 20 | // Commands is a slice of Command 21 | type Commands []Command 22 | -------------------------------------------------------------------------------- /render/plain_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/psanford/memfs" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Plain(t *testing.T) { 12 | r := require.New(t) 13 | 14 | rootFS := memfs.New() 15 | r.NoError(rootFS.WriteFile("test.txt", []byte("<%= name %>"), 0644)) 16 | 17 | e := NewEngine() 18 | e.TemplatesFS = rootFS 19 | 20 | re := e.Plain("test.txt") 21 | r.Equal("text/plain; charset=utf-8", re.ContentType()) 22 | 23 | var examples = []string{"Mark", "Jém"} 24 | for _, example := range examples { 25 | example := example 26 | bb := &bytes.Buffer{} 27 | r.NoError(re.Render(bb, Data{"name": example})) 28 | r.Equal(example, bb.String()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /servers/tls.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // TLS server 10 | type TLS struct { 11 | *http.Server 12 | CertFile string 13 | KeyFile string 14 | } 15 | 16 | // SetAddr sets the servers address, if it hasn't already been set 17 | func (s *TLS) SetAddr(addr string) { 18 | if s.Server.Addr == "" { 19 | s.Server.Addr = addr 20 | } 21 | } 22 | 23 | // String returns a string representation of a Listener 24 | func (s *TLS) String() string { 25 | return fmt.Sprintf("TLS server on %s", s.Server.Addr) 26 | } 27 | 28 | // Start the server 29 | func (s *TLS) Start(c context.Context, h http.Handler) error { 30 | s.Handler = h 31 | return s.ListenAndServeTLS(s.CertFile, s.KeyFile) 32 | } 33 | -------------------------------------------------------------------------------- /method_override.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gobuffalo/buffalo/internal/defaults" 7 | ) 8 | 9 | // MethodOverride is the default implementation for the 10 | // Options#MethodOverride. By default it will look for a form value 11 | // name `_method` and change the request method if that is 12 | // present and the original request is of type "POST". This is 13 | // added automatically when using `New` Buffalo, unless 14 | // an alternative is defined in the Options. 15 | func MethodOverride(res http.ResponseWriter, req *http.Request) { 16 | if req.Method == "POST" { 17 | req.Method = defaults.String(req.FormValue("_method"), "POST") 18 | req.Form.Del("_method") 19 | req.PostForm.Del("_method") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /render/template_engine.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | ) 7 | 8 | // TemplateEngine needs to be implemented for a template system to be able to be used with Buffalo. 9 | type TemplateEngine func(input string, data map[string]interface{}, helpers map[string]interface{}) (string, error) 10 | 11 | // GoTemplateEngine implements the TemplateEngine interface for using standard Go templates 12 | func GoTemplateEngine(input string, data map[string]interface{}, helpers map[string]interface{}) (string, error) { 13 | t := template.New(input) 14 | 15 | t, err := t.Parse(input) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | bb := &bytes.Buffer{} 21 | err = t.Execute(bb, data) 22 | return bb.String(), err 23 | } 24 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | // Handler is the basis for all of Buffalo. A Handler 4 | // will be given a Context interface that represents the 5 | // give request/response. It is the responsibility of the 6 | // Handler to handle the request/response correctly. This 7 | // could mean rendering a template, JSON, etc... or it could 8 | // mean returning an error. 9 | /* 10 | func (c Context) error { 11 | return c.Render(http.StatusOK, render.String("Hello World!")) 12 | } 13 | 14 | func (c Context) error { 15 | return c.Redirect(http.StatusMovedPermanently, "http://github.com/gobuffalo/buffalo") 16 | } 17 | 18 | func (c Context) error { 19 | return c.Error(http.StatusUnprocessableEntity, fmt.Errorf("oops!!")) 20 | } 21 | */ 22 | type Handler func(Context) error 23 | -------------------------------------------------------------------------------- /render/xml.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | ) 7 | 8 | type xmlRenderer struct { 9 | value interface{} 10 | } 11 | 12 | func (s xmlRenderer) ContentType() string { 13 | return "application/xml; charset=utf-8" 14 | } 15 | 16 | func (s xmlRenderer) Render(w io.Writer, data Data) error { 17 | io.WriteString(w, xml.Header) 18 | enc := xml.NewEncoder(w) 19 | enc.Indent("", " ") 20 | return enc.Encode(s.value) 21 | } 22 | 23 | // XML renders the value using the "application/xml" 24 | // content type. 25 | func XML(v interface{}) Renderer { 26 | return xmlRenderer{value: v} 27 | } 28 | 29 | // XML renders the value using the "application/xml" 30 | // content type. 31 | func (e *Engine) XML(v interface{}) Renderer { 32 | return XML(v) 33 | } 34 | -------------------------------------------------------------------------------- /servers/simple.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Simple server 10 | type Simple struct { 11 | *http.Server 12 | } 13 | 14 | // SetAddr sets the servers address, if it hasn't already been set 15 | func (s *Simple) SetAddr(addr string) { 16 | if s.Server.Addr == "" { 17 | s.Server.Addr = addr 18 | } 19 | } 20 | 21 | // String returns a string representation of a Simple server 22 | func (s *Simple) String() string { 23 | return fmt.Sprintf("simple server on %s", s.Server.Addr) 24 | } 25 | 26 | // Start the server 27 | func (s *Simple) Start(c context.Context, h http.Handler) error { 28 | s.Handler = h 29 | return s.ListenAndServe() 30 | } 31 | 32 | // New Simple server 33 | func New() *Simple { 34 | return &Simple{ 35 | Server: &http.Server{}, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugins/plugdeps/plugin.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gobuffalo/meta" 7 | ) 8 | 9 | // Plugin represents a Go plugin for Buffalo applications 10 | type Plugin struct { 11 | Binary string `toml:"binary" json:"binary"` 12 | GoGet string `toml:"go_get,omitempty" json:"go_get,omitempty"` 13 | Local string `toml:"local,omitempty" json:"local,omitempty"` 14 | Commands []Command `toml:"command,omitempty" json:"commands,omitempty"` 15 | Tags meta.BuildTags `toml:"tags,omitempty" json:"tags,omitempty"` 16 | } 17 | 18 | // String implementation of fmt.Stringer 19 | func (p Plugin) String() string { 20 | b, _ := json.Marshal(p) 21 | return string(b) 22 | } 23 | 24 | func (p Plugin) key() string { 25 | return p.Binary + p.GoGet + p.Local 26 | } 27 | -------------------------------------------------------------------------------- /mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gobuffalo/buffalo" 8 | "github.com/gobuffalo/buffalo/render" 9 | ) 10 | 11 | // NewMessage builds a new message. 12 | func NewMessage() Message { 13 | return Message{ 14 | Context: context.Background(), 15 | Headers: map[string]string{}, 16 | Data: render.Data{}, 17 | moot: &sync.RWMutex{}, 18 | } 19 | } 20 | 21 | // NewFromData builds a new message with raw template data given 22 | func NewFromData(data render.Data) Message { 23 | d := render.Data{} 24 | for k, v := range data { 25 | d[k] = v 26 | } 27 | m := NewMessage() 28 | m.Data = d 29 | return m 30 | } 31 | 32 | // New builds a new message with the current buffalo.Context 33 | func New(c buffalo.Context) Message { 34 | m := NewFromData(c.Data()) 35 | m.Context = c 36 | return m 37 | } 38 | -------------------------------------------------------------------------------- /render/helpers.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | 7 | "github.com/gobuffalo/helpers/forms" 8 | "github.com/gobuffalo/helpers/forms/bootstrap" 9 | "github.com/gobuffalo/plush/v5" 10 | "github.com/gobuffalo/tags/v3" 11 | ) 12 | 13 | func init() { 14 | plush.Helpers.Add("paginator", func(pagination interface{}, opts map[string]interface{}, help plush.HelperContext) (template.HTML, error) { 15 | if opts["path"] == nil { 16 | if req, ok := help.Value("request").(*http.Request); ok { 17 | opts["path"] = req.URL.String() 18 | } 19 | } 20 | t, err := tags.Pagination(pagination, opts) 21 | if err != nil { 22 | return "", err 23 | } 24 | return t.HTML(), nil 25 | }) 26 | plush.Helpers.Add(forms.RemoteFormKey, bootstrap.RemoteForm) 27 | plush.Helpers.Add(forms.RemoteFormForKey, bootstrap.RemoteFormFor) 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile.slim.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine 2 | 3 | EXPOSE 3000 4 | 5 | ENV GOPROXY=https://proxy.golang.org 6 | 7 | RUN apk add --no-cache --upgrade apk-tools \ 8 | && apk add --no-cache bash curl openssl git build-base nodejs npm sqlite sqlite-dev mysql-client vim postgresql libpq postgresql-contrib libc6-compat 9 | 10 | # Installing linter 11 | RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \ 12 | | sh -s -- -b $(go env GOPATH)/bin v1.24.0 13 | 14 | # Installing Yarn 15 | RUN npm i -g --no-progress yarn \ 16 | && yarn config set yarn-offline-mirror /npm-packages-offline-cache \ 17 | && yarn config set yarn-offline-mirror-pruning true 18 | 19 | # Installing buffalo binary 20 | RUN go install github.com/gobuffalo/cli/cmd/buffalo@latest 21 | RUN go get github.com/gobuffalo/buffalo-pop/v2 22 | 23 | RUN mkdir /src 24 | WORKDIR /src 25 | -------------------------------------------------------------------------------- /buffalo.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package buffalo is a Go web development eco-system, designed to make your life easier. 3 | 4 | Buffalo helps you to generate a web project that already has everything from front-end (JavaScript, SCSS, etc.) to back-end (database, routing, etc.) already hooked up and ready to run. From there it provides easy APIs to build your web application quickly in Go. 5 | 6 | Buffalo **isn't just a framework**, it's a holistic web development environment and project structure that **lets developers get straight to the business** of, well, building their business. 7 | */ 8 | package buffalo 9 | 10 | // we need to import the runtime package 11 | // as its needed by `buffalo build` and without 12 | // this import the package doesn't get vendored 13 | // by go mod vendor or by dep. this import fixes 14 | // this problem. 15 | import _ "github.com/gobuffalo/buffalo/runtime" // Load the runtime package variables 16 | -------------------------------------------------------------------------------- /binding/decoders/null_time.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import "github.com/gobuffalo/nulls" 4 | 5 | // NullTimeDecoderFn is a custom type decoder func for null.Time fields 6 | func NullTimeDecoderFn() func([]string) (interface{}, error) { 7 | return func(vals []string) (interface{}, error) { 8 | var ti nulls.Time 9 | 10 | // If vals is empty, return a nulls.Time with Valid = false (i.e. NULL). 11 | // The parseTime() function called below does this check as well, but 12 | // because it doesn't return an error in the case where vals is empty, 13 | // we have no way to determine from its response that the nulls.Time 14 | // should actually be NULL. 15 | if len(vals) == 0 || vals[0] == "" { 16 | return ti, nil 17 | } 18 | 19 | t, err := parseTime(vals) 20 | if err != nil { 21 | return ti, err 22 | } 23 | 24 | ti.Time = t 25 | ti.Valid = true 26 | 27 | return ti, nil 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /binding/html_content_type_binder.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/monoculum/formam" 7 | ) 8 | 9 | // HTMLContentTypeBinder is in charge of binding HTML request types. 10 | type HTMLContentTypeBinder struct { 11 | decoder *formam.Decoder 12 | } 13 | 14 | // ContentTypes that will be used to identify HTML requests 15 | func (ht HTMLContentTypeBinder) ContentTypes() []string { 16 | return []string{ 17 | "application/html", 18 | "text/html", 19 | "application/x-www-form-urlencoded", 20 | "html", 21 | } 22 | } 23 | 24 | // BinderFunc that will take care of the HTML binding 25 | func (ht HTMLContentTypeBinder) BinderFunc() Binder { 26 | return func(req *http.Request, i interface{}) error { 27 | err := req.ParseForm() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if err := ht.decoder.Decode(req.Form, i); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /servers/servers.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | // Server allows for custom server implementations 10 | type Server interface { 11 | Shutdown(context.Context) error 12 | Start(context.Context, http.Handler) error 13 | SetAddr(string) 14 | } 15 | 16 | // Wrap converts a standard *http.Server to a buffalo.Server 17 | func Wrap(s *http.Server) Server { 18 | return &Simple{Server: s} 19 | } 20 | 21 | // WrapTLS Server converts a standard *http.Server to a buffalo.Server 22 | // but makes sure it is run with TLS. 23 | func WrapTLS(s *http.Server, certFile string, keyFile string) Server { 24 | return &TLS{ 25 | Server: s, 26 | CertFile: certFile, 27 | KeyFile: keyFile, 28 | } 29 | } 30 | 31 | // WrapListener wraps an *http.Server and a net.Listener 32 | func WrapListener(s *http.Server, l net.Listener) Server { 33 | return &Listener{ 34 | Server: s, 35 | Listener: l, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /runtime/build.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // BuildInfo holds information about the build 10 | type BuildInfo struct { 11 | Version string `json:"version"` 12 | Time time.Time `json:"-"` 13 | } 14 | 15 | // String implements fmt.String 16 | func (b BuildInfo) String() string { 17 | return fmt.Sprintf("%s (%s)", b.Version, b.Time) 18 | } 19 | 20 | var build = BuildInfo{ 21 | Version: "", 22 | Time: time.Time{}, 23 | } 24 | 25 | // Build returns the information about the current build 26 | // of the application. In development mode this will almost 27 | // always run zero values for BuildInfo. 28 | func Build() BuildInfo { 29 | return build 30 | } 31 | 32 | var so sync.Once 33 | 34 | // SetBuild allows the setting of build information only once. 35 | // This is typically managed by the binary built by `buffalo build`. 36 | func SetBuild(b BuildInfo) { 37 | so.Do(func() { 38 | build = b 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /render/string_map_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_stringMap(t *testing.T) { 11 | r := require.New(t) 12 | 13 | sm := &stringMap{} 14 | 15 | sm.Store("a", `A`) 16 | 17 | s, ok := sm.Load("a") 18 | r.True(ok) 19 | r.Equal(`A`, s) 20 | 21 | s, ok = sm.LoadOrStore("b", `B`) 22 | r.True(ok) 23 | r.Equal(`B`, s) 24 | 25 | s, ok = sm.LoadOrStore("b", `BB`) 26 | r.True(ok) 27 | r.Equal(`B`, s) 28 | 29 | var keys []string 30 | 31 | sm.Range(func(key string, value string) bool { 32 | keys = append(keys, key) 33 | return true 34 | }) 35 | 36 | sort.Strings(keys) 37 | 38 | r.Equal(sm.Keys(), keys) 39 | 40 | sm.Delete("b") 41 | r.Equal([]string{"a", "b"}, keys) 42 | 43 | sm.Delete("b") 44 | _, ok = sm.Load("b") 45 | r.False(ok) 46 | 47 | func(m *stringMap) { 48 | m.Store("c", `C`) 49 | }(sm) 50 | s, ok = sm.Load("c") 51 | r.True(ok) 52 | r.Equal(`C`, s) 53 | } 54 | -------------------------------------------------------------------------------- /internal/httpx/content_type_test.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "net/http/httptest" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_ContentType(t *testing.T) { 11 | r := require.New(t) 12 | 13 | table := []struct { 14 | Header string 15 | Value string 16 | Expected string 17 | }{ 18 | {"content-type", "a", "a"}, 19 | {"Content-Type", "c,d", "c"}, 20 | {"Content-Type", "e;f", "e"}, 21 | {"Content-Type", "", ""}, 22 | {"Accept", "", ""}, 23 | {"Accept", "*/*", ""}, 24 | {"Accept", "*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript", "text/javascript"}, 25 | {"accept", "text/javascript,application/javascript,application/ecmascript,application/x-ecmascript", "text/javascript"}, 26 | } 27 | 28 | for _, tt := range table { 29 | req := httptest.NewRequest("GET", "/", nil) 30 | req.Header.Set(tt.Header, tt.Value) 31 | r.Equal(tt.Expected, ContentType(req)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /binding/decoders/decoders.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var ( 9 | lock = &sync.RWMutex{} 10 | 11 | // timeFormats are the base time formats supported by the time.Time and 12 | // nulls.Time Decoders you can prepend custom formats to this list 13 | // by using RegisterTimeFormats. 14 | timeFormats = []string{ 15 | time.RFC3339, 16 | "01/02/2006", 17 | "2006-01-02", 18 | "2006-01-02T15:04", 19 | time.ANSIC, 20 | time.UnixDate, 21 | time.RubyDate, 22 | time.RFC822, 23 | time.RFC822Z, 24 | time.RFC850, 25 | time.RFC1123, 26 | time.RFC1123Z, 27 | time.RFC3339Nano, 28 | time.Kitchen, 29 | time.Stamp, 30 | time.StampMilli, 31 | time.StampMicro, 32 | time.StampNano, 33 | } 34 | ) 35 | 36 | // RegisterTimeFormats allows to add custom time layouts that 37 | // the binder will be able to use for decoding. 38 | func RegisterTimeFormats(layouts ...string) { 39 | lock.Lock() 40 | defer lock.Unlock() 41 | 42 | timeFormats = append(layouts, timeFormats...) 43 | } 44 | -------------------------------------------------------------------------------- /mail/mail_test.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/gobuffalo/buffalo" 9 | "github.com/gobuffalo/buffalo/render" 10 | "github.com/gobuffalo/httptest" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_NewFromData(t *testing.T) { 15 | r := require.New(t) 16 | m := NewFromData(map[string]interface{}{ 17 | "foo": "bar", 18 | }) 19 | r.Equal("bar", m.Data["foo"]) 20 | } 21 | 22 | func Test_New(t *testing.T) { 23 | r := require.New(t) 24 | 25 | var m Message 26 | app := buffalo.New(buffalo.Options{}) 27 | app.GET("/", func(c buffalo.Context) error { 28 | c.Set("foo", "bar") 29 | m = New(c) 30 | return c.Render(http.StatusOK, render.String("")) 31 | }) 32 | w := httptest.New(app) 33 | w.HTML("/").Get() 34 | 35 | r.NotNil(m) 36 | r.Equal("bar", m.Data["foo"]) 37 | rp, ok := m.Data["rootPath"].(buffalo.RouteHelperFunc) 38 | r.True(ok) 39 | x, err := rp(map[string]interface{}{}) 40 | r.NoError(err) 41 | r.Equal(template.HTML("/"), x) 42 | } 43 | -------------------------------------------------------------------------------- /render/plain.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // Plain renders the named files using the 'text/html' 4 | // content type and the github.com/gobuffalo/plush 5 | // package for templating. If more than 1 file is provided 6 | // the second file will be considered a "layout" file 7 | // and the first file will be the "content" file which will 8 | // be placed into the "layout" using "<%= yield %>". 9 | func Plain(names ...string) Renderer { 10 | e := New(Options{}) 11 | return e.Plain(names...) 12 | } 13 | 14 | // Plain renders the named files using the 'text/plain' 15 | // content type and the github.com/gobuffalo/plush 16 | // package for templating. If more than 1 file is provided 17 | // the second file will be considered a "layout" file 18 | // and the first file will be the "content" file which will 19 | // be placed into the "layout" using "<%= yield %>". 20 | func (e *Engine) Plain(names ...string) Renderer { 21 | hr := &templateRenderer{ 22 | Engine: e, 23 | contentType: "text/plain; charset=utf-8", 24 | names: names, 25 | } 26 | return hr 27 | } 28 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Financial Backers of the Buffalo Project 2 | 3 | Buffalo is a community-driven project that is run by individuals who believe that Buffalo is the way to quickly, and easily, build high quality, scalable applications in Go. 4 | 5 | Financial contributions to the Buffalo go towards ongoing development costs, servers, swag, conferences, etc... 6 | 7 | If you, or your company, use Buffalo, please consider supporting this effort to make rapid web development in Go, simple, easy, and fun! 8 | 9 | [http://patreon.com/buffalo](http://patreon.com/buffalo) 10 | 11 | --- 12 | 13 | ## Platinum Sponsors 14 | 15 | * **[Gopher Guides](https://www.gopherguides.com)** 16 | * **[PaperCall.io](https://www.papercall.io)** 17 | * [Your Company Here](http://patreon.com/buffalo) 18 | 19 | ### Gold Sponsors 20 | 21 | * [Your Company Here](http://patreon.com/buffalo) 22 | 23 | ### Premium Backers 24 | 25 | * [Your Company Here](http://patreon.com/buffalo) 26 | 27 | #### Generous Backers 28 | 29 | * **[Zhorty](https://zhorty.com)** 30 | * [Your Company Here](http://patreon.com/buffalo) 31 | 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Mark Bates 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /servers/listener.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | ) 9 | 10 | // Listener server for using a pre-defined net.Listener 11 | type Listener struct { 12 | *http.Server 13 | Listener net.Listener 14 | } 15 | 16 | // SetAddr sets the servers address, if it hasn't already been set 17 | func (s *Listener) SetAddr(addr string) { 18 | if s.Server.Addr == "" { 19 | s.Server.Addr = addr 20 | } 21 | } 22 | 23 | // String returns a string representation of a Listener 24 | func (s *Listener) String() string { 25 | return fmt.Sprintf("listener on %s", s.Server.Addr) 26 | } 27 | 28 | // Start the server 29 | func (s *Listener) Start(c context.Context, h http.Handler) error { 30 | s.Handler = h 31 | return s.Serve(s.Listener) 32 | } 33 | 34 | // UnixSocket returns a new Listener on that address 35 | func UnixSocket(addr string) (*Listener, error) { 36 | listener, err := net.Listen("unix", addr) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return &Listener{ 41 | Server: &http.Server{}, 42 | Listener: listener, 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /plugins/plugcmds/plug_map_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. 2 | 3 | package plugcmds 4 | 5 | import ( 6 | "sort" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_plugMap(t *testing.T) { 13 | r := require.New(t) 14 | 15 | sm := &plugMap{} 16 | 17 | sm.Store("a", plug{}) 18 | 19 | s, ok := sm.Load("a") 20 | r.True(ok) 21 | r.Equal(plug{}, s) 22 | 23 | s, ok = sm.LoadOrStore("b", plug{}) 24 | r.True(ok) 25 | r.Equal(plug{}, s) 26 | 27 | s, ok = sm.LoadOrStore("b", plug{}) 28 | r.True(ok) 29 | r.Equal(plug{}, s) 30 | 31 | var keys []string 32 | 33 | sm.Range(func(key string, value plug) bool { 34 | keys = append(keys, key) 35 | return true 36 | }) 37 | 38 | sort.Strings(keys) 39 | 40 | r.Equal(sm.Keys(), keys) 41 | 42 | sm.Delete("b") 43 | r.Equal([]string{"a", "b"}, keys) 44 | 45 | sm.Delete("b") 46 | _, ok = sm.Load("b") 47 | r.False(ok) 48 | p := plug{ 49 | BuffaloCommand: "foo", 50 | } 51 | func(m *plugMap) { 52 | m.Store("c", p) 53 | }(sm) 54 | s, ok = sm.Load("c") 55 | r.True(ok) 56 | r.Equal(p, s) 57 | } 58 | -------------------------------------------------------------------------------- /plugins/plugdeps/plugins_test.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_Plugins_Encode(t *testing.T) { 13 | r := require.New(t) 14 | 15 | bb := &bytes.Buffer{} 16 | 17 | plugs := New() 18 | plugs.Add(pop, heroku, local) 19 | 20 | r.NoError(plugs.Encode(bb)) 21 | 22 | fmt.Println(bb.String()) 23 | act := strings.TrimSpace(bb.String()) 24 | exp := strings.TrimSpace(eToml) 25 | r.Equal(exp, act) 26 | } 27 | 28 | func Test_Plugins_Decode(t *testing.T) { 29 | r := require.New(t) 30 | 31 | plugs := New() 32 | r.NoError(plugs.Decode(strings.NewReader(eToml))) 33 | 34 | names := []string{"buffalo-hello.rb", "buffalo-heroku", "buffalo-pop"} 35 | list := plugs.List() 36 | 37 | r.Len(list, len(names)) 38 | for i, p := range list { 39 | r.Equal(names[i], p.Binary) 40 | } 41 | } 42 | 43 | func Test_Plugins_Remove(t *testing.T) { 44 | r := require.New(t) 45 | 46 | plugs := New() 47 | plugs.Add(pop, heroku) 48 | r.Len(plugs.List(), 2) 49 | plugs.Remove(pop) 50 | r.Len(plugs.List(), 1) 51 | } 52 | -------------------------------------------------------------------------------- /internal/defaults/defaults_test.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_String(t *testing.T) { 10 | a := assert.New(t) 11 | 12 | a.Equal(String("", "foo"), "foo") 13 | a.Equal(String("bar", "foo"), "bar") 14 | var s string 15 | a.Equal(String(s, "foo"), "foo") 16 | } 17 | 18 | func Test_Int(t *testing.T) { 19 | a := assert.New(t) 20 | 21 | a.Equal(Int(0, 1), 1) 22 | a.Equal(Int(2, 1), 2) 23 | var s int 24 | a.Equal(Int(s, 1), 1) 25 | } 26 | 27 | func Test_Int64(t *testing.T) { 28 | a := assert.New(t) 29 | 30 | a.Equal(Int64(0, 1), int64(1)) 31 | a.Equal(Int64(2, 1), int64(2)) 32 | var s int64 33 | a.Equal(Int64(s, 1), int64(1)) 34 | } 35 | 36 | func Test_Float32(t *testing.T) { 37 | a := assert.New(t) 38 | 39 | a.Equal(Float32(0, 1), float32(1)) 40 | a.Equal(Float32(2, 1), float32(2)) 41 | var s float32 42 | a.Equal(Float32(s, 1), float32(1)) 43 | } 44 | 45 | func Test_Float64(t *testing.T) { 46 | a := assert.New(t) 47 | 48 | a.Equal(Float64(0, 1), float64(1)) 49 | a.Equal(Float64(2, 1), float64(2)) 50 | var s float64 51 | a.Equal(Float64(s, 1), float64(1)) 52 | } 53 | -------------------------------------------------------------------------------- /mail/internal/mail/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexandre Cesaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /mail/internal/mail/auth.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/smtp" 7 | ) 8 | 9 | // loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism. 10 | type loginAuth struct { 11 | username string 12 | password string 13 | host string 14 | } 15 | 16 | func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { 17 | if !server.TLS { 18 | advertised := false 19 | for _, mechanism := range server.Auth { 20 | if mechanism == "LOGIN" { 21 | advertised = true 22 | break 23 | } 24 | } 25 | if !advertised { 26 | return "", nil, fmt.Errorf("gomail: unencrypted connection") 27 | } 28 | } 29 | if server.Name != a.host { 30 | return "", nil, fmt.Errorf("gomail: wrong host name") 31 | } 32 | return "LOGIN", nil, nil 33 | } 34 | 35 | func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { 36 | if !more { 37 | return nil, nil 38 | } 39 | 40 | switch { 41 | case bytes.Equal(fromServer, []byte("Username:")): 42 | return []byte(a.username), nil 43 | case bytes.Equal(fromServer, []byte("Password:")): 44 | return []byte(a.password), nil 45 | default: 46 | return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /render/string.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type stringRenderer struct { 9 | *Engine 10 | body string 11 | } 12 | 13 | func (s stringRenderer) ContentType() string { 14 | return "text/plain; charset=utf-8" 15 | } 16 | 17 | func (s stringRenderer) Render(w io.Writer, data Data) error { 18 | te, ok := s.TemplateEngines["text"] 19 | if !ok { 20 | return fmt.Errorf("could not find a template engine for text") 21 | } 22 | t, err := te(s.body, data, s.Helpers) 23 | if err != nil { 24 | return err 25 | } 26 | _, err = w.Write([]byte(t)) 27 | return err 28 | } 29 | 30 | // String renderer that will run the string through 31 | // the github.com/gobuffalo/plush package and return 32 | // "text/plain" as the content type. 33 | func String(s string, args ...interface{}) Renderer { 34 | e := New(Options{}) 35 | return e.String(s, args...) 36 | } 37 | 38 | // String renderer that will run the string through 39 | // the github.com/gobuffalo/plush package and return 40 | // "text/plain" as the content type. 41 | func (e *Engine) String(s string, args ...interface{}) Renderer { 42 | if len(args) > 0 { 43 | s = fmt.Sprintf(s, args...) 44 | } 45 | return stringRenderer{ 46 | Engine: e, 47 | body: s, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /render/func.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "io" 4 | 5 | // RendererFunc is the interface for the the function 6 | // needed by the Func renderer. 7 | type RendererFunc func(io.Writer, Data) error 8 | 9 | type funcRenderer struct { 10 | contentType string 11 | renderFunc RendererFunc 12 | } 13 | 14 | // ContentType returns the content type for this render. 15 | // Examples would be "text/html" or "application/json". 16 | func (s funcRenderer) ContentType() string { 17 | return s.contentType 18 | } 19 | 20 | // Render the provided Data to the provider Writer using the 21 | // RendererFunc provide. 22 | func (s funcRenderer) Render(w io.Writer, data Data) error { 23 | return s.renderFunc(w, data) 24 | } 25 | 26 | // Func renderer allows for easily building one of renderers 27 | // using just a RendererFunc and not having to build a whole 28 | // implementation of the Render interface. 29 | func Func(s string, fn RendererFunc) Renderer { 30 | return funcRenderer{ 31 | contentType: s, 32 | renderFunc: fn, 33 | } 34 | } 35 | 36 | // Func renderer allows for easily building one of renderers 37 | // using just a RendererFunc and not having to build a whole 38 | // implementation of the Render interface. 39 | func (e *Engine) Func(s string, fn RendererFunc) Renderer { 40 | return Func(s, fn) 41 | } 42 | -------------------------------------------------------------------------------- /plugins/plugcmds/available_test.go: -------------------------------------------------------------------------------- 1 | package plugcmds 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gobuffalo/events" 9 | "github.com/spf13/cobra" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_Available_Add(t *testing.T) { 14 | r := require.New(t) 15 | 16 | a := NewAvailable() 17 | err := a.Add("generate", &cobra.Command{ 18 | Use: "foo", 19 | Short: "generates foo", 20 | Aliases: []string{"f"}, 21 | }) 22 | r.NoError(err) 23 | 24 | r.Len(a.Commands(), 2) 25 | } 26 | 27 | func Test_Available_Encode(t *testing.T) { 28 | r := require.New(t) 29 | 30 | bb := &bytes.Buffer{} 31 | 32 | a := NewAvailable() 33 | err := a.Add("generate", &cobra.Command{ 34 | Use: "foo", 35 | Short: "generates foo", 36 | Aliases: []string{"f"}, 37 | }) 38 | r.NoError(err) 39 | 40 | r.NoError(a.Encode(bb)) 41 | const exp = `[{"name":"foo","use_command":"foo","buffalo_command":"generate","description":"generates foo","aliases":["f"]}]` 42 | r.Equal(exp, strings.TrimSpace(bb.String())) 43 | } 44 | 45 | func Test_Available_Listen(t *testing.T) { 46 | r := require.New(t) 47 | 48 | a := NewAvailable() 49 | err := a.Listen(func(e events.Event) error { 50 | return nil 51 | }) 52 | r.NoError(err) 53 | 54 | r.Len(a.Commands(), 2) 55 | } 56 | -------------------------------------------------------------------------------- /route_info_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/gobuffalo/buffalo/render" 10 | "github.com/gobuffalo/httptest" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_RouteInfo_ServeHTTP_SQL_Error(t *testing.T) { 15 | r := require.New(t) 16 | 17 | app := New(Options{}) 18 | app.GET("/good", func(c Context) error { 19 | return c.Render(http.StatusOK, render.String("hi")) 20 | }) 21 | 22 | app.GET("/bad", func(c Context) error { 23 | return sql.ErrNoRows 24 | }) 25 | 26 | app.GET("/bad-2", func(c Context) error { 27 | return sql.ErrTxDone 28 | }) 29 | 30 | app.GET("/gone-unwrap", func(c Context) error { 31 | return c.Error(http.StatusGone, sql.ErrTxDone) 32 | }) 33 | 34 | app.GET("/gone-wrap", func(c Context) error { 35 | return c.Error(http.StatusGone, fmt.Errorf("some error wrapping here: %w", sql.ErrNoRows)) 36 | }) 37 | 38 | w := httptest.New(app) 39 | 40 | res := w.HTML("/good").Get() 41 | r.Equal(http.StatusOK, res.Code) 42 | 43 | res = w.HTML("/bad").Get() 44 | r.Equal(http.StatusNotFound, res.Code) 45 | 46 | res = w.HTML("/gone-wrap").Get() 47 | r.Equal(http.StatusGone, res.Code) 48 | 49 | res = w.HTML("/gone-unwrap").Get() 50 | r.Equal(http.StatusGone, res.Code) 51 | } 52 | -------------------------------------------------------------------------------- /binding/decoders/null_time_test.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gobuffalo/nulls" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_NullTimeCustomDecoder_Decode(t *testing.T) { 12 | r := require.New(t) 13 | 14 | testCases := []struct { 15 | input string 16 | expected time.Time 17 | expValid bool 18 | expectErr bool 19 | }{ 20 | { 21 | input: "2017-01-01", 22 | expected: time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC), 23 | expValid: true, 24 | }, 25 | { 26 | input: "2018-07-13T15:34", 27 | expected: time.Date(2018, time.July, 13, 15, 34, 0, 0, time.UTC), 28 | expValid: true, 29 | }, 30 | { 31 | input: "2018-20-10T30:15", 32 | expected: time.Time{}, 33 | expValid: false, 34 | }, 35 | { 36 | input: "", 37 | expected: time.Time{}, 38 | expValid: false, 39 | }, 40 | } 41 | 42 | for _, testCase := range testCases { 43 | 44 | tt, err := NullTimeDecoderFn()([]string{testCase.input}) 45 | r.IsType(tt, nulls.Time{}) 46 | nt := tt.(nulls.Time) 47 | 48 | if testCase.expectErr { 49 | r.Error(err) 50 | r.Equal(nt.Valid, false) 51 | continue 52 | } 53 | 54 | r.Equal(testCase.expected, nt.Time) 55 | r.Equal(testCase.expValid, nt.Valid) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /render/markdown_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/psanford/memfs" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const mdTemplate = "my-template.md" 13 | 14 | func Test_MD_WithoutLayout(t *testing.T) { 15 | r := require.New(t) 16 | 17 | rootFS := memfs.New() 18 | r.NoError(rootFS.WriteFile(mdTemplate, []byte("<%= name %>"), 0644)) 19 | 20 | e := NewEngine() 21 | e.TemplatesFS = rootFS 22 | 23 | h := e.HTML(mdTemplate) 24 | r.Equal("text/html; charset=utf-8", h.ContentType()) 25 | bb := &bytes.Buffer{} 26 | 27 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 28 | r.Equal("

Mark

", strings.TrimSpace(bb.String())) 29 | } 30 | 31 | func Test_MD_WithLayout(t *testing.T) { 32 | r := require.New(t) 33 | 34 | rootFS := memfs.New() 35 | r.NoError(rootFS.WriteFile(mdTemplate, []byte("<%= name %>"), 0644)) 36 | r.NoError(rootFS.WriteFile(htmlLayout, []byte("<%= yield %>"), 0644)) 37 | 38 | e := NewEngine() 39 | e.TemplatesFS = rootFS 40 | e.HTMLLayout = htmlLayout 41 | 42 | h := e.HTML(mdTemplate) 43 | r.Equal("text/html; charset=utf-8", h.ContentType()) 44 | bb := &bytes.Buffer{} 45 | 46 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 47 | r.Equal("

Mark

\n", strings.TrimSpace(bb.String())) 48 | } 49 | -------------------------------------------------------------------------------- /render/js.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | // JavaScript renders the named files using the 'application/javascript' 4 | // content type and the github.com/gobuffalo/plush 5 | // package for templating. If more than 1 file is provided 6 | // the second file will be considered a "layout" file 7 | // and the first file will be the "content" file which will 8 | // be placed into the "layout" using "<%= yield %>". 9 | func JavaScript(names ...string) Renderer { 10 | e := New(Options{}) 11 | return e.JavaScript(names...) 12 | } 13 | 14 | // JavaScript renders the named files using the 'application/javascript' 15 | // content type and the github.com/gobuffalo/plush 16 | // package for templating. If more than 1 file is provided 17 | // the second file will be considered a "layout" file 18 | // and the first file will be the "content" file which will 19 | // be placed into the "layout" using "<%= yield %>". If no 20 | // second file is provided and an `JavaScriptLayout` is specified 21 | // in the options, then that layout file will be used 22 | // automatically. 23 | func (e *Engine) JavaScript(names ...string) Renderer { 24 | if e.JavaScriptLayout != "" && len(names) == 1 { 25 | names = append(names, e.JavaScriptLayout) 26 | } 27 | hr := &templateRenderer{ 28 | Engine: e, 29 | contentType: "application/javascript", 30 | names: names, 31 | } 32 | return hr 33 | } 34 | -------------------------------------------------------------------------------- /binding/decoders/time_test.go: -------------------------------------------------------------------------------- 1 | package decoders 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseTimeErrorParsing(t *testing.T) { 11 | r := require.New(t) 12 | 13 | _, err := parseTime([]string{"this is sparta"}) 14 | r.Error(err) 15 | } 16 | 17 | func TestParseTime(t *testing.T) { 18 | r := require.New(t) 19 | 20 | testCases := []struct { 21 | input string 22 | expected time.Time 23 | expectErr bool 24 | }{ 25 | { 26 | input: "2017-01-01", 27 | expected: time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC), 28 | expectErr: false, 29 | }, 30 | { 31 | input: "2018-07-13T15:34", 32 | expected: time.Date(2018, time.July, 13, 15, 34, 0, 0, time.UTC), 33 | expectErr: false, 34 | }, 35 | { 36 | input: "2018-20-10T30:15", 37 | expected: time.Time{}, 38 | expectErr: true, 39 | }, 40 | } 41 | 42 | for _, tc := range testCases { 43 | tt, err := parseTime([]string{tc.input}) 44 | if !tc.expectErr { 45 | r.NoError(err) 46 | } 47 | 48 | r.Equal(tc.expected, tt) 49 | } 50 | } 51 | 52 | func TestParseTimeConflicting(t *testing.T) { 53 | r := require.New(t) 54 | 55 | RegisterTimeFormats("2006-02-01") 56 | tt, err := parseTime([]string{"2017-01-10"}) 57 | 58 | r.NoError(err) 59 | expected := time.Date(2017, time.October, 1, 0, 0, 0, 0, time.UTC) 60 | r.Equal(expected, tt) 61 | } 62 | -------------------------------------------------------------------------------- /plugins/plugins_test.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gobuffalo/envy" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestAskBin_respectsTimeout(t *testing.T) { 15 | r := require.New(t) 16 | 17 | from, err := envy.MustGet("BUFFALO_PLUGIN_PATH") 18 | if err != nil { 19 | t.Skipf("BUFFALO_PLUGIN_PATH not set.") 20 | return 21 | } 22 | 23 | if fileEntries, err := os.ReadDir(from); err == nil { 24 | found := false 25 | for _, e := range fileEntries { 26 | if strings.HasPrefix(e.Name(), "buffalo-") { 27 | from = e.Name() 28 | found = true 29 | break 30 | } 31 | } 32 | if !found { 33 | t.Skipf("no plugins found") 34 | return 35 | } 36 | } else { 37 | r.Error(err, "plugin path not able to be read") 38 | return 39 | } 40 | 41 | const tooShort = time.Millisecond 42 | impossible, cancel := context.WithTimeout(context.Background(), tooShort) 43 | defer cancel() 44 | 45 | done := make(chan struct{}) 46 | go func() { 47 | askBin(impossible, from) 48 | close(done) 49 | }() 50 | 51 | select { 52 | case <-time.After(tooShort + 80*time.Millisecond): 53 | r.Fail("did not time-out quickly enough") 54 | case <-done: 55 | t.Log("timed-out successfully") 56 | } 57 | 58 | if _, ok := findInCache(from); ok { 59 | r.Fail("expected plugin not to be added to cache on failure, but it was in cache") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | EXPOSE 3000 4 | 5 | ENV GOPROXY=https://proxy.golang.org 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y -q build-essential sqlite3 libsqlite3-dev postgresql libpq-dev vim 9 | 10 | # Installing Node 12 11 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash 12 | RUN apt-get update && apt-get install nodejs 13 | 14 | # Installing Postgres 15 | RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' \ 16 | && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | apt-key add - \ 17 | && apt-get update \ 18 | && apt-get install -y -q postgresql postgresql-contrib libpq-dev\ 19 | && rm -rf /var/lib/apt/lists/* \ 20 | && service postgresql start && \ 21 | # Setting up password for postgres 22 | su -c "psql -c \"ALTER USER postgres WITH PASSWORD 'postgres';\"" - postgres 23 | 24 | # Installing yarn 25 | RUN npm install -g --no-progress yarn \ 26 | && yarn config set yarn-offline-mirror /npm-packages-offline-cache \ 27 | && yarn config set yarn-offline-mirror-pruning true 28 | 29 | # Install golangci 30 | RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 31 | # Installing buffalo binary 32 | RUN go install github.com/gobuffalo/cli/cmd/buffalo@latest 33 | RUN go get github.com/gobuffalo/buffalo-pop/v2 34 | 35 | RUN mkdir /src 36 | WORKDIR /src 37 | -------------------------------------------------------------------------------- /worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Handler function that will be run by the worker and given 9 | // a slice of arguments 10 | type Handler func(Args) error 11 | 12 | // Worker interface that needs to be implemented to be considered 13 | // a "worker" 14 | type Worker interface { 15 | // Start the worker with the given context 16 | Start(context.Context) error 17 | // Stop the worker 18 | Stop() error 19 | // Perform a job as soon as possibly 20 | Perform(Job) error 21 | // PerformAt performs a job at a particular time 22 | PerformAt(Job, time.Time) error 23 | // PerformIn performs a job after waiting for a specified amount of time 24 | PerformIn(Job, time.Duration) error 25 | // Register a Handler 26 | Register(string, Handler) error 27 | } 28 | 29 | /* TODO(sio4): #road-to-v1 - redefine Worker interface clearer 30 | 1. The Start() functions of current implementations including Simple, 31 | Gocraft Work Adapter do not block and immediately return the error. 32 | However, App.Serve() calls them within a go routine. 33 | 2. The Perform() family of functions can be called before the worker 34 | was started once the worker configured. Could be fine but there should 35 | be some guidiance for its usage. 36 | 3. The Perform() function could be interpreted as "Do it" by its name but 37 | their actual job is "Enqueue it" even though Simple worker has no clear 38 | boundary between them. It could make confusion. 39 | */ 40 | -------------------------------------------------------------------------------- /flash.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import "encoding/json" 4 | 5 | // flashKey is the prefix inside the Session. 6 | const flashKey = "_flash_" 7 | 8 | //Flash is a struct that helps with the operations over flash messages. 9 | type Flash struct { 10 | data map[string][]string 11 | } 12 | 13 | //Delete removes a particular key from the Flash. 14 | func (f Flash) Delete(key string) { 15 | delete(f.data, key) 16 | } 17 | 18 | //Clear removes all keys from the Flash. 19 | func (f *Flash) Clear() { 20 | f.data = map[string][]string{} 21 | } 22 | 23 | //Set allows to set a list of values into a particular key. 24 | func (f Flash) Set(key string, values []string) { 25 | f.data[key] = values 26 | } 27 | 28 | //Add adds a flash value for a flash key, if the key already has values the list for that value grows. 29 | func (f Flash) Add(key, value string) { 30 | if len(f.data[key]) == 0 { 31 | f.data[key] = []string{value} 32 | return 33 | } 34 | 35 | f.data[key] = append(f.data[key], value) 36 | } 37 | 38 | //Persist the flash inside the session. 39 | func (f Flash) persist(session *Session) { 40 | b, _ := json.Marshal(f.data) 41 | session.Set(flashKey, b) 42 | } 43 | 44 | //newFlash creates a new Flash and loads the session data inside its data. 45 | func newFlash(session *Session) *Flash { 46 | result := &Flash{ 47 | data: map[string][]string{}, 48 | } 49 | 50 | if session.Session != nil { 51 | if f := session.Get(flashKey); f != nil { 52 | json.Unmarshal(f.([]byte), &result.data) 53 | } 54 | } 55 | return result 56 | } 57 | -------------------------------------------------------------------------------- /render/options.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "github.com/gobuffalo/plush/v5/helpers/hctx" 7 | ) 8 | 9 | // Helpers to be included in all templates 10 | type Helpers hctx.Map 11 | 12 | // Options for render.Engine 13 | type Options struct { 14 | // HTMLLayout is the default layout to be used with all HTML renders. 15 | HTMLLayout string 16 | 17 | // JavaScriptLayout is the default layout to be used with all JavaScript renders. 18 | JavaScriptLayout string 19 | 20 | // TemplateFS is the fs.FS that holds the templates 21 | TemplatesFS fs.FS 22 | 23 | // AssetsFS is the fs.FS that holds the of the public assets the app will serve. 24 | AssetsFS fs.FS 25 | 26 | // Helpers to be rendered with the templates 27 | Helpers Helpers 28 | 29 | // TemplateEngine to be used for rendering HTML templates 30 | TemplateEngines map[string]TemplateEngine 31 | 32 | // DefaultContentType instructs the engine what it should fall back to if 33 | // the "content-type" is unknown 34 | DefaultContentType string 35 | 36 | // TemplateMetadataKeys allows users to specify custom keys for template metadata 37 | // If nil, uses default Buffalo metadata approach 38 | TemplateMetadataKeys map[string]string 39 | 40 | TemplateBaseDir string 41 | } 42 | 43 | // Default metadata keys 44 | var defaultTemplateMetadataKeys = map[string]string{ 45 | "template_file": "_buffalo_template_file", 46 | "base_name": "_buffalo_base_name", 47 | "extension": "_buffalo_extension", 48 | "last_modified": "_buffalo_last_modification", 49 | } 50 | -------------------------------------------------------------------------------- /home.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | ) 6 | 7 | /* TODO: consider to split out Home (or Router, whatever) from App #road-to-v1 8 | Group and Domain based multi-homing are actually not an App if the concept 9 | of the App represents the application. The App should be only one for whole 10 | application. 11 | 12 | For an extreme example, App.Group().Stop() or even App.Group().Serve() are 13 | still valid function calls while they should not be allowed and the result 14 | could be strage. 15 | */ 16 | 17 | // Home is a container for Domains and Groups that independently serves a 18 | // group of pages with its own Middleware and ErrorHandlers. It is usually 19 | // a multi-homed server domain or group of paths under a certain prefix. 20 | // 21 | // While the App is for managing whole application life cycle along with its 22 | // default Home, including initializing and stopping its all components such 23 | // as listeners and long-running jobs, Home is only for a specific group of 24 | // services to serve its service logic efficiently. 25 | type Home struct { 26 | app *App // will replace App.root 27 | appSelf *App // temporary while the App is in action. 28 | // replace Options' Name, Host, and Prefix 29 | name string 30 | host string 31 | prefix string 32 | 33 | // moved from App 34 | // Middleware returns the current MiddlewareStack for the App/Group. 35 | Middleware *MiddlewareStack `json:"-"` 36 | ErrorHandlers ErrorHandlers `json:"-"` 37 | router *mux.Router 38 | filepaths []string 39 | } 40 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gobuffalo/envy" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestOptions_NewOptions(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | env string 16 | secret string 17 | expectErr string 18 | }{ 19 | {name: "Development doesn't fail with no secret", env: "development", secret: "", expectErr: "securecookie:"}, 20 | {name: "Development doesn't fail with secret set", env: "development", secret: "secrets", expectErr: "securecookie:"}, 21 | {name: "Test doesn't fail with secret set", env: "test", secret: "", expectErr: "securecookie:"}, 22 | {name: "Test doesn't fail with secret set", env: "test", secret: "secrets", expectErr: "securecookie:"}, 23 | {name: "Production fails with no secret", env: "production", secret: "", expectErr: "securecookie:"}, 24 | {name: "Production doesn't fail with secret set", env: "production", secret: "secrets", expectErr: "securecookie:"}, 25 | } 26 | 27 | for _, test := range tests { 28 | t.Run(test.name, func(t *testing.T) { 29 | r := require.New(t) 30 | envy.Temp(func() { 31 | envy.Set("GO_ENV", test.env) 32 | envy.Set("SESSION_SECRET", test.secret) 33 | 34 | opts := NewOptions() 35 | 36 | req, _ := http.NewRequest("GET", "/", strings.NewReader("")) 37 | req.AddCookie(&http.Cookie{Name: "_buffalo_session"}) 38 | 39 | _, err := opts.SessionStore.New(req, "_buffalo_session") 40 | 41 | r.Error(err) 42 | r.Contains(err.Error(), test.expectErr) 43 | }) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /render/template_helpers.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "io" 7 | "path" 8 | "path/filepath" 9 | 10 | ht "github.com/gobuffalo/helpers/tags" 11 | "github.com/gobuffalo/tags/v3" 12 | ) 13 | 14 | type helperTag struct { 15 | name string 16 | fn func(string, tags.Options) template.HTML 17 | } 18 | 19 | func (s *templateRenderer) addAssetsHelpers(helpers Helpers) Helpers { 20 | helpers["assetPath"] = s.assetPath 21 | 22 | ah := []helperTag{ 23 | {"javascriptTag", ht.JS}, 24 | {"stylesheetTag", ht.CSS}, 25 | {"imgTag", ht.Img}, 26 | } 27 | 28 | for _, h := range ah { 29 | func(h helperTag) { 30 | helpers[h.name] = func(file string, options tags.Options) (template.HTML, error) { 31 | if options == nil { 32 | options = tags.Options{} 33 | } 34 | f, err := s.assetPath(file) 35 | if err != nil { 36 | return "", err 37 | } 38 | return h.fn(f, options), nil 39 | } 40 | }(h) 41 | } 42 | 43 | return helpers 44 | } 45 | 46 | var assetMap = stringMap{} 47 | 48 | func assetPathFor(file string) string { 49 | filePath, ok := assetMap.Load(file) 50 | if filePath == "" || !ok { 51 | filePath = file 52 | } 53 | return path.Join("/assets", filePath) 54 | } 55 | 56 | func loadManifest(manifest io.Reader) error { 57 | m := map[string]string{} 58 | 59 | if err := json.NewDecoder(manifest).Decode(&m); err != nil { 60 | return err 61 | } 62 | for k, v := range m { 63 | // I don't think v has backslash but if so, correct them when 64 | // creating the map instead using the value in `assetPathFor()`. 65 | assetMap.Store(k, filepath.ToSlash(v)) 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /not_found_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/gobuffalo/httptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_App_Dev_NotFound(t *testing.T) { 13 | r := require.New(t) 14 | 15 | a := New(Options{}) 16 | a.Env = "development" 17 | a.GET("/foo", func(c Context) error { return nil }) 18 | 19 | w := httptest.New(a) 20 | res := w.HTML("/bad").Get() 21 | 22 | body := res.Body.String() 23 | r.Contains(body, "404 - ERROR!") 24 | r.Contains(body, "/foo") 25 | r.Equal(http.StatusNotFound, res.Code) 26 | } 27 | 28 | func Test_App_Dev_NotFound_JSON(t *testing.T) { 29 | r := require.New(t) 30 | 31 | a := New(Options{}) 32 | a.Env = "development" 33 | a.GET("/foo", func(c Context) error { return nil }) 34 | 35 | w := httptest.New(a) 36 | res := w.JSON("/bad").Get() 37 | r.Equal(http.StatusNotFound, res.Code) 38 | 39 | jb := map[string]interface{}{} 40 | err := json.NewDecoder(res.Body).Decode(&jb) 41 | r.NoError(err) 42 | r.Equal(float64(http.StatusNotFound), jb["code"]) 43 | } 44 | 45 | func Test_App_Override_NotFound(t *testing.T) { 46 | r := require.New(t) 47 | 48 | a := New(Options{}) 49 | a.ErrorHandlers[http.StatusNotFound] = func(status int, err error, c Context) error { 50 | c.Response().WriteHeader(http.StatusNotFound) 51 | c.Response().Write([]byte("oops!!!")) 52 | return nil 53 | } 54 | a.GET("/foo", func(c Context) error { return nil }) 55 | 56 | w := httptest.New(a) 57 | res := w.HTML("/bad").Get() 58 | r.Equal(http.StatusNotFound, res.Code) 59 | 60 | body := res.Body.String() 61 | r.Equal(body, "oops!!!") 62 | r.NotContains(body, "/foo") 63 | } 64 | -------------------------------------------------------------------------------- /plugins.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gobuffalo/buffalo/plugins" 12 | "github.com/gobuffalo/envy" 13 | "github.com/gobuffalo/events" 14 | ) 15 | 16 | var loadPlugins sync.Once 17 | 18 | // LoadPlugins will add listeners for any plugins that support "events" 19 | func LoadPlugins() error { 20 | var errResult error 21 | loadPlugins.Do(func() { 22 | // don't send plugins events during testing 23 | if envy.Get("GO_ENV", "development") == "test" { 24 | return 25 | } 26 | plugs, err := plugins.Available() 27 | if err != nil { 28 | errResult = err 29 | return 30 | } 31 | for _, cmds := range plugs { 32 | for _, c := range cmds { 33 | if c.BuffaloCommand != "events" { 34 | continue 35 | } 36 | err := func(c plugins.Command) error { 37 | n := fmt.Sprintf("[PLUGIN] %s %s", c.Binary, c.Name) 38 | fn := func(e events.Event) { 39 | b, err := json.Marshal(e) 40 | if err != nil { 41 | fmt.Println("error trying to marshal event", e, err) 42 | return 43 | } 44 | cmd := exec.Command(c.Binary, c.UseCommand, string(b)) 45 | cmd.Stderr = os.Stderr 46 | cmd.Stdout = os.Stdout 47 | cmd.Stdin = os.Stdin 48 | if err := cmd.Run(); err != nil { 49 | fmt.Println("error trying to send event", strings.Join(cmd.Args, " "), err) 50 | } 51 | } 52 | _, err := events.NamedListen(n, events.Filter(c.ListenFor, fn)) 53 | return err 54 | }(c) 55 | if err != nil { 56 | errResult = err 57 | return 58 | } 59 | } 60 | } 61 | }) 62 | return errResult 63 | } 64 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | // Response implements the http.ResponseWriter interface and allows 12 | // for the capture of the response status and size to be used for things 13 | // like logging requests. 14 | type Response struct { 15 | Status int 16 | Size int 17 | http.ResponseWriter 18 | } 19 | 20 | // WriteHeader sets the status code for a response 21 | func (w *Response) WriteHeader(code int) { 22 | if code == w.Status { 23 | return 24 | } 25 | 26 | if w.Status > 0 { 27 | fmt.Printf("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.Status, code) 28 | return 29 | } 30 | 31 | w.Status = code 32 | w.ResponseWriter.WriteHeader(code) 33 | } 34 | 35 | // Write the body of the response 36 | func (w *Response) Write(b []byte) (int, error) { 37 | w.Size = binary.Size(b) 38 | return w.ResponseWriter.Write(b) 39 | } 40 | 41 | // Hijack implements the http.Hijacker interface to allow for things like websockets. 42 | func (w *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { 43 | if hj, ok := w.ResponseWriter.(http.Hijacker); ok { 44 | return hj.Hijack() 45 | } 46 | return nil, nil, fmt.Errorf("does not implement http.Hijack") 47 | } 48 | 49 | // Flush the response 50 | func (w *Response) Flush() { 51 | if f, ok := w.ResponseWriter.(http.Flusher); ok { 52 | f.Flush() 53 | } 54 | } 55 | 56 | type closeNotifier interface { 57 | CloseNotify() <-chan bool 58 | } 59 | 60 | // CloseNotify implements the http.CloseNotifier interface 61 | func (w *Response) CloseNotify() <-chan bool { 62 | if cn, ok := w.ResponseWriter.(closeNotifier); ok { 63 | return cn.CloseNotify() 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/sessions" 7 | ) 8 | 9 | // Session wraps the "github.com/gorilla/sessions" API 10 | // in something a little cleaner and a bit more useable. 11 | type Session struct { 12 | Session *sessions.Session 13 | req *http.Request 14 | res http.ResponseWriter 15 | } 16 | 17 | // Save the current session. 18 | func (s *Session) Save() error { 19 | return s.Session.Save(s.req, s.res) 20 | } 21 | 22 | // Get a value from the current session. 23 | func (s *Session) Get(name interface{}) interface{} { 24 | return s.Session.Values[name] 25 | } 26 | 27 | // GetOnce gets a value from the current session and then deletes it. 28 | func (s *Session) GetOnce(name interface{}) interface{} { 29 | if x, ok := s.Session.Values[name]; ok { 30 | s.Delete(name) 31 | return x 32 | } 33 | return nil 34 | } 35 | 36 | // Set a value onto the current session. If a value with that name 37 | // already exists it will be overridden with the new value. 38 | func (s *Session) Set(name, value interface{}) { 39 | s.Session.Values[name] = value 40 | } 41 | 42 | // Delete a value from the current session. 43 | func (s *Session) Delete(name interface{}) { 44 | delete(s.Session.Values, name) 45 | } 46 | 47 | // Clear the current session 48 | func (s *Session) Clear() { 49 | for k := range s.Session.Values { 50 | s.Delete(k) 51 | } 52 | } 53 | 54 | // Get a session using a request and response. 55 | func (a *App) getSession(r *http.Request, w http.ResponseWriter) *Session { 56 | if a.root != nil { 57 | return a.root.getSession(r, w) 58 | } 59 | session, _ := a.SessionStore.Get(r, a.SessionName) 60 | return &Session{ 61 | Session: session, 62 | req: r, 63 | res: w, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /render/string_map.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | // stringMap wraps sync.Map and uses the following types: 9 | // key: string 10 | // value: string 11 | type stringMap struct { 12 | data sync.Map 13 | } 14 | 15 | // Delete the key from the map 16 | func (m *stringMap) Delete(key string) { 17 | m.data.Delete(key) 18 | } 19 | 20 | // Load the key from the map. 21 | // Returns string or bool. 22 | // A false return indicates either the key was not found 23 | // or the value is not of type string 24 | func (m *stringMap) Load(key string) (string, bool) { 25 | i, ok := m.data.Load(key) 26 | if !ok { 27 | return ``, false 28 | } 29 | s, ok := i.(string) 30 | return s, ok 31 | } 32 | 33 | // LoadOrStore will return an existing key or 34 | // store the value if not already in the map 35 | func (m *stringMap) LoadOrStore(key string, value string) (string, bool) { 36 | i, _ := m.data.LoadOrStore(key, value) 37 | s, ok := i.(string) 38 | return s, ok 39 | } 40 | 41 | // Range over the string values in the map 42 | func (m *stringMap) Range(f func(key string, value string) bool) { 43 | m.data.Range(func(k, v interface{}) bool { 44 | key, ok := k.(string) 45 | if !ok { 46 | return false 47 | } 48 | value, ok := v.(string) 49 | if !ok { 50 | return false 51 | } 52 | return f(key, value) 53 | }) 54 | } 55 | 56 | // Store a string in the map 57 | func (m *stringMap) Store(key string, value string) { 58 | m.data.Store(key, value) 59 | } 60 | 61 | // Keys returns a list of keys in the map 62 | func (m *stringMap) Keys() []string { 63 | var keys []string 64 | m.Range(func(key string, value string) bool { 65 | keys = append(keys, key) 66 | return true 67 | }) 68 | sort.Strings(keys) 69 | return keys 70 | } 71 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gobuffalo/buffalo/render" 10 | "github.com/gobuffalo/httptest" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func Test_Session_SingleCookie(t *testing.T) { 16 | r := require.New(t) 17 | 18 | sessionName := "_test_session" 19 | a := New(Options{SessionName: sessionName}) 20 | rr := render.New(render.Options{}) 21 | 22 | a.GET("/", func(c Context) error { 23 | return c.Render(http.StatusCreated, rr.String("")) 24 | }) 25 | 26 | w := httptest.New(a) 27 | res := w.HTML("/").Get() 28 | 29 | var sessionCookies []string 30 | for _, c := range res.Header().Values("Set-Cookie") { 31 | if strings.HasPrefix(c, sessionName) { 32 | sessionCookies = append(sessionCookies, c) 33 | } 34 | } 35 | 36 | r.Equal(1, len(sessionCookies)) 37 | } 38 | 39 | func Test_Session_CustomValue(t *testing.T) { 40 | r := require.New(t) 41 | 42 | a := New(Options{}) 43 | rr := render.New(render.Options{}) 44 | 45 | // Root path sets a custom session value 46 | a.GET("/", func(c Context) error { 47 | c.Session().Set("example", "test") 48 | return c.Render(http.StatusCreated, rr.String("")) 49 | }) 50 | // /session path prints custom session value as response 51 | a.GET("/session", func(c Context) error { 52 | sessionValue := c.Session().Get("example") 53 | return c.Render(http.StatusCreated, rr.String(fmt.Sprintf("%s", sessionValue))) 54 | }) 55 | 56 | w := httptest.New(a) 57 | _ = w.HTML("/").Get() 58 | 59 | // Create second request that should contain the cookie from the first response 60 | reqGetSession := w.HTML("/session") 61 | resGetSession := reqGetSession.Get() 62 | 63 | r.Equal(resGetSession.Body.String(), "test") 64 | } 65 | -------------------------------------------------------------------------------- /binding/binding_test.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type blogPost struct { 12 | Tags []string 13 | Dislikes int 14 | Likes int32 15 | } 16 | 17 | func Test_Register(t *testing.T) { 18 | r := require.New(t) 19 | 20 | Register("foo/bar", func(*http.Request, interface{}) error { 21 | return nil 22 | }) 23 | 24 | r.NotNil(BaseRequestBinder.binders["foo/bar"]) 25 | 26 | req, err := http.NewRequest("POST", "/", nil) 27 | r.NoError(err) 28 | 29 | req.Header.Set("Content-Type", "foo/bar") 30 | req.Form = url.Values{ 31 | "Tags": []string{"AAA"}, 32 | "Likes": []string{"12"}, 33 | "Dislikes": []string{"1000"}, 34 | } 35 | 36 | req.ParseForm() 37 | 38 | var post blogPost 39 | r.NoError(Exec(req, &post)) 40 | 41 | r.Equal([]string(nil), post.Tags) 42 | r.Equal(int32(0), post.Likes) 43 | r.Equal(0, post.Dislikes) 44 | 45 | } 46 | 47 | func Test_RegisterCustomDecoder(t *testing.T) { 48 | r := require.New(t) 49 | 50 | RegisterCustomDecoder(func(vals []string) (interface{}, error) { 51 | return []string{"X"}, nil 52 | }, []interface{}{[]string{}}, nil) 53 | 54 | RegisterCustomDecoder(func(vals []string) (interface{}, error) { 55 | return 0, nil 56 | }, []interface{}{int(0)}, nil) 57 | 58 | post := blogPost{} 59 | req, err := http.NewRequest("POST", "/", nil) 60 | r.NoError(err) 61 | 62 | req.Header.Set("Content-Type", "application/html") 63 | req.Form = url.Values{ 64 | "Tags": []string{"AAA"}, 65 | "Likes": []string{"12"}, 66 | "Dislikes": []string{"1000"}, 67 | } 68 | req.ParseForm() 69 | 70 | r.NoError(Exec(req, &post)) 71 | r.Equal([]string{"X"}, post.Tags) 72 | r.Equal(int32(12), post.Likes) 73 | r.Equal(0, post.Dislikes) 74 | } 75 | -------------------------------------------------------------------------------- /plugins/plugdeps/plugdeps_test.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gobuffalo/meta" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var heroku = Plugin{ 14 | Binary: "buffalo-heroku", 15 | GoGet: "github.com/gobuffalo/buffalo-heroku", 16 | Commands: []Command{ 17 | {Name: "deploy", Flags: []string{"-v"}}, 18 | }, 19 | Tags: []string{"foo", "bar"}, 20 | } 21 | 22 | var local = Plugin{ 23 | Binary: "buffalo-hello.rb", 24 | Local: "./plugins/buffalo-hello.rb", 25 | } 26 | 27 | func Test_ConfigPath(t *testing.T) { 28 | r := require.New(t) 29 | 30 | x := ConfigPath(meta.App{Root: "foo"}) 31 | r.Equal(x, filepath.Join("foo", "config", "buffalo-plugins.toml")) 32 | } 33 | 34 | func Test_List_Off(t *testing.T) { 35 | r := require.New(t) 36 | 37 | app := meta.App{} 38 | plugs, err := List(app) 39 | r.Error(err) 40 | r.True(errors.Is(err, ErrMissingConfig)) 41 | r.Len(plugs.List(), 0) 42 | } 43 | 44 | func Test_List_On(t *testing.T) { 45 | r := require.New(t) 46 | 47 | app := meta.New(os.TempDir()) 48 | 49 | p := ConfigPath(app) 50 | r.NoError(os.MkdirAll(filepath.Dir(p), 0755)) 51 | f, err := os.Create(p) 52 | r.NoError(err) 53 | f.WriteString(eToml) 54 | r.NoError(f.Close()) 55 | 56 | plugs, err := List(app) 57 | r.NoError(err) 58 | r.Len(plugs.List(), 3) 59 | } 60 | 61 | const eToml = `[[plugin]] 62 | binary = "buffalo-hello.rb" 63 | local = "./plugins/buffalo-hello.rb" 64 | 65 | [[plugin]] 66 | binary = "buffalo-heroku" 67 | go_get = "github.com/gobuffalo/buffalo-heroku" 68 | tags = ["foo", "bar"] 69 | 70 | [[plugin.command]] 71 | name = "deploy" 72 | flags = ["-v"] 73 | 74 | [[plugin]] 75 | binary = "buffalo-pop" 76 | go_get = "github.com/gobuffalo/buffalo-pop/v2" 77 | ` 78 | -------------------------------------------------------------------------------- /notfound.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 56 | 57 | 58 | 59 |
60 |
61 |

Not Found

62 |
63 |

The page you're looking for does not exist, you may have mistyped the address or the page may have been 64 | moved.

65 |
66 | 67 |

powered by gobuffalo.io

68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /error.prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 55 | 56 | 57 | 58 |
59 |
60 |

We're Sorry!

61 |
62 |

It looks like something went wrong! Don't worry, we are aware of the problem and are looking into it.

63 |

Sorry if this has caused you any problems. Please check back again later.

64 |
65 | 66 |

powered by gobuffalo.io

67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /binding/request_binder.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/gobuffalo/buffalo/internal/httpx" 11 | ) 12 | 13 | var ( 14 | errBlankContentType = errors.New("blank content type") 15 | ) 16 | 17 | // RequestBinder is in charge of binding multiple requests types to 18 | // struct. 19 | type RequestBinder struct { 20 | lock *sync.RWMutex 21 | binders map[string]Binder 22 | } 23 | 24 | // Register maps a request Content-Type (application/json) 25 | // to a Binder. 26 | func (rb *RequestBinder) Register(contentType string, fn Binder) { 27 | rb.lock.Lock() 28 | defer rb.lock.Unlock() 29 | 30 | rb.binders[strings.ToLower(contentType)] = fn 31 | } 32 | 33 | // Exec binds a request with a passed value, depending on the content type 34 | // It will look for the correct RequestTypeBinder and use it. 35 | func (rb *RequestBinder) Exec(req *http.Request, value interface{}) error { 36 | rb.lock.Lock() 37 | defer rb.lock.Unlock() 38 | 39 | if ba, ok := value.(Bindable); ok { 40 | return ba.Bind(req) 41 | } 42 | 43 | ct := httpx.ContentType(req) 44 | if ct == "" { 45 | return errBlankContentType 46 | } 47 | 48 | binder := rb.binders[ct] 49 | if binder == nil { 50 | return fmt.Errorf("could not find a binder for %s", ct) 51 | } 52 | 53 | return binder(req, value) 54 | } 55 | 56 | // NewRequestBinder creates our request binder with support for 57 | // XML, JSON, HTTP and File request types. 58 | func NewRequestBinder(requestBinders ...ContenTypeBinder) *RequestBinder { 59 | result := &RequestBinder{ 60 | lock: &sync.RWMutex{}, 61 | binders: map[string]Binder{}, 62 | } 63 | 64 | for _, requestBinder := range requestBinders { 65 | for _, contentType := range requestBinder.ContentTypes() { 66 | result.Register(contentType, requestBinder.BinderFunc()) 67 | } 68 | } 69 | 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /routenamer.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gobuffalo/flect" 7 | "github.com/gobuffalo/flect/name" 8 | ) 9 | 10 | // RouteNamer is in charge of naming a route from the 11 | // path assigned, this name typically will be used if no 12 | // name is assined with .Name(...). 13 | type RouteNamer interface { 14 | // NameRoute receives the path and returns the name 15 | // for the route. 16 | NameRoute(string) string 17 | } 18 | 19 | // BaseRouteNamer is the default route namer used by apps. 20 | type baseRouteNamer struct{} 21 | 22 | func (drn baseRouteNamer) NameRoute(p string) string { 23 | if p == "/" || p == "" { 24 | return "root" 25 | } 26 | 27 | resultParts := []string{} 28 | parts := strings.Split(p, "/") 29 | 30 | for index, part := range parts { 31 | 32 | originalPart := parts[index] 33 | 34 | var previousPart string 35 | if index > 0 { 36 | previousPart = parts[index-1] 37 | } 38 | 39 | var nextPart string 40 | if len(parts) > index+1 { 41 | nextPart = parts[index+1] 42 | } 43 | 44 | isIdentifierPart := strings.Contains(part, "{") && (strings.Contains(part, flect.Singularize(previousPart))) 45 | isSimplifiedID := part == `{id}` 46 | 47 | if isIdentifierPart || isSimplifiedID || part == "" { 48 | continue 49 | } 50 | 51 | if strings.Contains(nextPart, "{") { 52 | part = flect.Singularize(part) 53 | } 54 | 55 | if originalPart == "new" || originalPart == "edit" { 56 | resultParts = append([]string{part}, resultParts...) 57 | continue 58 | } 59 | 60 | if strings.Contains(previousPart, "}") { 61 | resultParts = append(resultParts, part) 62 | continue 63 | } 64 | 65 | resultParts = append(resultParts, part) 66 | } 67 | 68 | if len(resultParts) == 0 { 69 | return "unnamed" 70 | } 71 | 72 | underscore := strings.TrimSpace(strings.Join(resultParts, "_")) 73 | return name.VarCase(underscore) 74 | } 75 | -------------------------------------------------------------------------------- /plugins/plugcmds/plug_map.go: -------------------------------------------------------------------------------- 1 | //go:generate mapgen -name "plug" -zero "plug{}" -go-type "plug" -pkg "" -a "nil" -b "nil" -c "nil" -bb "nil" -destination "plugcmds" 2 | // Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. 3 | 4 | package plugcmds 5 | 6 | import ( 7 | "sort" 8 | "sync" 9 | ) 10 | 11 | // plugMap wraps sync.Map and uses the following types: 12 | // key: string 13 | // value: plug 14 | type plugMap struct { 15 | data sync.Map 16 | } 17 | 18 | // Delete the key from the map 19 | func (m *plugMap) Delete(key string) { 20 | m.data.Delete(key) 21 | } 22 | 23 | // Load the key from the map. 24 | // Returns plug or bool. 25 | // A false return indicates either the key was not found 26 | // or the value is not of type plug 27 | func (m *plugMap) Load(key string) (plug, bool) { 28 | i, ok := m.data.Load(key) 29 | if !ok { 30 | return plug{}, false 31 | } 32 | s, ok := i.(plug) 33 | return s, ok 34 | } 35 | 36 | // LoadOrStore will return an existing key or 37 | // store the value if not already in the map 38 | func (m *plugMap) LoadOrStore(key string, value plug) (plug, bool) { 39 | i, _ := m.data.LoadOrStore(key, value) 40 | s, ok := i.(plug) 41 | return s, ok 42 | } 43 | 44 | // Range over the plug values in the map 45 | func (m *plugMap) Range(f func(key string, value plug) bool) { 46 | m.data.Range(func(k, v interface{}) bool { 47 | key, ok := k.(string) 48 | if !ok { 49 | return false 50 | } 51 | value, ok := v.(plug) 52 | if !ok { 53 | return false 54 | } 55 | return f(key, value) 56 | }) 57 | } 58 | 59 | // Store a plug in the map 60 | func (m *plugMap) Store(key string, value plug) { 61 | m.data.Store(key, value) 62 | } 63 | 64 | // Keys returns a list of keys in the map 65 | func (m *plugMap) Keys() []string { 66 | var keys []string 67 | m.Range(func(key string, value plug) bool { 68 | keys = append(keys, key) 69 | return true 70 | }) 71 | sort.Strings(keys) 72 | return keys 73 | } 74 | -------------------------------------------------------------------------------- /binding/request_binder_test.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_RequestBinder_Exec(t *testing.T) { 13 | r := require.New(t) 14 | 15 | var used bool 16 | BaseRequestBinder.Register("paganotoni/test", func(*http.Request, interface{}) error { 17 | used = true 18 | return nil 19 | }) 20 | 21 | req, err := http.NewRequest("GET", "/home", strings.NewReader("")) 22 | req.Header.Add("content-type", "paganotoni/test") 23 | r.NoError(err) 24 | 25 | data := &struct{}{} 26 | r.NoError(BaseRequestBinder.Exec(req, data)) 27 | r.True(used) 28 | } 29 | 30 | func Test_RequestBinder_Exec_BlankContentType(t *testing.T) { 31 | r := require.New(t) 32 | 33 | req, err := http.NewRequest("GET", "/home", strings.NewReader("")) 34 | r.NoError(err) 35 | 36 | data := &struct{}{} 37 | r.Equal(BaseRequestBinder.Exec(req, data), errBlankContentType) 38 | } 39 | 40 | func Test_RequestBinder_Exec_Bindable(t *testing.T) { 41 | r := require.New(t) 42 | 43 | BaseRequestBinder.Register("paganotoni/orbison", func(req *http.Request, val interface{}) error { 44 | switch v := val.(type) { 45 | case orbison: 46 | v.bound = false 47 | } 48 | 49 | return errors.New("this should not be called") 50 | }) 51 | 52 | req, err := http.NewRequest("GET", "/home", strings.NewReader("")) 53 | req.Header.Add("content-type", "paganotoni/orbison") 54 | r.NoError(err) 55 | 56 | data := &orbison{} 57 | r.NoError(BaseRequestBinder.Exec(req, data)) 58 | r.True(data.bound) 59 | } 60 | 61 | func Test_RequestBinder_Exec_NoBinder(t *testing.T) { 62 | r := require.New(t) 63 | 64 | req, err := http.NewRequest("GET", "/home", strings.NewReader("")) 65 | req.Header.Add("content-type", "paganotoni/other") 66 | r.NoError(err) 67 | 68 | err = BaseRequestBinder.Exec(req, &struct{}{}) 69 | r.Error(err) 70 | r.Equal(err.Error(), "could not find a binder for paganotoni/other") 71 | } 72 | -------------------------------------------------------------------------------- /cookies_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCookies_Get(t *testing.T) { 13 | r := require.New(t) 14 | req := httptest.NewRequest("POST", "/", nil) 15 | req.Header.Set("Cookie", "name=Arthur Dent; answer=42") 16 | 17 | c := Cookies{req, nil} 18 | 19 | v, err := c.Get("name") 20 | r.NoError(err) 21 | r.Equal("Arthur Dent", v) 22 | 23 | v, err = c.Get("answer") 24 | r.NoError(err) 25 | r.Equal("42", v) 26 | 27 | _, err = c.Get("unknown") 28 | r.EqualError(err, http.ErrNoCookie.Error()) 29 | } 30 | 31 | func TestCookies_Set(t *testing.T) { 32 | r := require.New(t) 33 | res := httptest.NewRecorder() 34 | 35 | c := Cookies{&http.Request{}, res} 36 | 37 | c.Set("name", "Rob Pike", time.Hour*24) 38 | 39 | h := res.Header().Get("Set-Cookie") 40 | r.Equal("name=\"Rob Pike\"; Max-Age=86400", h) 41 | } 42 | 43 | func TestCookies_SetWithPath(t *testing.T) { 44 | r := require.New(t) 45 | res := httptest.NewRecorder() 46 | 47 | c := Cookies{&http.Request{}, res} 48 | 49 | c.SetWithPath("name", "Rob Pike", "/foo") 50 | 51 | h := res.Header().Get("Set-Cookie") 52 | r.Equal("name=\"Rob Pike\"; Path=/foo", h) 53 | } 54 | 55 | func TestCookies_SetWithExpirationTime(t *testing.T) { 56 | r := require.New(t) 57 | res := httptest.NewRecorder() 58 | 59 | c := Cookies{&http.Request{}, res} 60 | 61 | e := time.Date(2017, 7, 29, 19, 28, 45, 0, time.UTC) 62 | c.SetWithExpirationTime("name", "Rob Pike", e) 63 | 64 | h := res.Header().Get("Set-Cookie") 65 | r.Equal("name=\"Rob Pike\"; Expires=Sat, 29 Jul 2017 19:28:45 GMT", h) 66 | } 67 | 68 | func TestCookies_Delete(t *testing.T) { 69 | r := require.New(t) 70 | res := httptest.NewRecorder() 71 | 72 | c := Cookies{&http.Request{}, res} 73 | 74 | c.Delete("remove-me") 75 | 76 | h := res.Header().Get("Set-Cookie") 77 | r.Equal("remove-me=v; Expires=Thu, 01 Jan 1970 00:00:00 GMT", h) 78 | } 79 | -------------------------------------------------------------------------------- /binding/file_request_type_binder.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | 7 | "github.com/monoculum/formam" 8 | ) 9 | 10 | // FileRequestTypeBinder is in charge of binding File request types. 11 | type FileRequestTypeBinder struct { 12 | decoder *formam.Decoder 13 | } 14 | 15 | // ContentTypes returns the list of content types for FileRequestTypeBinder 16 | func (ht FileRequestTypeBinder) ContentTypes() []string { 17 | return []string{ 18 | "multipart/form-data", 19 | } 20 | } 21 | 22 | // BinderFunc that will take care of the HTML File binding 23 | func (ht FileRequestTypeBinder) BinderFunc() Binder { 24 | return func(req *http.Request, i interface{}) error { 25 | err := req.ParseMultipartForm(MaxFileMemory) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | if err := ht.decoder.Decode(req.Form, i); err != nil { 31 | return err 32 | } 33 | 34 | form := req.MultipartForm.File 35 | if len(form) == 0 { 36 | return nil 37 | } 38 | 39 | ri := reflect.Indirect(reflect.ValueOf(i)) 40 | rt := ri.Type() 41 | for n := range form { 42 | f := ri.FieldByName(n) 43 | if !f.IsValid() { 44 | for i := 0; i < rt.NumField(); i++ { 45 | sf := rt.Field(i) 46 | if sf.Tag.Get("form") == n { 47 | f = ri.Field(i) 48 | break 49 | } 50 | } 51 | } 52 | if !f.IsValid() { 53 | continue 54 | } 55 | if f.Kind() == reflect.Slice { 56 | for _, fh := range req.MultipartForm.File[n] { 57 | mf, err := fh.Open() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | f.Set(reflect.Append(f, reflect.ValueOf(File{ 63 | File: mf, 64 | FileHeader: fh, 65 | }))) 66 | } 67 | continue 68 | } 69 | if _, ok := f.Interface().(File); ok { 70 | mf, mh, err := req.FormFile(n) 71 | if err != nil { 72 | return err 73 | } 74 | f.Set(reflect.ValueOf(File{ 75 | File: mf, 76 | FileHeader: mh, 77 | })) 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /render/sse.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type sse struct { 10 | Data interface{} `json:"data"` 11 | Type string `json:"type"` 12 | } 13 | 14 | func (s *sse) String() string { 15 | b, _ := json.Marshal(s) 16 | return fmt.Sprintf("data: %s\n\n", string(b)) 17 | } 18 | 19 | func (s *sse) Bytes() []byte { 20 | return []byte(s.String()) 21 | } 22 | 23 | // EventSource is designed to work with JavaScript EventSource objects. 24 | // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource for 25 | // more details 26 | type EventSource struct { 27 | w http.ResponseWriter 28 | fl http.Flusher 29 | } 30 | 31 | func (es *EventSource) Write(t string, d interface{}) error { 32 | s := &sse{Type: t, Data: d} 33 | _, err := es.w.Write(s.Bytes()) 34 | if err != nil { 35 | return err 36 | } 37 | es.Flush() 38 | return nil 39 | } 40 | 41 | // Flush messages down the pipe. If messages aren't flushed they 42 | // won't be sent. 43 | func (es *EventSource) Flush() { 44 | es.fl.Flush() 45 | } 46 | 47 | type closeNotifier interface { 48 | CloseNotify() <-chan bool 49 | } 50 | 51 | // CloseNotify return true across the channel when the connection 52 | // in the browser has been severed. 53 | func (es *EventSource) CloseNotify() <-chan bool { 54 | if cn, ok := es.w.(closeNotifier); ok { 55 | return cn.CloseNotify() 56 | } 57 | return nil 58 | } 59 | 60 | // NewEventSource returns a new EventSource instance while ensuring 61 | // that the http.ResponseWriter is able to handle EventSource messages. 62 | // It also makes sure to set the proper response heads. 63 | func NewEventSource(w http.ResponseWriter) (*EventSource, error) { 64 | es := &EventSource{w: w} 65 | var ok bool 66 | es.fl, ok = w.(http.Flusher) 67 | if !ok { 68 | return es, fmt.Errorf("streaming is not supported") 69 | } 70 | 71 | es.w.Header().Set("Content-Type", "text/event-stream") 72 | es.w.Header().Set("Cache-Control", "no-cache") 73 | es.w.Header().Set("Connection", "keep-alive") 74 | es.w.Header().Set("Access-Control-Allow-Origin", "*") 75 | return es, nil 76 | } 77 | -------------------------------------------------------------------------------- /plugins/cache.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/user" 10 | "path/filepath" 11 | "sync" 12 | 13 | "github.com/gobuffalo/envy" 14 | ) 15 | 16 | type cachedPlugin struct { 17 | Commands Commands `json:"commands"` 18 | CheckSum string `json:"check_sum"` 19 | } 20 | 21 | type cachedPlugins map[string]cachedPlugin 22 | 23 | // CachePath returns the path to the plugins cache 24 | var CachePath = func() string { 25 | home := "." 26 | if usr, err := user.Current(); err == nil { 27 | home = usr.HomeDir 28 | } 29 | return filepath.Join(home, ".buffalo", "plugin.cache") 30 | }() 31 | 32 | var cacheMoot sync.RWMutex 33 | 34 | var cacheOn = envy.Get("BUFFALO_PLUGIN_CACHE", "on") 35 | 36 | var cache = func() cachedPlugins { 37 | m := cachedPlugins{} 38 | if cacheOn != "on" { 39 | return m 40 | } 41 | f, err := os.Open(CachePath) 42 | if err != nil { 43 | return m 44 | } 45 | defer f.Close() 46 | if err := json.NewDecoder(f).Decode(&m); err != nil { 47 | f.Close() 48 | os.Remove(f.Name()) 49 | } 50 | return m 51 | }() 52 | 53 | func findInCache(path string) (cachedPlugin, bool) { 54 | cacheMoot.RLock() 55 | defer cacheMoot.RUnlock() 56 | cp, ok := cache[path] 57 | return cp, ok 58 | } 59 | 60 | func saveCache() error { 61 | if cacheOn != "on" { 62 | return nil 63 | } 64 | cacheMoot.Lock() 65 | defer cacheMoot.Unlock() 66 | os.MkdirAll(filepath.Dir(CachePath), 0744) 67 | f, err := os.Create(CachePath) 68 | if err != nil { 69 | return err 70 | } 71 | return json.NewEncoder(f).Encode(cache) 72 | } 73 | 74 | func sum(path string) string { 75 | f, err := os.Open(path) 76 | if err != nil { 77 | return "" 78 | } 79 | defer f.Close() 80 | hash := sha256.New() 81 | if _, err := io.Copy(hash, f); err != nil { 82 | return "" 83 | } 84 | sum := hash.Sum(nil) 85 | 86 | s := fmt.Sprintf("%x", sum) 87 | return s 88 | } 89 | 90 | func addToCache(path string, cp cachedPlugin) { 91 | if cp.CheckSum == "" { 92 | cp.CheckSum = sum(path) 93 | } 94 | cacheMoot.Lock() 95 | defer cacheMoot.Unlock() 96 | cache[path] = cp 97 | } 98 | -------------------------------------------------------------------------------- /request_logger.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "net/http" 7 | "time" 8 | 9 | humanize "github.com/dustin/go-humanize" 10 | "github.com/gobuffalo/buffalo/internal/httpx" 11 | ) 12 | 13 | // RequestLogger can be be overridden to a user specified 14 | // function that can be used to log the request. 15 | var RequestLogger = RequestLoggerFunc 16 | 17 | func randString(i int) (string, error) { 18 | if i == 0 { 19 | i = 64 20 | } 21 | b := make([]byte, i) 22 | _, err := rand.Read(b) 23 | return hex.EncodeToString(b), err 24 | } 25 | 26 | // RequestLoggerFunc is the default implementation of the RequestLogger. 27 | // By default it will log a uniq "request_id", the HTTP Method of the request, 28 | // the path that was requested, the duration (time) it took to process the 29 | // request, the size of the response (and the "human" size), and the status 30 | // code of the response. 31 | func RequestLoggerFunc(h Handler) Handler { 32 | return func(c Context) error { 33 | rs, err := randString(10) 34 | if err != nil { 35 | return err 36 | } 37 | var irid interface{} 38 | if irid = c.Session().Get("requestor_id"); irid == nil { 39 | rs, err := randString(10) 40 | if err != nil { 41 | return err 42 | } 43 | irid = rs 44 | c.Session().Set("requestor_id", irid) 45 | } 46 | 47 | rid := irid.(string) + "-" + rs 48 | c.Set("request_id", rid) 49 | c.LogField("request_id", rid) 50 | 51 | start := time.Now() 52 | defer func() { 53 | ws, ok := c.Response().(*Response) 54 | if !ok { 55 | ws = &Response{ResponseWriter: c.Response()} 56 | ws.Status = http.StatusOK 57 | } 58 | req := c.Request() 59 | ct := httpx.ContentType(req) 60 | if ct != "" { 61 | c.LogField("content_type", ct) 62 | } 63 | c.LogFields(map[string]interface{}{ 64 | "method": req.Method, 65 | "path": req.URL.String(), 66 | "duration": time.Since(start), 67 | "size": ws.Size, 68 | "human_size": humanize.Bytes(uint64(ws.Size)), 69 | "status": ws.Status, 70 | }) 71 | c.Logger().Info(req.URL.String()) 72 | }() 73 | return h(c) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /render/render.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "github.com/gobuffalo/helpers" 5 | "github.com/gobuffalo/helpers/forms" 6 | "github.com/gobuffalo/helpers/forms/bootstrap" 7 | "github.com/gobuffalo/plush/v5" 8 | ) 9 | 10 | // Engine used to power all defined renderers. 11 | // This allows you to configure the system to your 12 | // preferred settings, instead of just getting 13 | // the defaults. 14 | type Engine struct { 15 | Options 16 | } 17 | 18 | // New render.Engine ready to go with your Options 19 | // and some defaults we think you might like. 20 | func New(opts Options) *Engine { 21 | if opts.Helpers == nil { 22 | opts.Helpers = Helpers{} 23 | } 24 | 25 | if len(opts.Helpers) == 0 { 26 | opts.Helpers = defaultHelpers() 27 | } 28 | 29 | if opts.TemplateEngines == nil { 30 | opts.TemplateEngines = map[string]TemplateEngine{} 31 | } 32 | if _, ok := opts.TemplateEngines["html"]; !ok { 33 | opts.TemplateEngines["html"] = plush.BuffaloRenderer 34 | } 35 | if _, ok := opts.TemplateEngines["plush"]; !ok { 36 | opts.TemplateEngines["plush"] = plush.BuffaloRenderer 37 | } 38 | if _, ok := opts.TemplateEngines["text"]; !ok { 39 | opts.TemplateEngines["text"] = plush.BuffaloRenderer 40 | } 41 | if _, ok := opts.TemplateEngines["txt"]; !ok { 42 | opts.TemplateEngines["txt"] = plush.BuffaloRenderer 43 | } 44 | if _, ok := opts.TemplateEngines["js"]; !ok { 45 | opts.TemplateEngines["js"] = plush.BuffaloRenderer 46 | } 47 | if _, ok := opts.TemplateEngines["md"]; !ok { 48 | opts.TemplateEngines["md"] = MDTemplateEngine 49 | } 50 | if _, ok := opts.TemplateEngines["tmpl"]; !ok { 51 | opts.TemplateEngines["tmpl"] = GoTemplateEngine 52 | } 53 | 54 | if opts.DefaultContentType == "" { 55 | opts.DefaultContentType = "text/html; charset=utf-8" 56 | } 57 | 58 | if opts.TemplateMetadataKeys == nil { 59 | opts.TemplateMetadataKeys = defaultTemplateMetadataKeys 60 | } 61 | 62 | e := &Engine{ 63 | Options: opts, 64 | } 65 | return e 66 | } 67 | 68 | func defaultHelpers() Helpers { 69 | h := Helpers(helpers.ALL()) 70 | h[forms.FormKey] = bootstrap.Form 71 | h[forms.FormForKey] = bootstrap.FormFor 72 | h["form_for"] = bootstrap.FormFor 73 | return h 74 | } 75 | -------------------------------------------------------------------------------- /plugins/plugdeps/plugins.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "sync" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | // Plugins manages the config/buffalo-plugins.toml file 12 | // as well as the plugins available from the file. 13 | type Plugins struct { 14 | plugins map[string]Plugin 15 | moot *sync.RWMutex 16 | } 17 | 18 | // Encode the list of plugins, in TOML format, to the reader 19 | func (plugs *Plugins) Encode(w io.Writer) error { 20 | tp := tomlPlugins{ 21 | Plugins: plugs.List(), 22 | } 23 | 24 | if err := toml.NewEncoder(w).Encode(tp); err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // Decode the list of plugins, in TOML format, from the reader 31 | func (plugs *Plugins) Decode(r io.Reader) error { 32 | tp := &tomlPlugins{ 33 | Plugins: []Plugin{}, 34 | } 35 | if _, err := toml.NewDecoder(r).Decode(tp); err != nil { 36 | return err 37 | } 38 | for _, p := range tp.Plugins { 39 | plugs.Add(p) 40 | } 41 | return nil 42 | } 43 | 44 | // List of dependent plugins listed in order of Plugin.String() 45 | func (plugs *Plugins) List() []Plugin { 46 | m := map[string]Plugin{} 47 | plugs.moot.RLock() 48 | for _, p := range plugs.plugins { 49 | m[p.key()] = p 50 | } 51 | plugs.moot.RUnlock() 52 | var pp []Plugin 53 | for _, v := range m { 54 | pp = append(pp, v) 55 | } 56 | sort.Slice(pp, func(a, b int) bool { 57 | return pp[a].Binary < pp[b].Binary 58 | }) 59 | return pp 60 | } 61 | 62 | // Add plugin(s) to the list of dependencies 63 | func (plugs *Plugins) Add(pp ...Plugin) { 64 | plugs.moot.Lock() 65 | for _, p := range pp { 66 | plugs.plugins[p.key()] = p 67 | } 68 | plugs.moot.Unlock() 69 | } 70 | 71 | // Remove plugin(s) from the list of dependencies 72 | func (plugs *Plugins) Remove(pp ...Plugin) { 73 | plugs.moot.Lock() 74 | for _, p := range pp { 75 | delete(plugs.plugins, p.key()) 76 | } 77 | plugs.moot.Unlock() 78 | } 79 | 80 | // New returns a configured *Plugins value 81 | func New() *Plugins { 82 | plugs := &Plugins{ 83 | plugins: map[string]Plugin{}, 84 | moot: &sync.RWMutex{}, 85 | } 86 | return plugs 87 | } 88 | 89 | type tomlPlugins struct { 90 | Plugins []Plugin `toml:"plugin"` 91 | } 92 | -------------------------------------------------------------------------------- /render/download_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strconv" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type dlRenderer func(context.Context, string, io.Reader) Renderer 16 | 17 | type dlContext struct { 18 | context.Context 19 | rw http.ResponseWriter 20 | } 21 | 22 | func (c dlContext) Response() http.ResponseWriter { 23 | return c.rw 24 | } 25 | 26 | var data = []byte("data") 27 | 28 | func Test_Download_KnownExtension(t *testing.T) { 29 | r := require.New(t) 30 | 31 | table := []dlRenderer{ 32 | Download, 33 | New(Options{}).Download, 34 | } 35 | 36 | for _, dl := range table { 37 | ctx := dlContext{rw: httptest.NewRecorder()} 38 | 39 | re := dl(ctx, "filename.pdf", bytes.NewReader(data)) 40 | 41 | bb := &bytes.Buffer{} 42 | r.NoError(re.Render(bb, nil)) 43 | 44 | r.Equal(data, bb.Bytes()) 45 | r.Equal(strconv.Itoa(len(data)), ctx.Response().Header().Get("Content-Length")) 46 | r.Equal("attachment; filename=filename.pdf", ctx.Response().Header().Get("Content-Disposition")) 47 | r.Equal("application/pdf", re.ContentType()) 48 | } 49 | } 50 | 51 | func Test_Download_UnknownExtension(t *testing.T) { 52 | r := require.New(t) 53 | 54 | table := []dlRenderer{ 55 | Download, 56 | New(Options{}).Download, 57 | } 58 | 59 | for _, dl := range table { 60 | ctx := dlContext{rw: httptest.NewRecorder()} 61 | re := dl(ctx, "filename", bytes.NewReader(data)) 62 | 63 | bb := &bytes.Buffer{} 64 | r.NoError(re.Render(bb, nil)) 65 | 66 | r.Equal(data, bb.Bytes()) 67 | r.Equal(strconv.Itoa(len(data)), ctx.Response().Header().Get("Content-Length")) 68 | r.Equal("attachment; filename=filename", ctx.Response().Header().Get("Content-Disposition")) 69 | r.Equal("application/octet-stream", re.ContentType()) 70 | } 71 | } 72 | 73 | func Test_InvalidContext(t *testing.T) { 74 | r := require.New(t) 75 | 76 | table := []dlRenderer{ 77 | Download, 78 | New(Options{}).Download, 79 | } 80 | 81 | for _, dl := range table { 82 | re := dl(context.TODO(), "filename", bytes.NewReader(data)) 83 | 84 | bb := &bytes.Buffer{} 85 | r.Error(re.Render(bb, nil)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /render/html.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "html" 5 | "strings" 6 | 7 | "github.com/gobuffalo/github_flavored_markdown" 8 | "github.com/gobuffalo/plush/v5" 9 | ) 10 | 11 | // HTML renders the named files using the 'text/html' 12 | // content type and the github.com/gobuffalo/plush/v5 13 | // package for templating. If more than 1 file is provided 14 | // the second file will be considered a "layout" file 15 | // and the first file will be the "content" file which will 16 | // be placed into the "layout" using "<%= yield %>". 17 | func HTML(names ...string) Renderer { 18 | e := New(Options{}) 19 | return e.HTML(names...) 20 | } 21 | 22 | // HTML renders the named files using the 'text/html' 23 | // content type and the github.com/gobuffalo/plush/v5 24 | // package for templating. If more than 1 file is provided 25 | // the second file will be considered a "layout" file 26 | // and the first file will be the "content" file which will 27 | // be placed into the "layout" using "<%= yield %>". If no 28 | // second file is provided and an `HTMLLayout` is specified 29 | // in the options, then that layout file will be used 30 | // automatically. 31 | func (e *Engine) HTML(names ...string) Renderer { 32 | // just allow leading slash and remove them here. 33 | // generated actions were various by buffalo versions. 34 | tmp := []string{} 35 | for _, name := range names { 36 | tmp = append(tmp, strings.TrimPrefix(name, "/")) 37 | } 38 | names = tmp 39 | 40 | if e.HTMLLayout != "" && len(names) == 1 { 41 | names = append(names, e.HTMLLayout) 42 | } 43 | hr := &templateRenderer{ 44 | Engine: e, 45 | contentType: "text/html; charset=utf-8", 46 | names: names, 47 | } 48 | return hr 49 | } 50 | 51 | // MDTemplateEngine runs the input through github flavored markdown before sending it to the Plush engine. 52 | func MDTemplateEngine(input string, data map[string]interface{}, helpers map[string]interface{}) (string, error) { 53 | if ct, ok := data["contentType"].(string); ok && ct == "text/plain" { 54 | return plush.BuffaloRenderer(input, data, helpers) 55 | } 56 | source := github_flavored_markdown.Markdown([]byte(input)) 57 | source = []byte(html.UnescapeString(string(source))) 58 | return plush.BuffaloRenderer(string(source), data, helpers) 59 | } 60 | -------------------------------------------------------------------------------- /cookies.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // Cookies allows you to easily get cookies from the request, and set cookies on the response. 9 | type Cookies struct { 10 | req *http.Request 11 | res http.ResponseWriter 12 | } 13 | 14 | // Get returns the value of the cookie with the given name. Returns http.ErrNoCookie if there's no cookie with that name in the request. 15 | func (c *Cookies) Get(name string) (string, error) { 16 | ck, err := c.req.Cookie(name) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | return ck.Value, nil 22 | } 23 | 24 | // Set a cookie on the response, which will expire after the given duration. 25 | func (c *Cookies) Set(name, value string, maxAge time.Duration) { 26 | ck := http.Cookie{ 27 | Name: name, 28 | Value: value, 29 | MaxAge: int(maxAge.Seconds()), 30 | } 31 | 32 | http.SetCookie(c.res, &ck) 33 | } 34 | 35 | // SetWithExpirationTime sets a cookie that will expire at a specific time. 36 | // Note that the time is determined by the client's browser, so it might not expire at the expected time, 37 | // for example if the client has changed the time on their computer. 38 | func (c *Cookies) SetWithExpirationTime(name, value string, expires time.Time) { 39 | ck := http.Cookie{ 40 | Name: name, 41 | Value: value, 42 | Expires: expires, 43 | } 44 | 45 | http.SetCookie(c.res, &ck) 46 | } 47 | 48 | // SetWithPath sets a cookie path on the server in which the cookie will be available on. 49 | // If set to '/', the cookie will be available within the entire domain. 50 | // If set to '/foo/', the cookie will only be available within the /foo/ directory and 51 | // all sub-directories such as /foo/bar/ of domain. 52 | func (c *Cookies) SetWithPath(name, value, path string) { 53 | ck := http.Cookie{ 54 | Name: name, 55 | Value: value, 56 | Path: path, 57 | } 58 | 59 | http.SetCookie(c.res, &ck) 60 | } 61 | 62 | // Delete sets a header that tells the browser to remove the cookie with the given name. 63 | func (c *Cookies) Delete(name string) { 64 | ck := http.Cookie{ 65 | Name: name, 66 | Value: "v", 67 | // Setting a time in the distant past, like the unix epoch, removes the cookie, 68 | // since it has long expired. 69 | Expires: time.Unix(0, 0), 70 | } 71 | 72 | http.SetCookie(c.res, &ck) 73 | } 74 | -------------------------------------------------------------------------------- /plugins/decorate.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/gobuffalo/envy" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // ErrPlugMissing error for when a plugin is missing 16 | var ErrPlugMissing = fmt.Errorf("plugin missing") 17 | 18 | // Decorate setup cobra Commands for plugins 19 | func Decorate(c Command) *cobra.Command { 20 | var flags []string 21 | if len(c.Flags) > 0 { 22 | flags = append(flags, c.Flags...) 23 | } 24 | cc := &cobra.Command{ 25 | Use: c.Name, 26 | Short: fmt.Sprintf("[PLUGIN] %s", c.Description), 27 | Aliases: c.Aliases, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | plugCmd := c.Name 30 | if c.UseCommand != "" { 31 | plugCmd = c.UseCommand 32 | } 33 | 34 | ax := []string{plugCmd} 35 | if plugCmd == "-" { 36 | ax = []string{} 37 | } 38 | 39 | ax = append(ax, args...) 40 | ax = append(ax, flags...) 41 | 42 | bin, err := LookPath(c.Binary) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | ex := exec.Command(bin, ax...) 48 | if runtime.GOOS != "windows" { 49 | ex.Env = append(envy.Environ(), "BUFFALO_PLUGIN=1") 50 | } 51 | ex.Stdin = os.Stdin 52 | ex.Stdout = os.Stdout 53 | ex.Stderr = os.Stderr 54 | return log(strings.Join(ex.Args, " "), ex.Run) 55 | }, 56 | } 57 | cc.DisableFlagParsing = true 58 | return cc 59 | } 60 | 61 | // LookPath for plugin 62 | func LookPath(s string) (string, error) { 63 | if _, err := os.Stat(s); err == nil { 64 | return s, nil 65 | } 66 | 67 | if lp, err := exec.LookPath(s); err == nil { 68 | return lp, err 69 | } 70 | 71 | var bin string 72 | pwd, err := os.Getwd() 73 | if err != nil { 74 | return "", err 75 | } 76 | 77 | var looks []string 78 | if from, err := envy.MustGet("BUFFALO_PLUGIN_PATH"); err == nil { 79 | looks = append(looks, from) 80 | } else { 81 | looks = []string{filepath.Join(pwd, "plugins"), filepath.Join(envy.GoPath(), "bin"), envy.Get("PATH", "")} 82 | } 83 | 84 | for _, p := range looks { 85 | lp := filepath.Join(p, s) 86 | if lp, err = filepath.EvalSymlinks(lp); err == nil { 87 | bin = lp 88 | break 89 | } 90 | } 91 | 92 | if len(bin) == 0 { 93 | return "", ErrPlugMissing 94 | } 95 | return bin, nil 96 | } 97 | -------------------------------------------------------------------------------- /plugins/plugdeps/plugdeps.go: -------------------------------------------------------------------------------- 1 | package plugdeps 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/gobuffalo/meta" 11 | ) 12 | 13 | // ErrMissingConfig is if config/buffalo-plugins.toml file is not found. Use plugdeps#On(app) to test if plugdeps are being used 14 | var ErrMissingConfig = fmt.Errorf("could not find a buffalo-plugins config file at %s", ConfigPath(meta.New("."))) 15 | 16 | // List all of the plugins the application depends on. Will return ErrMissingConfig 17 | // if the app is not using config/buffalo-plugins.toml to manage their plugins. 18 | // Use plugdeps#On(app) to test if plugdeps are being used. 19 | func List(app meta.App) (*Plugins, error) { 20 | plugs := New() 21 | if app.WithPop { 22 | plugs.Add(pop) 23 | } 24 | 25 | lp, err := listLocal(app) 26 | if err != nil { 27 | return plugs, err 28 | } 29 | plugs.Add(lp.List()...) 30 | 31 | if !On(app) { 32 | return plugs, ErrMissingConfig 33 | } 34 | 35 | p := ConfigPath(app) 36 | tf, err := os.Open(p) 37 | if err != nil { 38 | return plugs, err 39 | } 40 | if err := plugs.Decode(tf); err != nil { 41 | return plugs, err 42 | } 43 | 44 | return plugs, nil 45 | } 46 | 47 | func listLocal(app meta.App) (*Plugins, error) { 48 | plugs := New() 49 | pRoot := filepath.Join(app.Root, "plugins") 50 | if _, err := os.Stat(pRoot); err != nil { 51 | return plugs, nil 52 | } 53 | err := filepath.WalkDir(pRoot, func(path string, d fs.DirEntry, err error) error { 54 | if d.IsDir() { 55 | return nil 56 | } 57 | if !strings.HasPrefix(d.Name(), "buffalo-") { 58 | return nil 59 | } 60 | 61 | plugs.Add(Plugin{ 62 | Binary: d.Name(), 63 | Local: "." + strings.TrimPrefix(path, app.Root), 64 | }) 65 | return nil 66 | }) 67 | if err != nil { 68 | return plugs, err 69 | } 70 | 71 | return plugs, nil 72 | } 73 | 74 | // ConfigPath returns the path to the config/buffalo-plugins.toml file 75 | // relative to the app 76 | func ConfigPath(app meta.App) string { 77 | return filepath.Join(app.Root, "config", "buffalo-plugins.toml") 78 | } 79 | 80 | // On checks for the existence of config/buffalo-plugins.toml if this 81 | // file exists its contents will be used to list plugins. If the file is not 82 | // found, then the BUFFALO_PLUGIN_PATH and ./plugins folders are consulted. 83 | func On(app meta.App) bool { 84 | _, err := os.Stat(ConfigPath(app)) 85 | return err == nil 86 | } 87 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/gobuffalo/envy" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | // App is where it all happens! It holds on to options, 13 | // the underlying router, the middleware, and more. 14 | // Without an App you can't do much! 15 | type App struct { 16 | Options 17 | // Middleware, ErrorHandlers, router, and filepaths are moved to Home. 18 | Home 19 | moot *sync.RWMutex 20 | routes RouteList 21 | // TODO: to be deprecated #road-to-v1 22 | root *App 23 | children []*App 24 | 25 | // Routenamer for the app. This field provides the ability to override the 26 | // base route namer for something more specific to the app. 27 | RouteNamer RouteNamer 28 | } 29 | 30 | // Muxer returns the underlying mux router to allow 31 | // for advance configurations 32 | func (a *App) Muxer() *mux.Router { 33 | return a.router 34 | } 35 | 36 | // New returns a new instance of App and adds some sane, and useful, defaults. 37 | func New(opts Options) *App { 38 | LoadPlugins() 39 | envy.Load() 40 | 41 | opts = optionsWithDefaults(opts) 42 | 43 | a := &App{ 44 | Options: opts, 45 | Home: Home{ 46 | name: opts.Name, 47 | host: opts.Host, 48 | prefix: opts.Prefix, 49 | ErrorHandlers: ErrorHandlers{ 50 | http.StatusNotFound: defaultErrorHandler, 51 | http.StatusInternalServerError: defaultErrorHandler, 52 | }, 53 | router: mux.NewRouter(), 54 | }, 55 | moot: &sync.RWMutex{}, 56 | routes: RouteList{}, 57 | children: []*App{}, 58 | 59 | RouteNamer: baseRouteNamer{}, 60 | } 61 | a.Home.app = a // replace root. 62 | a.Home.appSelf = a // temporary, reverse reference to the group app. 63 | 64 | notFoundHandler := func(errorf string, code int) http.HandlerFunc { 65 | return func(res http.ResponseWriter, req *http.Request) { 66 | c := a.newContext(RouteInfo{}, res, req) 67 | err := fmt.Errorf(errorf, req.Method, req.URL.Path) 68 | _ = a.ErrorHandlers.Get(code)(code, err, c) 69 | } 70 | } 71 | 72 | a.router.NotFoundHandler = notFoundHandler("path not found: %s %s", http.StatusNotFound) 73 | a.router.MethodNotAllowedHandler = notFoundHandler("method not found: %s %s", http.StatusMethodNotAllowed) 74 | 75 | if a.MethodOverride == nil { 76 | a.MethodOverride = MethodOverride 77 | } 78 | 79 | a.Middleware = newMiddlewareStack(RequestLogger) 80 | a.Use(a.defaultErrorMiddleware) 81 | a.Use(a.PanicHandler) 82 | 83 | return a 84 | } 85 | -------------------------------------------------------------------------------- /render/template_helpers_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "testing" 7 | 8 | "github.com/gobuffalo/tags/v3" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type tagHelper = func(string, tags.Options) (template.HTML, error) 13 | 14 | func tag(name string) (tagHelper, error) { 15 | e := NewEngine() 16 | tr := e.Template("").(*templateRenderer) 17 | 18 | h := tr.addAssetsHelpers(Helpers{}) 19 | jt := h[name] 20 | f, ok := jt.(func(string, tags.Options) (template.HTML, error)) 21 | if !ok { 22 | return f, fmt.Errorf("expected tagHelper got %T", jt) 23 | } 24 | return f, nil 25 | } 26 | 27 | func Test_javascriptTag(t *testing.T) { 28 | r := require.New(t) 29 | 30 | f, err := tag("javascriptTag") 31 | r.NoError(err) 32 | 33 | s, err := f("application.js", nil) 34 | r.NoError(err) 35 | r.Equal(template.HTML(``), s) 36 | } 37 | 38 | func Test_javascriptTag_Options(t *testing.T) { 39 | r := require.New(t) 40 | 41 | f, err := tag("javascriptTag") 42 | r.NoError(err) 43 | 44 | s, err := f("application.js", tags.Options{"class": "foo"}) 45 | r.NoError(err) 46 | r.Equal(template.HTML(``), s) 47 | } 48 | 49 | func Test_stylesheetTag(t *testing.T) { 50 | r := require.New(t) 51 | 52 | f, err := tag("stylesheetTag") 53 | r.NoError(err) 54 | 55 | s, err := f("application.css", nil) 56 | r.NoError(err) 57 | r.Equal(template.HTML(``), s) 58 | } 59 | 60 | func Test_stylesheetTag_Options(t *testing.T) { 61 | r := require.New(t) 62 | 63 | f, err := tag("stylesheetTag") 64 | r.NoError(err) 65 | 66 | s, err := f("application.css", tags.Options{"class": "foo"}) 67 | r.NoError(err) 68 | r.Equal(template.HTML(``), s) 69 | } 70 | 71 | func Test_imgTag(t *testing.T) { 72 | r := require.New(t) 73 | 74 | f, err := tag("imgTag") 75 | r.NoError(err) 76 | 77 | s, err := f("foo.png", nil) 78 | r.NoError(err) 79 | r.Equal(template.HTML(``), s) 80 | } 81 | 82 | func Test_imgTag_Options(t *testing.T) { 83 | r := require.New(t) 84 | 85 | f, err := tag("imgTag") 86 | r.NoError(err) 87 | 88 | s, err := f("foo.png", tags.Options{"class": "foo"}) 89 | r.NoError(err) 90 | r.Equal(template.HTML(``), s) 91 | } 92 | -------------------------------------------------------------------------------- /mail/smtp_sender_test.go: -------------------------------------------------------------------------------- 1 | package mail_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/gobuffalo/buffalo/internal/fakesmtp" 8 | "github.com/gobuffalo/buffalo/mail" 9 | "github.com/gobuffalo/buffalo/render" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var sender mail.Sender 14 | var rend *render.Engine 15 | var smtpServer *fakesmtp.Server 16 | 17 | const smtpPort = "2002" 18 | 19 | func init() { 20 | rend = render.New(render.Options{}) 21 | smtpServer, _ = fakesmtp.New(smtpPort) 22 | sender, _ = mail.NewSMTPSender("127.0.0.1", smtpPort, "username", "password") 23 | 24 | go smtpServer.Start(smtpPort) 25 | } 26 | 27 | func TestSendPlain(t *testing.T) { 28 | smtpServer.Clear() 29 | r := require.New(t) 30 | 31 | m := mail.NewMessage() 32 | m.From = "mark@example.com" 33 | m.To = []string{"something@something.com"} 34 | m.Subject = "Cool Message" 35 | m.CC = []string{"other@other.com", "my@other.com"} 36 | m.Bcc = []string{"secret@other.com"} 37 | 38 | m.AddAttachment("someFile.txt", "text/plain", bytes.NewBuffer([]byte("hello"))) 39 | m.AddAttachment("otherFile.txt", "text/plain", bytes.NewBuffer([]byte("bye"))) 40 | m.AddEmbedded("test.jpg", bytes.NewBuffer([]byte("not a real image"))) 41 | m.AddBody(rend.String("Hello <%= Name %>"), render.Data{"Name": "Antonio"}) 42 | r.Equal(m.Bodies[0].Content, "Hello Antonio") 43 | 44 | m.SetHeader("X-SMTPAPI", `{"send_at": 1409348513}`) 45 | 46 | err := sender.Send(m) 47 | r.Nil(err) 48 | 49 | lastMessage := smtpServer.LastMessage() 50 | 51 | r.Contains(lastMessage, "FROM:") 52 | r.Contains(lastMessage, "RCPT TO:") 53 | r.Contains(lastMessage, "RCPT TO:") 54 | r.Contains(lastMessage, "RCPT TO:") 55 | r.Contains(lastMessage, "Subject: Cool Message") 56 | r.Contains(lastMessage, "Cc: other@other.com, my@other.com") 57 | r.Contains(lastMessage, "Content-Type: text/plain") 58 | r.Contains(lastMessage, "Hello Antonio") 59 | r.Contains(lastMessage, "Content-Disposition: attachment; filename=\"someFile.txt\"") 60 | r.Contains(lastMessage, "aGVsbG8=") //base64 of the file content 61 | r.Contains(lastMessage, "Content-Disposition: attachment; filename=\"otherFile.txt\"") 62 | r.Contains(lastMessage, "Ynll") //base64 of the file content 63 | r.Contains(lastMessage, "Content-Disposition: inline; filename=\"test.jpg\"") 64 | r.Contains(lastMessage, "bm90IGEgcmVhbCBpbWFnZQ==") //base64 of the file content 65 | 66 | r.Contains(lastMessage, `X-SMTPAPI: {"send_at": 1409348513}`) 67 | } 68 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | // TODO: TODO-v1 check if they are really need to be exported. 4 | /* The event id should be unique across packages as the format of 5 | "::" as documented. They 6 | should not be used by another packages to keep it informational. To make 7 | it sure, they need to be internal. 8 | Especially for plugable conponents like servers or workers, they can have 9 | their own event definition if they need but the buffalo runtime can emit 10 | generalize events when e.g. the runtime calls configured worker. 11 | */ 12 | const ( 13 | // EvtAppStart is emitted when buffalo.App#Serve is called 14 | EvtAppStart = "buffalo:app:start" 15 | // EvtAppStartErr is emitted when an error occurs calling buffalo.App#Serve 16 | EvtAppStartErr = "buffalo:app:start:err" 17 | 18 | // EvtAppStop is emitted when buffalo.App#Stop is called 19 | EvtAppStop = "buffalo:app:stop" 20 | // EvtAppStopErr is emitted when an error occurs calling buffalo.App#Stop 21 | EvtAppStopErr = "buffalo:app:stop:err" 22 | 23 | // EvtRouteStarted is emitted when a requested route is being processed 24 | EvtRouteStarted = "buffalo:route:started" 25 | // EvtRouteFinished is emitted when a requested route is completed 26 | EvtRouteFinished = "buffalo:route:finished" 27 | // EvtRouteErr is emitted when there is a problem handling processing a route 28 | EvtRouteErr = "buffalo:route:err" 29 | 30 | // EvtServerStart is emitted when buffalo is about to start servers 31 | EvtServerStart = "buffalo:server:start" 32 | // EvtServerStartErr is emitted when an error occurs when starting servers 33 | EvtServerStartErr = "buffalo:server:start:err" 34 | // EvtServerStop is emitted when buffalo is about to stop servers 35 | EvtServerStop = "buffalo:server:stop" 36 | // EvtServerStopErr is emitted when an error occurs when stopping servers 37 | EvtServerStopErr = "buffalo:server:stop:err" 38 | 39 | // EvtWorkerStart is emitted when buffalo is about to start workers 40 | EvtWorkerStart = "buffalo:worker:start" 41 | // EvtWorkerStartErr is emitted when an error occurs when starting workers 42 | EvtWorkerStartErr = "buffalo:worker:start:err" 43 | // EvtWorkerStop is emitted when buffalo is about to stop workers 44 | EvtWorkerStop = "buffalo:worker:stop" 45 | // EvtWorkerStopErr is emitted when an error occurs when stopping workers 46 | EvtWorkerStopErr = "buffalo:worker:stop:err" 47 | 48 | // EvtFailureErr is emitted when something can't be processed at all. it is a bad thing 49 | EvtFailureErr = "buffalo:failure:err" 50 | ) 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gobuffalo/buffalo 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.2.1 7 | github.com/dustin/go-humanize v1.0.1 8 | github.com/gobuffalo/envy v1.10.2 9 | github.com/gobuffalo/events v1.4.3 10 | github.com/gobuffalo/flect v1.0.2 11 | github.com/gobuffalo/github_flavored_markdown v1.1.3 12 | github.com/gobuffalo/grift v1.5.2 13 | github.com/gobuffalo/helpers v0.6.10 14 | github.com/gobuffalo/httptest v1.5.2 15 | github.com/gobuffalo/logger v1.0.7 16 | github.com/gobuffalo/meta v0.3.3 17 | github.com/gobuffalo/nulls v0.4.2 18 | github.com/gobuffalo/plush/v5 v5.0.4 19 | github.com/gobuffalo/refresh v1.13.3 20 | github.com/gobuffalo/tags/v3 v3.1.4 21 | github.com/gorilla/handlers v1.5.1 22 | github.com/gorilla/mux v1.8.0 23 | github.com/gorilla/sessions v1.2.1 24 | github.com/monoculum/formam v3.5.5+incompatible 25 | github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef 26 | github.com/sirupsen/logrus v1.9.0 27 | github.com/spf13/cobra v1.6.1 28 | github.com/stretchr/testify v1.9.0 29 | golang.org/x/text v0.25.0 30 | ) 31 | 32 | require ( 33 | github.com/aymerick/douceur v0.2.0 // indirect 34 | github.com/davecgh/go-spew v1.1.1 // indirect 35 | github.com/fatih/color v1.13.0 // indirect 36 | github.com/fatih/structs v1.1.0 // indirect 37 | github.com/felixge/httpsnoop v1.0.1 // indirect 38 | github.com/fsnotify/fsnotify v1.6.0 // indirect 39 | github.com/gobuffalo/validate/v3 v3.3.3 // indirect 40 | github.com/gofrs/uuid v4.2.0+incompatible // indirect 41 | github.com/gorilla/css v1.0.0 // indirect 42 | github.com/gorilla/securecookie v1.1.1 // indirect 43 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 44 | github.com/joho/godotenv v1.4.0 // indirect 45 | github.com/mattn/go-colorable v0.1.9 // indirect 46 | github.com/mattn/go-isatty v0.0.14 // indirect 47 | github.com/microcosm-cc/bluemonday v1.0.20 // indirect 48 | github.com/mitchellh/go-homedir v1.1.0 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/rogpeppe/go-internal v1.9.0 // indirect 51 | github.com/sergi/go-diff v1.2.0 // indirect 52 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect 53 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect 54 | github.com/spf13/pflag v1.0.5 // indirect 55 | golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect 56 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect 57 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/url" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // Routes returns a list of all of the routes defined 12 | // in this application. 13 | func (a *App) Routes() RouteList { 14 | // CHKME: why this function is exported? can we deprecate it? 15 | if a.root != nil { 16 | return a.root.routes 17 | } 18 | return a.routes 19 | } 20 | 21 | func addExtraParamsTo(path string, opts map[string]interface{}) string { 22 | pendingParams := map[string]string{} 23 | keys := []string{} 24 | for k, v := range opts { 25 | if strings.Contains(path, fmt.Sprintf("%v", v)) { 26 | continue 27 | } 28 | 29 | keys = append(keys, k) 30 | pendingParams[k] = fmt.Sprintf("%v", v) 31 | } 32 | 33 | if len(keys) == 0 { 34 | return path 35 | } 36 | 37 | if !strings.Contains(path, "?") { 38 | path = path + "?" 39 | } else { 40 | if !strings.HasSuffix(path, "?") { 41 | path = path + "&" 42 | } 43 | } 44 | 45 | sort.Strings(keys) 46 | 47 | for index, k := range keys { 48 | format := "%v=%v" 49 | 50 | if index > 0 { 51 | format = "&%v=%v" 52 | } 53 | 54 | path = path + fmt.Sprintf(format, url.QueryEscape(k), url.QueryEscape(pendingParams[k])) 55 | } 56 | 57 | return path 58 | } 59 | 60 | //RouteHelperFunc represents the function that takes the route and the opts and build the path 61 | type RouteHelperFunc func(opts map[string]interface{}) (template.HTML, error) 62 | 63 | // RouteList contains a mapping of the routes defined 64 | // in the application. This listing contains, Method, Path, 65 | // and the name of the Handler defined to process that route. 66 | type RouteList []*RouteInfo 67 | 68 | var methodOrder = map[string]string{ 69 | "GET": "1", 70 | "POST": "2", 71 | "PUT": "3", 72 | "DELETE": "4", 73 | } 74 | 75 | func (a RouteList) Len() int { return len(a) } 76 | func (a RouteList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 77 | func (a RouteList) Less(i, j int) bool { 78 | // NOTE: it was used for sorting of app.routes but we don't sort the routes anymore. 79 | // keep it for compatibility but could be deprecated. 80 | x := a[i].App.host + a[i].Path + methodOrder[a[i].Method] 81 | y := a[j].App.host + a[j].Path + methodOrder[a[j].Method] 82 | return x < y 83 | } 84 | 85 | // Lookup search a specific PathName in the RouteList and return the *RouteInfo 86 | func (a RouteList) Lookup(name string) (*RouteInfo, error) { 87 | for _, ri := range a { 88 | if ri.PathName == name { 89 | return ri, nil 90 | } 91 | } 92 | return nil, fmt.Errorf("path name not found") 93 | } 94 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Resource interface allows for the easy mapping 9 | // of common RESTful actions to a set of paths. See 10 | // the a.Resource documentation for more details. 11 | // NOTE: When skipping Resource handlers, you need to first declare your 12 | // resource handler as a type of buffalo.Resource for the Skip function to 13 | // properly recognize and match it. 14 | /* 15 | // Works: 16 | var cr Resource 17 | cr = &carsResource{&buffaloBaseResource{}} 18 | g = a.Resource("/cars", cr) 19 | g.Use(SomeMiddleware) 20 | g.Middleware.Skip(SomeMiddleware, cr.Show) 21 | 22 | // Doesn't Work: 23 | cr := &carsResource{&buffaloBaseResource{}} 24 | g = a.Resource("/cars", cr) 25 | g.Use(SomeMiddleware) 26 | g.Middleware.Skip(SomeMiddleware, cr.Show) 27 | */ 28 | type Resource interface { 29 | List(Context) error 30 | Show(Context) error 31 | Create(Context) error 32 | Update(Context) error 33 | Destroy(Context) error 34 | } 35 | 36 | // Middler can be implemented to specify additional 37 | // middleware specific to the resource 38 | type Middler interface { 39 | Use() []MiddlewareFunc 40 | } 41 | 42 | // BaseResource fills in the gaps for any Resource interface 43 | // functions you don't want/need to implement. 44 | /* 45 | type UsersResource struct { 46 | Resource 47 | } 48 | 49 | func (ur *UsersResource) List(c Context) error { 50 | return c.Render(http.StatusOK, render.String("hello") 51 | } 52 | 53 | // This will fulfill the Resource interface, despite only having 54 | // one of the functions defined. 55 | &UsersResource{&BaseResource{}) 56 | */ 57 | type BaseResource struct{} 58 | 59 | // List default implementation. Returns a 404 60 | func (v BaseResource) List(c Context) error { 61 | return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) 62 | } 63 | 64 | // Show default implementation. Returns a 404 65 | func (v BaseResource) Show(c Context) error { 66 | return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) 67 | } 68 | 69 | // Create default implementation. Returns a 404 70 | func (v BaseResource) Create(c Context) error { 71 | return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) 72 | } 73 | 74 | // Update default implementation. Returns a 404 75 | func (v BaseResource) Update(c Context) error { 76 | return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) 77 | } 78 | 79 | // Destroy default implementation. Returns a 404 80 | func (v BaseResource) Destroy(c Context) error { 81 | return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) 82 | } 83 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | ) 8 | 9 | // FS wraps a directory and an embed FS that are expected to have the same contents. 10 | // it prioritizes the directory FS and falls back to the embedded FS if the file cannot 11 | // be found on disk. This is useful during development or when deploying with 12 | // assets not embedded in the binary. 13 | // 14 | // Additionally FS hiddes any file named embed.go from the FS. 15 | type FS struct { 16 | embed fs.FS 17 | dir fs.FS 18 | } 19 | 20 | // NewFS returns a new FS that wraps the given directory and embedded FS. 21 | // the embed.FS is expected to embed the same files as the directory FS. 22 | func NewFS(embed fs.ReadDirFS, dir string) FS { 23 | return FS{ 24 | embed: embed, 25 | dir: os.DirFS(dir), 26 | } 27 | } 28 | 29 | // Open opens the named file. 30 | // 31 | // When Open returns an error, it should be of type *PathError with the Op 32 | // field set to "open", the Path field set to name, and the Err field 33 | // describing the problem. 34 | // 35 | // Open should reject attempts to open names that do not satisfy 36 | // ValidPath(name), returning a *PathError with Err set to ErrInvalid or 37 | // ErrNotExist. 38 | func (f FS) Open(name string) (fs.File, error) { 39 | if name == "embed.go" { 40 | return nil, &fs.PathError{ 41 | Op: "open", 42 | Path: name, 43 | Err: fs.ErrNotExist, 44 | } 45 | } 46 | file, err := f.getFile(name) 47 | if name == "." { 48 | // NOTE: It always returns the root from the "disk" instead 49 | // "embed". However, it could be fine since the the purpose 50 | // of buffalo.FS isn't supporting full featured filesystem. 51 | return rootFile{file}, err 52 | } 53 | return file, err 54 | } 55 | 56 | func (f FS) getFile(name string) (fs.File, error) { 57 | file, err := f.dir.Open(name) 58 | if err == nil { 59 | return file, nil 60 | } 61 | 62 | return f.embed.Open(name) 63 | } 64 | 65 | // rootFile wraps the "." directory for hidding the embed.go file. 66 | type rootFile struct { 67 | fs.File 68 | } 69 | 70 | // ReadDir implements the fs.ReadDirFile interface. 71 | func (f rootFile) ReadDir(n int) (entries []fs.DirEntry, err error) { 72 | dir, ok := f.File.(fs.ReadDirFile) 73 | if !ok { 74 | return nil, fmt.Errorf("%T is not a directory", f.File) 75 | } 76 | 77 | entries, err = dir.ReadDir(n) 78 | entries = hideEmbedFile(entries) 79 | return entries, err 80 | } 81 | 82 | func hideEmbedFile(entries []fs.DirEntry) []fs.DirEntry { 83 | result := make([]fs.DirEntry, 0, len(entries)) 84 | 85 | for _, entry := range entries { 86 | if entry.Name() != "embed.go" { 87 | result = append(result, entry) 88 | } 89 | } 90 | return result 91 | } 92 | -------------------------------------------------------------------------------- /mail/message.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | 8 | "bytes" 9 | 10 | "github.com/gobuffalo/buffalo/render" 11 | ) 12 | 13 | //Message represents an Email message 14 | type Message struct { 15 | Context context.Context 16 | From string 17 | To []string 18 | CC []string 19 | Bcc []string 20 | Subject string 21 | Headers map[string]string 22 | Data render.Data 23 | Bodies []Body 24 | Attachments []Attachment 25 | moot *sync.RWMutex 26 | } 27 | 28 | func (m *Message) merge(data render.Data) render.Data { 29 | d := render.Data{} 30 | m.moot.Lock() 31 | for k, v := range m.Data { 32 | d[k] = v 33 | } 34 | m.moot.Unlock() 35 | for k, v := range data { 36 | d[k] = v 37 | } 38 | return d 39 | } 40 | 41 | // AddBody the message by receiving a renderer and rendering data, first message will be 42 | // used as the main message Body rest of them will be passed as alternative bodies on the 43 | // email message 44 | func (m *Message) AddBody(r render.Renderer, data render.Data) error { 45 | buf := bytes.NewBuffer([]byte{}) 46 | err := r.Render(buf, m.merge(data)) 47 | 48 | if err != nil { 49 | return err 50 | } 51 | 52 | m.Bodies = append(m.Bodies, Body{ 53 | Content: buf.String(), 54 | ContentType: r.ContentType(), 55 | }) 56 | 57 | return nil 58 | } 59 | 60 | // AddBodies Allows to add multiple bodies to the message, it returns errors that 61 | // could happen in the rendering. 62 | func (m *Message) AddBodies(data render.Data, renderers ...render.Renderer) error { 63 | for _, r := range renderers { 64 | err := m.AddBody(r, data) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | //AddAttachment adds the attachment to the list of attachments the Message has. 74 | func (m *Message) AddAttachment(name, contentType string, r io.Reader) error { 75 | m.Attachments = append(m.Attachments, Attachment{ 76 | Name: name, 77 | ContentType: contentType, 78 | Reader: r, 79 | Embedded: false, 80 | }) 81 | 82 | return nil 83 | } 84 | 85 | //AddEmbedded adds the attachment to the list of attachments 86 | // the Message has and uses inline instead of attachement property. 87 | func (m *Message) AddEmbedded(name string, r io.Reader) error { 88 | m.Attachments = append(m.Attachments, Attachment{ 89 | Name: name, 90 | Reader: r, 91 | Embedded: true, 92 | }) 93 | 94 | return nil 95 | } 96 | 97 | // SetHeader sets the heder field and value for the message 98 | func (m *Message) SetHeader(field, value string) { 99 | m.Headers[field] = value 100 | } 101 | -------------------------------------------------------------------------------- /render/html_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/psanford/memfs" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const htmlLayout = "layout.html" 13 | const htmlAltLayout = "alt_layout.plush.html" 14 | const htmlTemplate = "my-template.html" 15 | 16 | func Test_HTML_WithoutLayout(t *testing.T) { 17 | r := require.New(t) 18 | 19 | rootFS := memfs.New() 20 | r.NoError(rootFS.WriteFile(htmlTemplate, []byte("<%= name %>"), 0644)) 21 | 22 | e := NewEngine() 23 | e.TemplatesFS = rootFS 24 | 25 | h := e.HTML(htmlTemplate) 26 | r.Equal("text/html; charset=utf-8", h.ContentType()) 27 | bb := &bytes.Buffer{} 28 | 29 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 30 | r.Equal("Mark", strings.TrimSpace(bb.String())) 31 | } 32 | 33 | func Test_HTML_WithLayout(t *testing.T) { 34 | r := require.New(t) 35 | 36 | rootFS := memfs.New() 37 | r.NoError(rootFS.WriteFile(htmlTemplate, []byte("<%= name %>"), 0644)) 38 | r.NoError(rootFS.WriteFile(htmlLayout, []byte("<%= yield %>"), 0644)) 39 | 40 | e := NewEngine() 41 | e.TemplatesFS = rootFS 42 | e.HTMLLayout = htmlLayout 43 | 44 | h := e.HTML(htmlTemplate) 45 | r.Equal("text/html; charset=utf-8", h.ContentType()) 46 | bb := &bytes.Buffer{} 47 | 48 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 49 | r.Equal("Mark", strings.TrimSpace(bb.String())) 50 | } 51 | 52 | func Test_HTML_WithLayout_Override(t *testing.T) { 53 | r := require.New(t) 54 | 55 | rootFS := memfs.New() 56 | r.NoError(rootFS.WriteFile(htmlTemplate, []byte("<%= name %>"), 0644)) 57 | r.NoError(rootFS.WriteFile(htmlLayout, []byte("<%= yield %>"), 0644)) 58 | r.NoError(rootFS.WriteFile(htmlAltLayout, []byte("<%= yield %>"), 0644)) 59 | 60 | e := NewEngine() 61 | e.TemplatesFS = rootFS 62 | e.HTMLLayout = htmlLayout 63 | 64 | h := e.HTML(htmlTemplate, htmlAltLayout) 65 | r.Equal("text/html; charset=utf-8", h.ContentType()) 66 | bb := &bytes.Buffer{} 67 | 68 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 69 | r.Equal("Mark", strings.TrimSpace(bb.String())) 70 | } 71 | 72 | func Test_HTML_LeadingSlash(t *testing.T) { 73 | r := require.New(t) 74 | 75 | rootFS := memfs.New() 76 | r.NoError(rootFS.WriteFile(htmlTemplate, []byte("<%= name %>"), 0644)) 77 | r.NoError(rootFS.WriteFile(htmlLayout, []byte("<%= yield %>"), 0644)) 78 | 79 | e := NewEngine() 80 | e.TemplatesFS = rootFS 81 | e.HTMLLayout = htmlLayout 82 | 83 | h := e.HTML("/my-template.html") // instead of "my-template.html" 84 | r.Equal("text/html; charset=utf-8", h.ContentType()) 85 | bb := &bytes.Buffer{} 86 | 87 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 88 | r.Equal("Mark", strings.TrimSpace(bb.String())) 89 | } 90 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/gobuffalo/buffalo/binding" 9 | "github.com/gobuffalo/buffalo/internal/httpx" 10 | "github.com/gobuffalo/buffalo/render" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | // Context holds on to information as you 15 | // pass it down through middleware, Handlers, 16 | // templates, etc... It strives to make your 17 | // life a happier one. 18 | type Context interface { 19 | context.Context 20 | Response() http.ResponseWriter 21 | Request() *http.Request 22 | Session() *Session 23 | Cookies() *Cookies 24 | Params() ParamValues 25 | Param(string) string 26 | Set(string, interface{}) 27 | LogField(string, interface{}) 28 | LogFields(map[string]interface{}) 29 | Logger() Logger 30 | Bind(interface{}) error 31 | Render(int, render.Renderer) error 32 | Error(int, error) error 33 | Redirect(int, string, ...interface{}) error 34 | Data() map[string]interface{} 35 | Flash() *Flash 36 | File(string) (binding.File, error) 37 | } 38 | 39 | // ParamValues will most commonly be url.Values, 40 | // but isn't it great that you set your own? :) 41 | type ParamValues interface { 42 | Get(string) string 43 | } 44 | 45 | func (a *App) newContext(info RouteInfo, res http.ResponseWriter, req *http.Request) Context { 46 | if ws, ok := res.(*Response); ok { 47 | res = ws 48 | } 49 | 50 | // Parse URL Params 51 | params := url.Values{} 52 | vars := mux.Vars(req) 53 | for k, v := range vars { 54 | params.Add(k, v) 55 | } 56 | 57 | // Parse URL Query String Params 58 | // For POST, PUT, and PATCH requests, it also parse the request body as a form. 59 | // Request body parameters take precedence over URL query string values in params 60 | if err := req.ParseForm(); err == nil { 61 | for k, v := range req.Form { 62 | for _, vv := range v { 63 | params.Add(k, vv) 64 | } 65 | } 66 | } 67 | 68 | session := a.getSession(req, res) 69 | 70 | ct := httpx.ContentType(req) 71 | 72 | data := newRequestData() 73 | data.d = map[string]interface{}{ 74 | "app": a, 75 | "env": a.Env, 76 | "routes": a.Routes(), 77 | "current_route": info, 78 | "current_path": req.URL.Path, 79 | "contentType": ct, 80 | "method": req.Method, 81 | } 82 | 83 | for _, route := range a.Routes() { 84 | cRoute := route 85 | data.d[cRoute.PathName] = cRoute.BuildPathHelper() 86 | } 87 | 88 | return &DefaultContext{ 89 | Context: req.Context(), 90 | contentType: ct, 91 | response: res, 92 | request: req, 93 | params: params, 94 | logger: a.Logger, 95 | session: session, 96 | flash: newFlash(session), 97 | data: data, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /binding/binding.go: -------------------------------------------------------------------------------- 1 | package binding 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gobuffalo/buffalo/binding/decoders" 8 | "github.com/gobuffalo/nulls" 9 | "github.com/monoculum/formam" 10 | ) 11 | 12 | var ( 13 | // MaxFileMemory can be used to set the maximum size, in bytes, for files to be 14 | // stored in memory during uploaded for multipart requests. 15 | // See https://golang.org/pkg/net/http/#Request.ParseMultipartForm for more 16 | // information on how this impacts file uploads. 17 | MaxFileMemory int64 = 5 * 1024 * 1024 18 | 19 | // formDecoder (formam) that will be used across ContentTypeBinders 20 | formDecoder = buildFormDecoder() 21 | 22 | // BaseRequestBinder is an instance of the requestBinder, it comes with preconfigured 23 | // content type binders for HTML, JSON, XML and Files, as well as custom types decoders 24 | // for time.Time and nulls.Time 25 | BaseRequestBinder = NewRequestBinder( 26 | HTMLContentTypeBinder{ 27 | decoder: formDecoder, 28 | }, 29 | JSONContentTypeBinder{}, 30 | XMLRequestTypeBinder{}, 31 | FileRequestTypeBinder{ 32 | decoder: formDecoder, 33 | }, 34 | ) 35 | ) 36 | 37 | // buildFormDecoder that will be used in the package. This method adds some custom decoders for time.Time and nulls.Time. 38 | func buildFormDecoder() *formam.Decoder { 39 | decoder := formam.NewDecoder(&formam.DecoderOptions{ 40 | TagName: "form", 41 | IgnoreUnknownKeys: true, 42 | }) 43 | 44 | decoder.RegisterCustomType(decoders.TimeDecoderFn(), []interface{}{time.Time{}}, nil) 45 | decoder.RegisterCustomType(decoders.NullTimeDecoderFn(), []interface{}{nulls.Time{}}, nil) 46 | 47 | return decoder 48 | } 49 | 50 | // RegisterTimeFormats allows to add custom time layouts that 51 | // the binder will be able to use for decoding. 52 | func RegisterTimeFormats(layouts ...string) { 53 | decoders.RegisterTimeFormats(layouts...) 54 | } 55 | 56 | // RegisterCustomDecoder allows to define custom decoders for certain types 57 | // In the request. 58 | func RegisterCustomDecoder(fn CustomTypeDecoder, types []interface{}, fields []interface{}) { 59 | rawFunc := (func([]string) (interface{}, error))(fn) 60 | formDecoder.RegisterCustomType(rawFunc, types, fields) 61 | } 62 | 63 | // Register maps a request Content-Type (application/json) 64 | // to a Binder. 65 | func Register(contentType string, fn Binder) { 66 | BaseRequestBinder.Register(contentType, fn) 67 | } 68 | 69 | // Exec will bind the interface to the request.Body. The type of binding 70 | // is dependent on the "Content-Type" for the request. If the type 71 | // is "application/json" it will use "json.NewDecoder". If the type 72 | // is "application/xml" it will use "xml.NewDecoder". The default 73 | // binder is "https://github.com/monoculum/formam". 74 | func Exec(req *http.Request, value interface{}) error { 75 | return BaseRequestBinder.Exec(req, value) 76 | } 77 | -------------------------------------------------------------------------------- /wrappers.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/gobuffalo/buffalo/internal/httpx" 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | // WrapHandler wraps a standard http.Handler and transforms it 12 | // into a buffalo.Handler. 13 | func WrapHandler(h http.Handler) Handler { 14 | return func(c Context) error { 15 | h.ServeHTTP(c.Response(), c.Request()) 16 | return nil 17 | } 18 | } 19 | 20 | // WrapHandlerFunc wraps a standard http.HandlerFunc and 21 | // transforms it into a buffalo.Handler. 22 | func WrapHandlerFunc(h http.HandlerFunc) Handler { 23 | return WrapHandler(h) 24 | } 25 | 26 | // WrapBuffaloHandler wraps a buffalo.Handler to a standard http.Handler 27 | // 28 | // NOTE: A buffalo Handler expects a buffalo Context. WrapBuffaloHandler uses 29 | // the same logic as DefaultContext where possible, but some functionality 30 | // (e.g. sessions and logging) WILL NOT work with this unwrap function. If 31 | // those features are needed a custom UnwrapHandlerFunc needs to be 32 | // implemented that provides a Context implementing those features. 33 | func WrapBuffaloHandler(h Handler) http.Handler { 34 | return WrapBuffaloHandlerFunc(h) 35 | } 36 | 37 | // WrapBuffaloHandlerFunc wraps a buffalo.Handler to a standard http.HandlerFunc 38 | // 39 | // NOTE: A buffalo Handler expects a buffalo Context. WrapBuffaloHandlerFunc uses 40 | // the same logic as DefaultContext where possible, but some functionality 41 | // (e.g. sessions and logging) WILL NOT work with this unwrap function. If 42 | // those features are needed a custom WrapBuffaloHandlerFunc needs to be 43 | // implemented that provides a Context implementing those features. 44 | func WrapBuffaloHandlerFunc(h Handler) http.HandlerFunc { 45 | return func(res http.ResponseWriter, req *http.Request) { 46 | if ws, ok := res.(*Response); ok { 47 | res = ws 48 | } 49 | 50 | // Parse URL Params 51 | params := url.Values{} 52 | vars := mux.Vars(req) 53 | for k, v := range vars { 54 | params.Add(k, v) 55 | } 56 | 57 | // Parse URL Query String Params 58 | // For POST, PUT, and PATCH requests, it also parse the request body as a form. 59 | // Request body parameters take precedence over URL query string values in params 60 | if err := req.ParseForm(); err == nil { 61 | for k, v := range req.Form { 62 | for _, vv := range v { 63 | params.Add(k, vv) 64 | } 65 | } 66 | } 67 | 68 | ct := httpx.ContentType(req) 69 | 70 | data := newRequestData() 71 | data.d = map[string]interface{}{ 72 | "current_path": req.URL.Path, 73 | "contentType": ct, 74 | "method": req.Method, 75 | } 76 | c := &DefaultContext{ 77 | Context: req.Context(), 78 | contentType: ct, 79 | response: res, 80 | request: req, 81 | params: params, 82 | flash: &Flash{data: map[string][]string{}}, 83 | data: data, 84 | } 85 | h(c) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /render/download.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "mime" 8 | "net/http" 9 | "path/filepath" 10 | "strconv" 11 | ) 12 | 13 | type downloadRenderer struct { 14 | ctx context.Context 15 | name string 16 | reader io.Reader 17 | } 18 | 19 | func (r downloadRenderer) ContentType() string { 20 | ext := filepath.Ext(r.name) 21 | t := mime.TypeByExtension(ext) 22 | if t == "" { 23 | t = "application/octet-stream" 24 | } 25 | 26 | return t 27 | } 28 | 29 | func (r downloadRenderer) Render(w io.Writer, d Data) error { 30 | written, err := io.Copy(w, r.reader) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | ctx, ok := r.ctx.(responsible) 36 | if !ok { 37 | return fmt.Errorf("context has no response writer") 38 | } 39 | 40 | header := ctx.Response().Header() 41 | disposition := fmt.Sprintf("attachment; filename=%s", r.name) 42 | header.Add("Content-Disposition", disposition) 43 | contentLength := strconv.Itoa(int(written)) 44 | header.Add("Content-Length", contentLength) 45 | 46 | return nil 47 | } 48 | 49 | // Download renders a file attachment automatically setting following headers: 50 | // 51 | // Content-Type 52 | // Content-Length 53 | // Content-Disposition 54 | // 55 | // Content-Type is set using mime#TypeByExtension with the filename's extension. Content-Type will default to 56 | // application/octet-stream if using a filename with an unknown extension. 57 | // 58 | // Note: the purpose of this function is not serving static files but to support 59 | // downloading of dynamically genrated data as a file. For example, you can use 60 | // this function when you implement CSV file download feature for the result of 61 | // a database query. 62 | // 63 | // Do not use this function for large io.Reader. It could cause out of memory if 64 | // the size of io.Reader is too big. 65 | func Download(ctx context.Context, name string, r io.Reader) Renderer { 66 | return downloadRenderer{ 67 | ctx: ctx, 68 | name: name, 69 | reader: r, 70 | } 71 | } 72 | 73 | // Download renders a file attachment automatically setting following headers: 74 | // 75 | // Content-Type 76 | // Content-Length 77 | // Content-Disposition 78 | // 79 | // Content-Type is set using mime#TypeByExtension with the filename's extension. Content-Type will default to 80 | // application/octet-stream if using a filename with an unknown extension. 81 | // 82 | // Note: the purpose of this method is not serving static files but to support 83 | // downloading of dynamically genrated data as a file. For example, you can use 84 | // this method when you implement CSV file download feature for the result of 85 | // a database query. 86 | // 87 | // Do not use this method for large io.Reader. It could cause out of memory if 88 | // the size of io.Reader is too big. 89 | func (e *Engine) Download(ctx context.Context, name string, r io.Reader) Renderer { 90 | return Download(ctx, name, r) 91 | } 92 | 93 | type responsible interface { 94 | Response() http.ResponseWriter 95 | } 96 | -------------------------------------------------------------------------------- /mail/internal/mail/send.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | stdmail "net/mail" 7 | ) 8 | 9 | // Sender is the interface that wraps the Send method. 10 | // 11 | // Send sends an email to the given addresses. 12 | type Sender interface { 13 | Send(from string, to []string, msg io.WriterTo) error 14 | } 15 | 16 | // SendCloser is the interface that groups the Send and Close methods. 17 | type SendCloser interface { 18 | Sender 19 | Close() error 20 | } 21 | 22 | // A SendFunc is a function that sends emails to the given addresses. 23 | // 24 | // The SendFunc type is an adapter to allow the use of ordinary functions as 25 | // email senders. If f is a function with the appropriate signature, SendFunc(f) 26 | // is a Sender object that calls f. 27 | type SendFunc func(from string, to []string, msg io.WriterTo) error 28 | 29 | // Send calls f(from, to, msg). 30 | func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error { 31 | return f(from, to, msg) 32 | } 33 | 34 | // Send sends emails using the given Sender. 35 | func Send(s Sender, msg ...*Message) []error { 36 | errors := make([]error, len(msg)) 37 | for i, m := range msg { 38 | if err := send(s, m); err != nil { 39 | errors[i] = &SendError{Cause: err, Index: uint(i)} 40 | } 41 | } 42 | 43 | return errors 44 | } 45 | 46 | func send(s Sender, m *Message) error { 47 | from, err := m.getFrom() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | to, err := m.getRecipients() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if err := s.Send(from, to, m); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (m *Message) getFrom() (string, error) { 65 | from := m.header["Sender"] 66 | if len(from) == 0 { 67 | from = m.header["From"] 68 | if len(from) == 0 { 69 | return "", fmt.Errorf(`gomail: invalid message, "From" field is absent`) 70 | } 71 | } 72 | 73 | return parseAddress(from[0]) 74 | } 75 | 76 | func (m *Message) getRecipients() ([]string, error) { 77 | n := 0 78 | for _, field := range []string{"To", "Cc", "Bcc"} { 79 | if addresses, ok := m.header[field]; ok { 80 | n += len(addresses) 81 | } 82 | } 83 | list := make([]string, 0, n) 84 | 85 | for _, field := range []string{"To", "Cc", "Bcc"} { 86 | if addresses, ok := m.header[field]; ok { 87 | for _, a := range addresses { 88 | addr, err := parseAddress(a) 89 | if err != nil { 90 | return nil, err 91 | } 92 | list = addAddress(list, addr) 93 | } 94 | } 95 | } 96 | 97 | return list, nil 98 | } 99 | 100 | func addAddress(list []string, addr string) []string { 101 | for _, a := range list { 102 | if addr == a { 103 | return list 104 | } 105 | } 106 | 107 | return append(list, addr) 108 | } 109 | 110 | func parseAddress(field string) (string, error) { 111 | addr, err := stdmail.ParseAddress(field) 112 | if err != nil { 113 | return "", fmt.Errorf("gomail: invalid address %q: %v", field, err) 114 | } 115 | return addr.Address, nil 116 | } 117 | -------------------------------------------------------------------------------- /wrappers_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gobuffalo/buffalo/render" 8 | "github.com/gobuffalo/httptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_WrapHandlerFunc(t *testing.T) { 13 | r := require.New(t) 14 | 15 | a := New(Options{}) 16 | a.GET("/foo", WrapHandlerFunc(func(res http.ResponseWriter, req *http.Request) { 17 | res.Write([]byte("hello")) 18 | })) 19 | 20 | w := httptest.New(a) 21 | res := w.HTML("/foo").Get() 22 | 23 | r.Equal("hello", res.Body.String()) 24 | } 25 | 26 | func Test_WrapHandler(t *testing.T) { 27 | r := require.New(t) 28 | 29 | a := New(Options{}) 30 | a.GET("/foo", WrapHandler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 31 | res.Write([]byte("hello")) 32 | }))) 33 | 34 | w := httptest.New(a) 35 | res := w.HTML("/foo").Get() 36 | 37 | r.Equal("hello", res.Body.String()) 38 | } 39 | 40 | func Test_WrapBuffaloHandler(t *testing.T) { 41 | r := require.New(t) 42 | 43 | tt := []struct { 44 | verb string 45 | path string 46 | status int 47 | }{ 48 | {"GET", "/", 200}, 49 | {"GET", "/foo", 201}, 50 | {"POST", "/", 300}, 51 | {"POST", "/foo", 400}, 52 | } 53 | for _, x := range tt { 54 | bf := func(c Context) error { 55 | req := c.Request() 56 | return c.Render(x.status, render.String(req.Method+req.URL.Path)) 57 | } 58 | 59 | h := WrapBuffaloHandler(bf) 60 | r.NotNil(h) 61 | 62 | req := httptest.NewRequest(x.verb, x.path, nil) 63 | res := httptest.NewRecorder() 64 | 65 | h.ServeHTTP(res, req) 66 | 67 | r.Equal(x.status, res.Code) 68 | r.Contains(res.Body.String(), x.verb+x.path) 69 | } 70 | } 71 | 72 | func Test_WrapBuffaloHandlerFunc(t *testing.T) { 73 | r := require.New(t) 74 | 75 | tt := []struct { 76 | verb string 77 | path string 78 | status int 79 | }{ 80 | {"GET", "/", 200}, 81 | {"GET", "/foo", 201}, 82 | {"POST", "/", 300}, 83 | {"POST", "/foo", 400}, 84 | } 85 | for _, x := range tt { 86 | bf := func(c Context) error { 87 | req := c.Request() 88 | return c.Render(x.status, render.String(req.Method+req.URL.Path)) 89 | } 90 | 91 | h := WrapBuffaloHandlerFunc(bf) 92 | r.NotNil(h) 93 | 94 | req := httptest.NewRequest(x.verb, x.path, nil) 95 | res := httptest.NewRecorder() 96 | 97 | h(res, req) 98 | 99 | r.Equal(x.status, res.Code) 100 | r.Contains(res.Body.String(), x.verb+x.path) 101 | } 102 | } 103 | 104 | func Benchmark_WrapBuffaloHandler(b *testing.B) { 105 | r := require.New(b) 106 | 107 | status := http.StatusOK 108 | 109 | bf := func(c Context) error { 110 | return c.Render(status, render.String(http.StatusText(status))) 111 | } 112 | 113 | req := httptest.NewRequest(http.MethodGet, "/foo", nil) 114 | res := httptest.NewRecorder() 115 | 116 | b.StartTimer() 117 | for i := 0; i < b.N; i++ { 118 | 119 | h := WrapBuffaloHandler(bf) 120 | r.NotNil(h) 121 | 122 | h.ServeHTTP(res, req) 123 | 124 | r.Equal(status, res.Code) 125 | r.Contains(res.Body.String(), http.StatusText(status)) 126 | } 127 | b.StopTimer() 128 | } 129 | -------------------------------------------------------------------------------- /mail/smtp_sender.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | 8 | gomail "github.com/gobuffalo/buffalo/mail/internal/mail" 9 | ) 10 | 11 | //SMTPSender allows to send Emails by connecting to a SMTP server. 12 | type SMTPSender struct { 13 | Dialer *gomail.Dialer 14 | } 15 | 16 | //Send a message using SMTP configuration or returns an error if something goes wrong. 17 | func (sm SMTPSender) Send(message Message) error { 18 | return sm.Dialer.DialAndSend(sm.prepareMessage(message)) 19 | } 20 | 21 | // SendBatch of message with one connection, returns general error or errors specific for each message 22 | func (sm SMTPSender) SendBatch(messages ...Message) (errorsByMessages []error, generalError error) { 23 | preparedMessages := make([]*gomail.Message, len(messages)) 24 | for i, message := range messages { 25 | preparedMessages[i] = sm.prepareMessage(message) 26 | } 27 | 28 | s, err := sm.Dialer.Dial() 29 | if err != nil { 30 | return nil, err 31 | } 32 | defer s.Close() 33 | 34 | return gomail.Send(s, preparedMessages...), nil 35 | } 36 | func (sm SMTPSender) prepareMessage(message Message) *gomail.Message { 37 | gm := gomail.NewMessage() 38 | 39 | gm.SetHeader("From", message.From) 40 | gm.SetHeader("To", message.To...) 41 | gm.SetHeader("Subject", message.Subject) 42 | gm.SetHeader("Cc", message.CC...) 43 | gm.SetHeader("Bcc", message.Bcc...) 44 | 45 | sm.addBodies(message, gm) 46 | sm.addAttachments(message, gm) 47 | 48 | for field, value := range message.Headers { 49 | gm.SetHeader(field, value) 50 | } 51 | 52 | return gm 53 | } 54 | 55 | func (sm SMTPSender) addBodies(message Message, gm *gomail.Message) { 56 | if len(message.Bodies) == 0 { 57 | return 58 | } 59 | 60 | mainBody := message.Bodies[0] 61 | gm.SetBody(mainBody.ContentType, mainBody.Content, gomail.SetPartEncoding(gomail.Unencoded)) 62 | 63 | for i := 1; i < len(message.Bodies); i++ { 64 | alt := message.Bodies[i] 65 | gm.AddAlternative(alt.ContentType, alt.Content, gomail.SetPartEncoding(gomail.Unencoded)) 66 | } 67 | } 68 | 69 | func (sm SMTPSender) addAttachments(message Message, gm *gomail.Message) { 70 | 71 | for _, at := range message.Attachments { 72 | currentAttachement := at 73 | settings := gomail.SetCopyFunc(func(w io.Writer) error { 74 | _, err := io.Copy(w, currentAttachement.Reader) 75 | return err 76 | }) 77 | 78 | if currentAttachement.Embedded { 79 | gm.Embed(currentAttachement.Name, settings) 80 | } else { 81 | gm.Attach(currentAttachement.Name, settings) 82 | } 83 | 84 | } 85 | } 86 | 87 | //NewSMTPSender builds a SMTP mail based in passed config. 88 | func NewSMTPSender(host string, port string, user string, password string) (SMTPSender, error) { 89 | iport, err := strconv.Atoi(port) 90 | 91 | if err != nil { 92 | return SMTPSender{}, fmt.Errorf("invalid port for the SMTP mail") 93 | } 94 | 95 | dialer := &gomail.Dialer{ 96 | Host: host, 97 | Port: iport, 98 | } 99 | 100 | if user != "" { 101 | dialer.Username = user 102 | dialer.Password = password 103 | } 104 | 105 | return SMTPSender{ 106 | Dialer: dialer, 107 | }, nil 108 | } 109 | -------------------------------------------------------------------------------- /fs_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | "testing" 7 | 8 | "github.com/gobuffalo/buffalo/internal/testdata/embedded" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_FS_Disallows_Parent_Folders(t *testing.T) { 13 | r := require.New(t) 14 | 15 | fsys := NewFS(embedded.FS(), "internal/testdata/disk") 16 | r.NotNil(fsys) 17 | 18 | f, err := fsys.Open("../panic.txt") 19 | r.ErrorIs(err, fs.ErrNotExist) 20 | r.Nil(f) 21 | 22 | f, err = fsys.Open("try/../to/../trick/../panic.txt") 23 | r.ErrorIs(err, fs.ErrNotExist) 24 | r.Nil(f) 25 | } 26 | 27 | func Test_FS_Hides_embed_go(t *testing.T) { 28 | r := require.New(t) 29 | 30 | fsys := NewFS(embedded.FS(), "internal/testdata/disk") 31 | r.NotNil(fsys) 32 | 33 | f, err := fsys.Open("embed.go") 34 | r.ErrorIs(err, fs.ErrNotExist) 35 | r.Nil(f) 36 | } 37 | 38 | func Test_FS_Prioritizes_Disk(t *testing.T) { 39 | r := require.New(t) 40 | 41 | fsys := NewFS(embedded.FS(), "internal/testdata/disk") 42 | r.NotNil(fsys) 43 | 44 | f, err := fsys.Open("file.txt") 45 | r.NoError(err) 46 | 47 | b, err := io.ReadAll(f) 48 | r.NoError(err) 49 | 50 | r.Equal("This file is on disk.", string(b)) 51 | 52 | // should handle slash-separated path for all systems including Windows 53 | f, err = fsys.Open("under/sub/subfile") 54 | r.NoError(err) 55 | 56 | b, err = io.ReadAll(f) 57 | r.NoError(err) 58 | 59 | r.Equal("This file is on disk/sub.", string(b)) 60 | } 61 | 62 | func Test_FS_Uses_Embed_If_No_Disk(t *testing.T) { 63 | r := require.New(t) 64 | 65 | fsys := NewFS(embedded.FS(), "internal/testdata/empty") 66 | r.NotNil(fsys) 67 | 68 | f, err := fsys.Open("file.txt") 69 | r.NoError(err) 70 | 71 | b, err := io.ReadAll(f) 72 | r.NoError(err) 73 | 74 | r.Equal("This file is embedded.", string(b)) 75 | 76 | // should handle slash-separated path for all systems including Windows 77 | f, err = fsys.Open("under/sub/subfile") 78 | r.NoError(err) 79 | 80 | b, err = io.ReadAll(f) 81 | r.NoError(err) 82 | 83 | r.Equal("This file is on embedded/sub.", string(b)) 84 | } 85 | 86 | func Test_FS_ReadDirFile(t *testing.T) { 87 | r := require.New(t) 88 | 89 | fsys := NewFS(embedded.FS(), "internal/testdata/disk") 90 | r.NotNil(fsys) 91 | 92 | f, err := fsys.Open(".") 93 | r.NoError(err) 94 | 95 | dir, ok := f.(fs.ReadDirFile) 96 | r.True(ok, "folder does not implement fs.ReadDirFile interface") 97 | 98 | // First read should return at most 1 file 99 | entries, err := dir.ReadDir(1) 100 | r.NoError(err) 101 | 102 | // The actual len will be 0 because the first file read is the embed.go file 103 | // this is counter-intuitive, but it's how the fs.ReadDirFile interface is specified; 104 | // if err == nil, just continue to call ReadDir until io.EOF is returned. 105 | r.LessOrEqual(len(entries), 1, "a call to ReadDir must at most return n entries") 106 | 107 | // Second read should return at most 2 files 108 | entries, err = dir.ReadDir(3) 109 | r.NoError(err) 110 | 111 | // The actual len will be 2 (file.txt & file2.txt + under/) 112 | r.LessOrEqual(len(entries), 3, "a call to ReadDir must at most return n entries") 113 | 114 | // trying to read next 2 files (none left) 115 | entries, err = dir.ReadDir(2) 116 | r.ErrorIs(err, io.EOF) 117 | r.Empty(entries) 118 | } 119 | -------------------------------------------------------------------------------- /grifts.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "text/tabwriter" 9 | 10 | "github.com/gobuffalo/grift/grift" 11 | ) 12 | 13 | // Grifts decorates the app with tasks 14 | func Grifts(app *App) { 15 | routesGrift(app) 16 | middlewareGrift(app) 17 | secretGrift() 18 | } 19 | 20 | func secretGrift() { 21 | grift.Desc("secret", "Generate a cryptographically secure secret key") 22 | grift.Add("secret", func(c *grift.Context) error { 23 | b := make([]byte, 64) 24 | _, err := rand.Read(b) 25 | if err != nil { 26 | return err 27 | } 28 | fmt.Println(string(b)) 29 | return nil 30 | }) 31 | } 32 | 33 | func middlewareGrift(a *App) { 34 | grift.Desc("middleware", "Prints out your middleware stack") 35 | grift.Add("middleware", func(c *grift.Context) error { 36 | printMiddleware(a) 37 | return nil 38 | }) 39 | } 40 | 41 | func printMiddleware(a *App) { 42 | printMiddlewareByRoute(a) 43 | } 44 | 45 | func printMiddlewareByRoute(a *App) { 46 | mws := map[string]string{} 47 | // TODO: middleware is 'per App' can it be a loop for Apps? 48 | for _, r := range a.Routes() { 49 | key := r.App.host + r.App.name 50 | if mws[key] == "" { 51 | pname := r.App.host 52 | if parent := getParentApp(r.App.root, r.App.Name); parent != nil { 53 | pname += parent.Name 54 | } 55 | 56 | mws[key] = r.App.Middleware.String() 57 | if pname == key || mws[pname] != mws[key] { 58 | fmt.Printf("-> %s\n", key) 59 | printMiddlewareStackWithIndent(mws[key]) 60 | } else { 61 | fmt.Printf("-> %s (see: %v)\n", key, pname) 62 | } 63 | } 64 | s := "\n" + mws[key] 65 | for k := range r.App.Middleware.skips { 66 | mw := strings.Split(k, funcKeyDelimeter)[0] 67 | h := strings.Split(k, funcKeyDelimeter)[1] 68 | if h == r.HandlerName { 69 | s = strings.Replace(s, "\n"+mw, "", 1) 70 | } 71 | } 72 | if "\n"+mws[key] != s { 73 | ahn := strings.Split(r.HandlerName, "/") 74 | hn := ahn[len(ahn)-1] 75 | fmt.Printf("-> %s %s (by %s)\n", r.Method, r.App.host+r.Path, hn) 76 | printMiddlewareStackWithIndent(s) 77 | } 78 | } 79 | } 80 | 81 | func getParentApp(r *App, name string) *App { 82 | if r == nil { 83 | return nil 84 | } 85 | for _, x := range r.children { 86 | if x.Name == name { 87 | return r 88 | } 89 | if len(x.children) > 0 { 90 | if ret := getParentApp(x, name); ret != nil { 91 | return ret 92 | } 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func printMiddlewareStackWithIndent(s string) { 99 | if s == "" { 100 | s = "[none]" 101 | } 102 | s = strings.Replace(s, "\n", "\n\t", -1) 103 | fmt.Printf("\t%v\n", strings.TrimSpace(s)) 104 | } 105 | 106 | func routesGrift(a *App) { 107 | grift.Desc("routes", "Print out all defined routes") 108 | grift.Add("routes", func(c *grift.Context) error { 109 | routes := a.Routes() 110 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug) 111 | fmt.Fprintln(w, "METHOD\t HOST\t PATH\t ALIASES\t NAME\t HANDLER") 112 | fmt.Fprintln(w, "------\t ----\t ----\t -------\t ----\t -------") 113 | for _, r := range routes { 114 | fmt.Fprintf(w, "%s\t %s\t %s\t %s\t %s\t %s\n", r.Method, r.App.host, r.Path, strings.Join(r.Aliases, " "), r.PathName, r.HandlerName) 115 | } 116 | w.Flush() 117 | return nil 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /mail/README.md: -------------------------------------------------------------------------------- 1 | # github.com/gobuffalo/buffalo/mail 2 | 3 | This package is intended to allow easy Email sending with Buffalo, it allows you to define your custom `mail.Sender` for the provider you would like to use. 4 | 5 | ## Generator 6 | 7 | ```bash 8 | buffalo generate mailer welcome_email 9 | ``` 10 | 11 | ## Example Usage 12 | 13 | ```go 14 | //actions/mail.go 15 | package x 16 | 17 | import ( 18 | "log" 19 | "net/http" 20 | 21 | "github.com/gobuffalo/buffalo/render" 22 | "github.com/gobuffalo/envy" 23 | "github.com/gobuffalo/plush" 24 | "github.com/gobuffalo/buffalo/mail" 25 | "errors" 26 | "gitlab.com/wawandco/app/models" 27 | ) 28 | 29 | var smtp mail.Sender 30 | var r *render.Engine 31 | 32 | func init() { 33 | 34 | //Pulling config from the env. 35 | port := envy.Get("SMTP_PORT", "1025") 36 | host := envy.Get("SMTP_HOST", "localhost") 37 | user := envy.Get("SMTP_USER", "") 38 | password := envy.Get("SMTP_PASSWORD", "") 39 | 40 | var err error 41 | smtp, err = mail.NewSMTPSender(host, port, user, password) 42 | 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | //The rendering engine, this is usually generated inside actions/render.go in your buffalo app. 48 | r = render.New(render.Options{ 49 | TemplatesFS: mailTemplates, 50 | }) 51 | } 52 | 53 | //SendContactMessage Sends contact message to contact@myapp.com 54 | func SendContactMessage(c *models.Contact) error { 55 | 56 | //Creates a new message 57 | m := mail.NewMessage() 58 | m.From = "sender@myapp.com" 59 | m.Subject = "New Contact" 60 | m.To = []string{"contact@myapp.com"} 61 | 62 | // Data that will be used inside the templates when rendering. 63 | data := map[string]interface{}{ 64 | "contact": c, 65 | } 66 | 67 | // You can add multiple bodies to the message you're creating to have content-types alternatives. 68 | err := m.AddBodies(data, r.HTML("mail/contact.html"), r.Plain("mail/contact.txt")) 69 | 70 | if err != nil { 71 | return err 72 | } 73 | 74 | err = smtp.Send(m) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | return nil 80 | } 81 | 82 | ``` 83 | 84 | This `SendContactMessage` could be called by one of your actions, p.e. the action that handles your contact form submission. 85 | 86 | ```go 87 | //actions/contact.go 88 | ... 89 | 90 | func ContactFormHandler(c buffalo.Context) error { 91 | contact := &models.Contact{} 92 | c.Bind(contact) 93 | 94 | //Calling to send the message 95 | SendContactMessage(contact) 96 | return c.Redirect(http.StatusFound, "contact/thanks") 97 | } 98 | ... 99 | ``` 100 | 101 | If you're using Gmail or need to configure your SMTP connection you can use the Dialer property on the SMTPSender, p.e: (for Gmail) 102 | 103 | ```go 104 | ... 105 | var smtp mail.Sender 106 | 107 | func init() { 108 | port := envy.Get("SMTP_PORT", "465") 109 | // or 587 with TLS 110 | 111 | host := envy.Get("SMTP_HOST", "smtp.gmail.com") 112 | user := envy.Get("SMTP_USER", "your@email.com") 113 | password := envy.Get("SMTP_PASSWORD", "yourp4ssw0rd") 114 | 115 | var err error 116 | sender, err := mail.NewSMTPSender(host, port, user, password) 117 | sender.Dialer.SSL = true 118 | 119 | //or if TLS 120 | sender.Dialer.TLSConfig = &tls.Config{...} 121 | 122 | smtp = sender 123 | } 124 | ... 125 | ``` 126 | -------------------------------------------------------------------------------- /route_info.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "html/template" 8 | "net/http" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/gobuffalo/flect" 13 | 14 | "github.com/gobuffalo/events" 15 | "github.com/gorilla/mux" 16 | ) 17 | 18 | // RouteInfo provides information about the underlying route that 19 | // was built. 20 | type RouteInfo struct { 21 | Method string `json:"method"` 22 | Path string `json:"path"` 23 | HandlerName string `json:"handler"` 24 | ResourceName string `json:"resourceName,omitempty"` 25 | PathName string `json:"pathName"` 26 | Aliases []string `json:"aliases"` 27 | MuxRoute *mux.Route `json:"-"` 28 | Handler Handler `json:"-"` 29 | App *App `json:"-"` 30 | } 31 | 32 | // String returns a JSON representation of the RouteInfo 33 | func (ri RouteInfo) String() string { 34 | b, _ := json.MarshalIndent(ri, "", " ") 35 | return string(b) 36 | } 37 | 38 | // Alias path patterns to the this route. This is not the 39 | // same as a redirect. 40 | func (ri *RouteInfo) Alias(aliases ...string) *RouteInfo { 41 | ri.Aliases = append(ri.Aliases, aliases...) 42 | for _, a := range aliases { 43 | ri.App.router.Handle(a, ri).Methods(ri.Method) 44 | } 45 | return ri 46 | } 47 | 48 | // Name allows users to set custom names for the routes. 49 | func (ri *RouteInfo) Name(name string) *RouteInfo { 50 | routeIndex := -1 51 | for index, route := range ri.App.Routes() { 52 | if route.App.host == ri.App.host && route.Path == ri.Path && route.Method == ri.Method { 53 | routeIndex = index 54 | break 55 | } 56 | } 57 | 58 | name = flect.Camelize(name) 59 | 60 | if !strings.HasSuffix(name, "Path") { 61 | name = name + "Path" 62 | } 63 | 64 | ri.PathName = name 65 | if routeIndex != -1 { 66 | ri.App.Routes()[routeIndex] = reflect.ValueOf(ri).Interface().(*RouteInfo) 67 | } 68 | 69 | return ri 70 | } 71 | 72 | // BuildPathHelper Builds a routeHelperfunc for a particular RouteInfo 73 | func (ri *RouteInfo) BuildPathHelper() RouteHelperFunc { 74 | cRoute := ri 75 | return func(opts map[string]interface{}) (template.HTML, error) { 76 | pairs := []string{} 77 | for k, v := range opts { 78 | pairs = append(pairs, k) 79 | pairs = append(pairs, fmt.Sprintf("%v", v)) 80 | } 81 | 82 | url, err := cRoute.MuxRoute.URL(pairs...) 83 | if err != nil { 84 | return "", fmt.Errorf("missing parameters for %v: %s", cRoute.Path, err) 85 | } 86 | 87 | result := url.String() 88 | result = addExtraParamsTo(result, opts) 89 | 90 | return template.HTML(result), nil 91 | } 92 | } 93 | 94 | func (ri RouteInfo) ServeHTTP(res http.ResponseWriter, req *http.Request) { 95 | a := ri.App 96 | 97 | c := a.newContext(ri, res, req) 98 | payload := events.Payload{ 99 | "route": ri, 100 | "app": a, 101 | "context": c, 102 | } 103 | 104 | events.EmitPayload(EvtRouteStarted, payload) 105 | err := a.Middleware.handler(ri)(c) 106 | 107 | if err != nil { 108 | status := http.StatusInternalServerError 109 | var he HTTPError 110 | if errors.As(err, &he) { 111 | status = he.Status 112 | } 113 | events.EmitError(EvtRouteErr, err, payload) 114 | // things have really hit the fan if we're here!! 115 | a.Logger.Error(err) 116 | c.Response().WriteHeader(status) 117 | c.Response().Write([]byte(err.Error())) 118 | } 119 | 120 | events.EmitPayload(EvtRouteFinished, payload) 121 | } 122 | -------------------------------------------------------------------------------- /mail/internal/mail/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## *Unreleased* 6 | 7 | ## [2.3.0] - 2018-11-10 8 | 9 | ### Added 10 | 11 | - #12: Adds `SendError` to provide additional info about the cause and index of 12 | a failed attempt to transmit a batch of messages. 13 | - go-gomail#78: Adds new `Message` methods for attaching and embedding 14 | `io.Reader`s: `AttachReader` and `EmbedReader`. 15 | - #39: Adds support for Go modules (Go 1.11+). 16 | 17 | ### Fixed 18 | 19 | - #26: Fixes RFC 1341 compliance by properly capitalizing the 20 | `MIME-Version` header. 21 | - #30: Fixes IO errors being silently dropped in `Message.WriteTo`. 22 | 23 | ## [2.2.0] - 2018-03-01 24 | 25 | ### Added 26 | 27 | - #20: Adds `Message.SetBoundary` to allow specifying a custom MIME boundary. 28 | - #22: Adds `Message.SetBodyWriter` to make it easy to use text/template and 29 | html/template for message bodies. Contributed by Quantcast. 30 | - #25: Adds `Dialer.StartTLSPolicy` so that `MandatoryStartTLS` can be required, 31 | or `NoStartTLS` can disable it. Contributed by Quantcast. 32 | 33 | ## [2.1.0] - 2017-12-14 34 | 35 | ### Added 36 | 37 | - go-gomail#40: Adds `Dialer.LocalName` field to allow specifying the hostname 38 | sent with SMTP's HELO command. 39 | - go-gomail#47: `Message.SetBody`, `Message.AddAlternative`, and 40 | `Message.AddAlternativeWriter` allow specifying the encoding of message parts. 41 | - `Dialer.Dial`'s returned `SendCloser` automatically redials after a timeout. 42 | - go-gomail#55, go-gomail#56: Adds `Rename` to allow specifying filename 43 | of an attachment. 44 | - go-gomail#100: Exports `NetDialTimeout` to allow setting a custom dialer. 45 | - go-gomail#70: Adds `Dialer.Timeout` field to allow specifying a timeout for 46 | dials, reads, and writes. 47 | 48 | ### Changed 49 | 50 | - go-gomail#52: `Dialer.Dial` automatically uses CRAM-MD5 when available. 51 | - `Dialer.Dial` specifies a default timeout of 10 seconds. 52 | - Gomail is forked from to 53 | . 54 | 55 | ### Deprecated 56 | 57 | - go-gomail#52: `NewPlainDialer` is deprecated in favor of `NewDialer`. 58 | 59 | ### Fixed 60 | 61 | - go-gomail#41, go-gomail#42: Fixes a panic when a `Message` contains a 62 | nil header. 63 | - go-gomail#44: Fixes `AddAlternativeWriter` replacing the message body instead 64 | of adding a body part. 65 | - go-gomail#53: Folds long header lines for RFC 2047 compliance. 66 | - go-gomail#54: Fixes `Message.FormatAddress` when name is blank. 67 | 68 | ## [2.0.0] - 2015-09-02 69 | 70 | - Mailer has been removed. It has been replaced by Dialer and Sender. 71 | - `File` type and the `CreateFile` and `OpenFile` functions have been removed. 72 | - `Message.Attach` and `Message.Embed` have a new signature. 73 | - `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter` 74 | instead. 75 | - `Message.Export` has been removed. `Message.WriteTo` can be used instead. 76 | - `Message.DelHeader` has been removed. 77 | - The `Bcc` header field is no longer sent. It is far more simpler and 78 | efficient: the same message is sent to all recipients instead of sending a 79 | different email to each Bcc address. 80 | - LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN 81 | authentication mechanism when needed. 82 | - Go 1.2 is now required instead of Go 1.3. No external dependency are used when 83 | using Go 1.5. 84 | -------------------------------------------------------------------------------- /internal/fakesmtp/server.go: -------------------------------------------------------------------------------- 1 | package fakesmtp 2 | 3 | // This server is inspired by https://github.com/andrewarrow/jungle_smtp 4 | // and most of its functionality have been taken from the original repo and updated to 5 | // work better for buffalo. 6 | 7 | import ( 8 | "bufio" 9 | "net" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | //Server is our fake server that will be listening for SMTP connections. 16 | type Server struct { 17 | Listener net.Listener 18 | messages []string 19 | mutex sync.Mutex 20 | } 21 | 22 | //Start listens for connections on the given port 23 | func (s *Server) Start(port string) error { 24 | for { 25 | conn, err := s.Listener.Accept() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | s.Handle(&Connection{ 31 | conn: conn, 32 | address: conn.RemoteAddr().String(), 33 | time: time.Now().Unix(), 34 | bufin: bufio.NewReader(conn), 35 | bufout: bufio.NewWriter(conn), 36 | }) 37 | } 38 | } 39 | 40 | //Handle a connection from a client 41 | func (s *Server) Handle(c *Connection) { 42 | s.mutex.Lock() 43 | defer s.mutex.Unlock() 44 | 45 | s.messages = append(s.messages, "") 46 | 47 | s.readHello(c) 48 | s.readSender(c) 49 | s.readRecipients(c) 50 | s.readData(c) 51 | 52 | c.conn.Close() 53 | } 54 | 55 | //Requests and notifies readed the Hello 56 | func (s *Server) readHello(c *Connection) { 57 | c.write("220 Welcome") 58 | text := c.read() 59 | s.addMessageLine(text) 60 | 61 | c.write("250 Received") 62 | } 63 | 64 | //readSender reads the Sender from the connection 65 | func (s *Server) readSender(c *Connection) { 66 | text := c.read() 67 | s.addMessageLine(text) 68 | c.write("250 Sender") 69 | } 70 | 71 | //readRecipients reads recipients from the connection 72 | func (s *Server) readRecipients(c *Connection) { 73 | text := c.read() 74 | s.addMessageLine(text) 75 | 76 | c.write("250 Recipient") 77 | text = c.read() 78 | for strings.Contains(text, "RCPT") { 79 | s.addMessageLine(text) 80 | c.write("250 Recipient") 81 | text = c.read() 82 | } 83 | } 84 | 85 | //readData reads the message data. 86 | func (s *Server) readData(c *Connection) { 87 | c.write("354 Ok Send data ending with .") 88 | 89 | for { 90 | text := c.read() 91 | bytes := []byte(text) 92 | s.addMessageLine(text) 93 | // 46 13 10 94 | if bytes[0] == 46 && bytes[1] == 13 && bytes[2] == 10 { 95 | break 96 | } 97 | } 98 | c.write("250 server has transmitted the message") 99 | } 100 | 101 | //addMessageLine ads a line to the last message 102 | func (s *Server) addMessageLine(text string) { 103 | s.messages[len(s.Messages())-1] = s.LastMessage() + text 104 | } 105 | 106 | //LastMessage returns the last message on the server 107 | func (s *Server) LastMessage() string { 108 | if len(s.Messages()) == 0 { 109 | return "" 110 | } 111 | 112 | return s.Messages()[len(s.Messages())-1] 113 | } 114 | 115 | //Messages returns the list of messages on the server 116 | func (s *Server) Messages() []string { 117 | return s.messages 118 | } 119 | 120 | //Clear the server messages 121 | func (s *Server) Clear() { 122 | s.mutex.Lock() 123 | defer s.mutex.Unlock() 124 | 125 | s.messages = []string{} 126 | } 127 | 128 | //New returns a pointer to a new Server instance listening on the given port. 129 | func New(port string) (*Server, error) { 130 | s := &Server{messages: []string{}} 131 | 132 | listener, err := net.Listen("tcp", "0.0.0.0:"+port) 133 | if err != nil { 134 | return s, err 135 | } 136 | s.Listener = listener 137 | return s, nil 138 | } 139 | -------------------------------------------------------------------------------- /mail/internal/mail/README.md: -------------------------------------------------------------------------------- 1 | # Gomail 2 | [![Build Status](https://travis-ci.org/go-mail/mail.svg?branch=master)](https://travis-ci.org/go-mail/mail) [![Code Coverage](http://gocover.io/_badge/github.com/go-mail/mail)](http://gocover.io/github.com/go-mail/mail) [![Documentation](https://godoc.org/github.com/go-mail/mail?status.svg)](https://godoc.org/github.com/go-mail/mail) 3 | 4 | This is an actively maintained fork of [Gomail][1] and includes fixes and 5 | improvements for a number of outstanding issues. The current progress is 6 | as follows: 7 | 8 | - [x] Timeouts and retries can be specified outside of the 10 second default. 9 | - [x] Proxying is supported through specifying a custom [NetDialTimeout][2]. 10 | - [ ] Filenames are properly encoded for non-ASCII characters. 11 | - [ ] Email addresses are properly encoded for non-ASCII characters. 12 | - [ ] Embedded files and attachments are tested for their existence. 13 | - [ ] An `io.Reader` can be supplied when embedding and attaching files. 14 | 15 | See [Transitioning Existing Codebases][3] for more information on switching. 16 | 17 | [1]: https://github.com/go-gomail/gomail 18 | [2]: https://godoc.org/gopkg.in/mail.v2#NetDialTimeout 19 | [3]: #transitioning-existing-codebases 20 | 21 | ## Introduction 22 | 23 | Gomail is a simple and efficient package to send emails. It is well tested and 24 | documented. 25 | 26 | Gomail can only send emails using an SMTP server. But the API is flexible and it 27 | is easy to implement other methods for sending emails using a local Postfix, an 28 | API, etc. 29 | 30 | It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used. 31 | 32 | 33 | ## Features 34 | 35 | Gomail supports: 36 | - Attachments 37 | - Embedded images 38 | - HTML and text templates 39 | - Automatic encoding of special characters 40 | - SSL and TLS 41 | - Sending multiple emails with the same SMTP connection 42 | 43 | 44 | ## Documentation 45 | 46 | https://godoc.org/github.com/go-mail/mail 47 | 48 | 49 | ## Download 50 | 51 | If you're already using a dependency manager, like [dep][dep], use the following 52 | import path: 53 | 54 | ``` 55 | github.com/go-mail/mail 56 | ``` 57 | 58 | If you *aren't* using vendoring, `go get` the [Gopkg.in](http://gopkg.in) 59 | import path: 60 | 61 | ``` 62 | gopkg.in/mail.v2 63 | ``` 64 | 65 | [dep]: https://github.com/golang/dep#readme 66 | 67 | ## Examples 68 | 69 | See the [examples in the documentation](https://godoc.org/github.com/go-mail/mail#example-package). 70 | 71 | 72 | ## FAQ 73 | 74 | ### x509: certificate signed by unknown authority 75 | 76 | If you get this error it means the certificate used by the SMTP server is not 77 | considered valid by the client running Gomail. As a quick workaround you can 78 | bypass the verification of the server's certificate chain and host name by using 79 | `SetTLSConfig`: 80 | 81 | ```go 82 | package main 83 | 84 | import ( 85 | "crypto/tls" 86 | 87 | "gopkg.in/mail.v2" 88 | ) 89 | 90 | func main() { 91 | d := mail.NewDialer("smtp.example.com", 587, "user", "123456") 92 | d.TLSConfig = &tls.Config{InsecureSkipVerify: true} 93 | 94 | // Send emails using d. 95 | } 96 | ``` 97 | 98 | Note, however, that this is insecure and should not be used in production. 99 | 100 | ### Transitioning Existing Codebases 101 | 102 | If you're already using the original Gomail, switching is as easy as updating 103 | the import line to: 104 | 105 | ``` 106 | import gomail "gopkg.in/mail.v2" 107 | ``` 108 | 109 | ## Contribute 110 | 111 | Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for 112 | more info. 113 | 114 | 115 | ## Change log 116 | 117 | See [CHANGELOG.md](CHANGELOG.md). 118 | 119 | 120 | ## License 121 | 122 | [MIT](LICENSE) 123 | 124 | 125 | ## Support & Contact 126 | 127 | You can ask questions on the [Gomail 128 | thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion) 129 | in the Go mailing-list. 130 | -------------------------------------------------------------------------------- /flash_test.go: -------------------------------------------------------------------------------- 1 | package buffalo 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "text/template" 7 | 8 | "github.com/gobuffalo/buffalo/render" 9 | "github.com/gobuffalo/httptest" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_FlashAdd(t *testing.T) { 14 | r := require.New(t) 15 | f := newFlash(&Session{}) 16 | 17 | r.Equal(f.data, map[string][]string{}) 18 | 19 | f.Add("error", "something") 20 | r.Equal(f.data, map[string][]string{ 21 | "error": {"something"}, 22 | }) 23 | 24 | f.Add("error", "other") 25 | r.Equal(f.data, map[string][]string{ 26 | "error": {"something", "other"}, 27 | }) 28 | } 29 | 30 | func Test_FlashRender(t *testing.T) { 31 | r := require.New(t) 32 | a := New(Options{}) 33 | rr := render.New(render.Options{}) 34 | 35 | a.GET("/", func(c Context) error { 36 | c.Flash().Add("errors", "Error AJ set") 37 | c.Flash().Add("errors", "Error DAL set") 38 | 39 | return c.Render(http.StatusCreated, rr.String(errorsTPL)) 40 | }) 41 | 42 | w := httptest.New(a) 43 | res := w.HTML("/").Get() 44 | 45 | r.Contains(res.Body.String(), "Error AJ set") 46 | r.Contains(res.Body.String(), "Error DAL set") 47 | } 48 | 49 | func Test_FlashRenderEmpty(t *testing.T) { 50 | r := require.New(t) 51 | a := New(Options{}) 52 | rr := render.New(render.Options{}) 53 | 54 | a.GET("/", func(c Context) error { 55 | return c.Render(http.StatusCreated, rr.String(errorsTPL)) 56 | }) 57 | 58 | w := httptest.New(a) 59 | 60 | res := w.HTML("/").Get() 61 | r.NotContains(res.Body.String(), "Flash:") 62 | } 63 | 64 | const errorsTPL = ` 65 | <%= for (k, v) in flash["errors"] { %> 66 | Flash: 67 | <%= k %>:<%= v %> 68 | <% } %> 69 | ` 70 | 71 | func Test_FlashRenderEntireFlash(t *testing.T) { 72 | r := require.New(t) 73 | a := New(Options{}) 74 | rr := render.New(render.Options{}) 75 | 76 | a.GET("/", func(c Context) error { 77 | c.Flash().Add("something", "something to say!") 78 | return c.Render(http.StatusCreated, rr.String(keyTPL)) 79 | }) 80 | 81 | w := httptest.New(a) 82 | res := w.HTML("/").Get() 83 | r.Contains(res.Body.String(), "something to say!") 84 | } 85 | 86 | const keyTPL = `<%= for (k, v) in flash { %> 87 | Flash: 88 | <%= k %>:<%= v %> 89 | <% } %> 90 | ` 91 | 92 | func Test_FlashRenderCustomKey(t *testing.T) { 93 | r := require.New(t) 94 | a := New(Options{}) 95 | rr := render.New(render.Options{}) 96 | 97 | a.GET("/", func(c Context) error { 98 | c.Flash().Add("something", "something to say!") 99 | return c.Render(http.StatusCreated, rr.String(keyTPL)) 100 | }) 101 | 102 | w := httptest.New(a) 103 | res := w.HTML("/").Get() 104 | r.Contains(res.Body.String(), "something to say!") 105 | } 106 | 107 | func Test_FlashRenderCustomKeyNotDefined(t *testing.T) { 108 | r := require.New(t) 109 | a := New(Options{}) 110 | rr := render.New(render.Options{}) 111 | 112 | a.GET("/", func(c Context) error { 113 | return c.Render(http.StatusCreated, rr.String(customKeyTPL)) 114 | }) 115 | 116 | w := httptest.New(a) 117 | res := w.HTML("/").Get() 118 | r.NotContains(res.Body.String(), "something to say!") 119 | } 120 | 121 | const customKeyTPL = ` 122 | {{#each flash.other as |k value|}} 123 | {{value}} 124 | {{/each}} 125 | ` 126 | 127 | func Test_FlashNotClearedOnRedirect(t *testing.T) { 128 | r := require.New(t) 129 | a := New(Options{}) 130 | rr := render.New(render.Options{}) 131 | 132 | a.GET("/flash", func(c Context) error { 133 | c.Flash().Add("success", "Antonio, you're welcome!") 134 | return c.Redirect(http.StatusSeeOther, "/") 135 | }) 136 | 137 | a.GET("/", func(c Context) error { 138 | template := `Message: <%= flash["success"] %>` 139 | return c.Render(http.StatusCreated, rr.String(template)) 140 | }) 141 | 142 | w := httptest.New(a) 143 | res := w.HTML("/flash").Get() 144 | r.Equal(res.Code, http.StatusSeeOther) 145 | r.Equal(res.Location(), "/") 146 | 147 | res = w.HTML("/").Get() 148 | r.Contains(res.Body.String(), template.HTMLEscapeString("Antonio, you're welcome!")) 149 | 150 | } 151 | -------------------------------------------------------------------------------- /render/js_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/psanford/memfs" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const jsLayout = "layout.js" 13 | const jsAltLayout = "alt_layout.plush.js" 14 | const jsTemplate = "my-template.js" 15 | 16 | func Test_JavaScript_WithoutLayout(t *testing.T) { 17 | r := require.New(t) 18 | 19 | rootFS := memfs.New() 20 | r.NoError(rootFS.WriteFile(jsTemplate, []byte("alert(<%= name %>)"), 0644)) 21 | 22 | e := NewEngine() 23 | e.TemplatesFS = rootFS 24 | 25 | h := e.JavaScript(jsTemplate) 26 | r.Equal("application/javascript", h.ContentType()) 27 | bb := &bytes.Buffer{} 28 | 29 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 30 | r.Equal("alert(Mark)", strings.TrimSpace(bb.String())) 31 | } 32 | 33 | func Test_JavaScript_WithLayout(t *testing.T) { 34 | r := require.New(t) 35 | 36 | rootFS := memfs.New() 37 | r.NoError(rootFS.WriteFile(jsTemplate, []byte("alert(<%= name %>)"), 0644)) 38 | r.NoError(rootFS.WriteFile(jsLayout, []byte("$(<%= yield %>)"), 0644)) 39 | 40 | e := NewEngine() 41 | e.TemplatesFS = rootFS 42 | e.JavaScriptLayout = jsLayout 43 | 44 | h := e.JavaScript(jsTemplate) 45 | r.Equal("application/javascript", h.ContentType()) 46 | bb := &bytes.Buffer{} 47 | 48 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 49 | r.Equal("$(alert(Mark))", strings.TrimSpace(bb.String())) 50 | } 51 | 52 | func Test_JavaScript_WithLayout_Override(t *testing.T) { 53 | r := require.New(t) 54 | 55 | rootFS := memfs.New() 56 | r.NoError(rootFS.WriteFile(jsTemplate, []byte("alert(<%= name %>)"), 0644)) 57 | r.NoError(rootFS.WriteFile(jsLayout, []byte("$(<%= yield %>)"), 0644)) 58 | r.NoError(rootFS.WriteFile(jsAltLayout, []byte("_(<%= yield %>)"), 0644)) 59 | 60 | e := NewEngine() 61 | e.TemplatesFS = rootFS 62 | e.JavaScriptLayout = jsLayout 63 | 64 | h := e.JavaScript(jsTemplate, jsAltLayout) 65 | r.Equal("application/javascript", h.ContentType()) 66 | bb := &bytes.Buffer{} 67 | 68 | r.NoError(h.Render(bb, Data{"name": "Mark"})) 69 | r.Equal("_(alert(Mark))", strings.TrimSpace(bb.String())) 70 | } 71 | 72 | func Test_JavaScript_Partial_Without_Extension(t *testing.T) { 73 | const tmpl = "let a = 1;\n<%= partial(\"part\") %>" 74 | const part = "alert('Hi <%= name %>!');" 75 | 76 | r := require.New(t) 77 | 78 | rootFS := memfs.New() 79 | r.NoError(rootFS.WriteFile(jsTemplate, []byte(tmpl), 0644)) 80 | r.NoError(rootFS.WriteFile("_part.js", []byte(part), 0644)) 81 | 82 | e := NewEngine() 83 | e.TemplatesFS = rootFS 84 | h := e.JavaScript(jsTemplate) 85 | r.Equal("application/javascript", h.ContentType()) 86 | bb := &bytes.Buffer{} 87 | 88 | r.NoError(h.Render(bb, Data{"name": "Yonghwan"})) 89 | r.Equal("let a = 1;\nalert('Hi Yonghwan!');", bb.String()) 90 | } 91 | 92 | func Test_JavaScript_Partial(t *testing.T) { 93 | const tmpl = "let a = 1;\n<%= partial(\"part.js\") %>" 94 | const part = "alert('Hi <%= name %>!');" 95 | 96 | r := require.New(t) 97 | 98 | rootFS := memfs.New() 99 | r.NoError(rootFS.WriteFile(jsTemplate, []byte(tmpl), 0644)) 100 | r.NoError(rootFS.WriteFile("_part.js", []byte(part), 0644)) 101 | 102 | e := NewEngine() 103 | e.TemplatesFS = rootFS 104 | 105 | h := e.JavaScript(jsTemplate) 106 | r.Equal("application/javascript", h.ContentType()) 107 | bb := &bytes.Buffer{} 108 | 109 | r.NoError(h.Render(bb, Data{"name": "Yonghwan"})) 110 | r.Equal("let a = 1;\nalert('Hi Yonghwan!');", bb.String()) 111 | } 112 | 113 | func Test_JavaScript_HTML_Partial(t *testing.T) { 114 | const tmpl = "let a = \"<%= partial(\"part.html\") %>\"" 115 | const part = `
116 |

hi

117 |
` 118 | 119 | r := require.New(t) 120 | 121 | rootFS := memfs.New() 122 | r.NoError(rootFS.WriteFile(jsTemplate, []byte(tmpl), 0644)) 123 | r.NoError(rootFS.WriteFile("_part.html", []byte(part), 0644)) 124 | 125 | e := NewEngine() 126 | e.TemplatesFS = rootFS 127 | 128 | h := e.JavaScript(jsTemplate) 129 | r.Equal("application/javascript", h.ContentType()) 130 | bb := &bytes.Buffer{} 131 | 132 | r.NoError(h.Render(bb, Data{})) 133 | r.Contains(bb.String(), `id`) 134 | r.Contains(bb.String(), `foo`) 135 | 136 | // To check it has escaped the partial 137 | r.NotContains(bb.String(), `
`) 138 | } 139 | -------------------------------------------------------------------------------- /binding/file_test.go: -------------------------------------------------------------------------------- 1 | package binding_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "mime/multipart" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/gobuffalo/buffalo" 16 | "github.com/gobuffalo/buffalo/binding" 17 | "github.com/gobuffalo/buffalo/render" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | type WithFile struct { 22 | MyFile binding.File 23 | } 24 | 25 | type NamedFileSlice struct { 26 | MyFiles []binding.File `form:"thefiles"` 27 | } 28 | 29 | type NamedFile struct { 30 | MyFile binding.File `form:"afile"` 31 | } 32 | 33 | func App() *buffalo.App { 34 | a := buffalo.New(buffalo.Options{}) 35 | a.POST("/on-struct", func(c buffalo.Context) error { 36 | wf := &WithFile{} 37 | if err := c.Bind(wf); err != nil { 38 | return err 39 | } 40 | return c.Render(http.StatusCreated, render.String(wf.MyFile.Filename)) 41 | }) 42 | a.POST("/named-file", func(c buffalo.Context) error { 43 | wf := &NamedFile{} 44 | if err := c.Bind(wf); err != nil { 45 | return err 46 | } 47 | return c.Render(http.StatusCreated, render.String(wf.MyFile.Filename)) 48 | }) 49 | a.POST("/named-file-slice", func(c buffalo.Context) error { 50 | wmf := &NamedFileSlice{} 51 | if err := c.Bind(wmf); err != nil { 52 | return err 53 | } 54 | result := make([]string, len(wmf.MyFiles)) 55 | for i, f := range wmf.MyFiles { 56 | result[i] += fmt.Sprintf("%s", f.Filename) 57 | 58 | } 59 | return c.Render(http.StatusCreated, render.String(strings.Join(result, ","))) 60 | }) 61 | a.POST("/on-context", func(c buffalo.Context) error { 62 | f, err := c.File("MyFile") 63 | if err != nil { 64 | return err 65 | } 66 | return c.Render(http.StatusCreated, render.String(f.Filename)) 67 | }) 68 | 69 | return a 70 | } 71 | 72 | func Test_File_Upload_On_Struct(t *testing.T) { 73 | r := require.New(t) 74 | 75 | req, err := newFileUploadRequest("/on-struct", "MyFile", "file_test.go") 76 | r.NoError(err) 77 | res := httptest.NewRecorder() 78 | 79 | App().ServeHTTP(res, req) 80 | 81 | r.Equal(http.StatusCreated, res.Code) 82 | r.Equal("file_test.go", res.Body.String()) 83 | } 84 | 85 | func Test_File_Upload_On_Struct_WithTag_WithMultipleFiles(t *testing.T) { 86 | r := require.New(t) 87 | 88 | req, err := newFileUploadRequest("/named-file-slice", "thefiles", "file_test.go", "file.go", "types.go") 89 | r.NoError(err) 90 | res := httptest.NewRecorder() 91 | 92 | App().ServeHTTP(res, req) 93 | 94 | r.Equal(http.StatusCreated, res.Code) 95 | r.Equal("file_test.go,file.go,types.go", res.Body.String()) 96 | } 97 | 98 | func Test_File_Upload_On_Struct_WithTag(t *testing.T) { 99 | r := require.New(t) 100 | 101 | req, err := newFileUploadRequest("/named-file", "afile", "file_test.go") 102 | r.NoError(err) 103 | res := httptest.NewRecorder() 104 | 105 | App().ServeHTTP(res, req) 106 | 107 | r.Equal(http.StatusCreated, res.Code) 108 | r.Equal("file_test.go", res.Body.String()) 109 | } 110 | 111 | func Test_File_Upload_On_Context(t *testing.T) { 112 | r := require.New(t) 113 | 114 | req, err := newFileUploadRequest("/on-context", "MyFile", "file_test.go") 115 | r.NoError(err) 116 | res := httptest.NewRecorder() 117 | 118 | App().ServeHTTP(res, req) 119 | 120 | r.Equal(http.StatusCreated, res.Code) 121 | r.Equal("file_test.go", res.Body.String()) 122 | } 123 | 124 | // this helper method was inspired by this blog post by Matt Aimonetti: 125 | // https://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/ 126 | func newFileUploadRequest(uri string, paramName string, paths ...string) (*http.Request, error) { 127 | body := &bytes.Buffer{} 128 | writer := multipart.NewWriter(body) 129 | 130 | for _, path := range paths { 131 | file, err := os.Open(path) 132 | if err != nil { 133 | return nil, err 134 | } 135 | defer file.Close() 136 | part, err := writer.CreateFormFile(paramName, filepath.Base(path)) 137 | if err != nil { 138 | return nil, err 139 | } 140 | if _, err = io.Copy(part, file); err != nil { 141 | return nil, err 142 | } 143 | } 144 | 145 | if err := writer.Close(); err != nil { 146 | return nil, err 147 | } 148 | req, err := http.NewRequest("POST", uri, body) 149 | req.Header.Set("Content-Type", writer.FormDataContentType()) 150 | return req, err 151 | } 152 | -------------------------------------------------------------------------------- /plugins/plugcmds/available.go: -------------------------------------------------------------------------------- 1 | package plugcmds 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/gobuffalo/buffalo/plugins" 10 | "github.com/gobuffalo/events" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // NewAvailable returns a fully formed Available type 15 | func NewAvailable() *Available { 16 | return &Available{ 17 | plugs: plugMap{}, 18 | } 19 | } 20 | 21 | // Available used to manage all of the available commands 22 | // for the plugin 23 | type Available struct { 24 | plugs plugMap 25 | } 26 | 27 | type plug struct { 28 | BuffaloCommand string 29 | Cmd *cobra.Command 30 | Plugin plugins.Command 31 | } 32 | 33 | func (p plug) String() string { 34 | b, _ := json.Marshal(p.Plugin) 35 | return string(b) 36 | } 37 | 38 | // Cmd returns the "available" command 39 | func (a *Available) Cmd() *cobra.Command { 40 | return &cobra.Command{ 41 | Use: "available", 42 | Short: "a list of available buffalo plugins", 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | return a.Encode(os.Stdout) 45 | }, 46 | } 47 | } 48 | 49 | // Commands returns all of the commands that are available 50 | func (a *Available) Commands() []*cobra.Command { 51 | cmds := []*cobra.Command{a.Cmd()} 52 | a.plugs.Range(func(_ string, p plug) bool { 53 | cmds = append(cmds, p.Cmd) 54 | return true 55 | }) 56 | return cmds 57 | } 58 | 59 | // Add a new command to this list of available ones. 60 | // The bufCmd should corresponding buffalo command that 61 | // command should live below. 62 | // 63 | // Special "commands": 64 | // "root" - is the `buffalo` command 65 | // "events" - listens for emitted events 66 | func (a *Available) Add(bufCmd string, cmd *cobra.Command) error { 67 | if len(cmd.Aliases) == 0 { 68 | cmd.Aliases = []string{} 69 | } 70 | p := plug{ 71 | BuffaloCommand: bufCmd, 72 | Cmd: cmd, 73 | Plugin: plugins.Command{ 74 | Name: cmd.Use, 75 | BuffaloCommand: bufCmd, 76 | Description: cmd.Short, 77 | Aliases: cmd.Aliases, 78 | UseCommand: cmd.Use, 79 | }, 80 | } 81 | a.plugs.Store(p.String(), p) 82 | return nil 83 | } 84 | 85 | // Mount all of the commands that are available 86 | // on to the other command. This is the recommended 87 | // approach for using Available. 88 | // a.Mount(rootCmd) 89 | func (a *Available) Mount(cmd *cobra.Command) { 90 | // mount all the cmds on to the cobra command 91 | cmd.AddCommand(a.Cmd()) 92 | a.plugs.Range(func(_ string, p plug) bool { 93 | cmd.AddCommand(p.Cmd) 94 | return true 95 | }) 96 | } 97 | 98 | // Encode into the required Buffalo plugins available 99 | // format 100 | func (a *Available) Encode(w io.Writer) error { 101 | var plugs plugins.Commands 102 | a.plugs.Range(func(_ string, p plug) bool { 103 | plugs = append(plugs, p.Plugin) 104 | return true 105 | }) 106 | return json.NewEncoder(w).Encode(plugs) 107 | } 108 | 109 | // Listen adds a command for github.com/gobuffalo/events. 110 | // This will listen for ALL events. Use ListenFor to 111 | // listen to a regex of events. 112 | func (a *Available) Listen(fn func(e events.Event) error) error { 113 | return a.Add("events", buildListen(fn)) 114 | } 115 | 116 | // ListenFor adds a command for github.com/gobuffalo/events. 117 | // This will only listen for events that match the regex provided. 118 | func (a *Available) ListenFor(rx string, fn func(e events.Event) error) error { 119 | cmd := buildListen(fn) 120 | p := plug{ 121 | BuffaloCommand: "events", 122 | Cmd: cmd, 123 | Plugin: plugins.Command{ 124 | Name: cmd.Use, 125 | BuffaloCommand: "events", 126 | Description: cmd.Short, 127 | Aliases: cmd.Aliases, 128 | UseCommand: cmd.Use, 129 | ListenFor: rx, 130 | }, 131 | } 132 | a.plugs.Store(p.String(), p) 133 | return nil 134 | } 135 | 136 | func buildListen(fn func(e events.Event) error) *cobra.Command { 137 | listenCmd := &cobra.Command{ 138 | Use: "listen", 139 | Short: "listens to github.com/gobuffalo/events", 140 | Aliases: []string{}, 141 | RunE: func(cmd *cobra.Command, args []string) error { 142 | if len(args) == 0 { 143 | return fmt.Errorf("must pass a payload") 144 | } 145 | 146 | e := events.Event{} 147 | err := json.Unmarshal([]byte(args[0]), &e) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return fn(e) 153 | }, 154 | } 155 | return listenCmd 156 | } 157 | --------------------------------------------------------------------------------