mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-09-04 10:06:24 -07:00
Replace internal self-generated captcha tests with hcaptcha
This commit is contained in:
parent
c180fec5eb
commit
89457e47f7
9 changed files with 153 additions and 155 deletions
1
go.mod
1
go.mod
|
@ -9,7 +9,6 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/tdewolff/minify v2.3.6+incompatible
|
||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -11,8 +11,6 @@ github.com/frustra/bbcode v0.0.0-20201127003707-6ef347fbe1c8/go.mod h1:0QBxkXxN+
|
|||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
|
@ -21,8 +19,6 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
|
|||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
|
||||
github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
|
@ -40,7 +36,6 @@ gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/9
|
|||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
|
|
@ -154,6 +154,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
defer boardPageFile.Close()
|
||||
// Render board page template to the file,
|
||||
// packaging the board/section list, threads, and board info
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
|
@ -163,6 +164,8 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
"currentPage": 1,
|
||||
"board": board,
|
||||
"board_config": boardConfig,
|
||||
"useCaptcha": captchaCfg.UseCaptcha(),
|
||||
"captcha": captchaCfg,
|
||||
}, boardPageFile, "text/html"); err != nil {
|
||||
errEv.Err(err).
|
||||
Str("page", "board.html").
|
||||
|
@ -204,6 +207,7 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
defer currentPageFile.Close()
|
||||
|
||||
// Render the boardpage template
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
|
@ -213,6 +217,8 @@ func BuildBoardPages(board *gcsql.Board) error {
|
|||
"currentPage": catalog.currentPage,
|
||||
"board": board,
|
||||
"board_config": boardCfg,
|
||||
"useCaptcha": captchaCfg.UseCaptcha(),
|
||||
"captcha": captchaCfg,
|
||||
}, currentPageFile, "text/html"); err != nil {
|
||||
errEv.Err(err).
|
||||
Caller().Send()
|
||||
|
|
|
@ -91,6 +91,7 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
}
|
||||
|
||||
// render thread page
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
|
||||
"webroot": criticalCfg.WebRoot,
|
||||
"boards": gcsql.AllBoards,
|
||||
|
@ -99,6 +100,8 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
"sections": gcsql.AllSections,
|
||||
"posts": posts[1:],
|
||||
"op": posts[0],
|
||||
"useCaptcha": captchaCfg.UseCaptcha(),
|
||||
"captcha": captchaCfg,
|
||||
}, threadPageFile, "text/html"); err != nil {
|
||||
errEv.Err(err).
|
||||
Caller().Send()
|
||||
|
|
|
@ -35,10 +35,7 @@ var (
|
|||
"MaxLogDays": 14,
|
||||
|
||||
// BoardConfig
|
||||
"DateTimeFormat": "Mon, January 02, 2006 3:04:05 PM",
|
||||
"CaptchaWidth": 240,
|
||||
"CaptchaHeight": 80,
|
||||
"CaptchaMinutesTimeout": 15,
|
||||
"DateTimeFormat": "Mon, January 02, 2006 3:04:05 PM",
|
||||
|
||||
// PostConfig
|
||||
"NewThreadDelay": 30,
|
||||
|
@ -219,14 +216,7 @@ func (gcfg *GochanConfig) ValidateValues() error {
|
|||
gcfg.DateTimeFormat = defaults["DateTimeFormat"].(string)
|
||||
changed = true
|
||||
}
|
||||
if gcfg.CaptchaWidth == 0 {
|
||||
gcfg.CaptchaWidth = defaults["CaptchaWidth"].(int)
|
||||
changed = true
|
||||
}
|
||||
if gcfg.CaptchaHeight == 0 {
|
||||
gcfg.CaptchaHeight = defaults["CaptchaHeight"].(int)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if gcfg.EnableGeoIP {
|
||||
if gcfg.GeoIPDBlocation == "" {
|
||||
return &ErrInvalidValue{Field: "GeoIPDBlocation", Value: "", Details: "GeoIPDBlocation must be set in gochan.json if EnableGeoIP is true"}
|
||||
|
@ -314,6 +304,18 @@ type SiteConfig struct {
|
|||
MinifyJS bool `description:"If checked, gochan will minify js and json files when building"`
|
||||
GeoIPDBlocation string `description:"Specifies the location of the GeoIP database file. If you're using CloudFlare, you can set it to cf to rely on CloudFlare for GeoIP information."`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
|
||||
Captcha CaptchaConfig
|
||||
}
|
||||
|
||||
type CaptchaConfig struct {
|
||||
Type string // may or may not be used, possibly for specifying which service (e.g. "hcaptcha","recaptcha")
|
||||
SiteKey string
|
||||
AccountSecret string
|
||||
}
|
||||
|
||||
func (cc *CaptchaConfig) UseCaptcha() bool {
|
||||
return cc.SiteKey != "" && cc.AccountSecret != ""
|
||||
}
|
||||
|
||||
type BoardCooldowns struct {
|
||||
|
@ -336,10 +338,6 @@ type BoardConfig struct {
|
|||
|
||||
DateTimeFormat string `description:"The format used for dates. See <a href=\"https://golang.org/pkg/time/#Time.Format\">here</a> for more info."`
|
||||
AkismetAPIKey string `description:"The API key to be sent to Akismet for post spam checking. If the key is invalid, Akismet won't be used."`
|
||||
UseCaptcha bool
|
||||
CaptchaWidth int
|
||||
CaptchaHeight int
|
||||
CaptchaMinutesTimeout int
|
||||
MaxBoardPages int
|
||||
ShowPosterID bool
|
||||
EnableSpoileredImages bool
|
||||
|
|
|
@ -1,134 +1,108 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gctemplates"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
var (
|
||||
captchaString *base64Captcha.DriverString
|
||||
driver *base64Captcha.DriverString
|
||||
ErrNoCaptchaToken = errors.New("missing required CAPTCHA")
|
||||
ErrUnsupportedCaptcha = errors.New("unsupported captcha type set in configuration (currently only hcaptcha is supported)")
|
||||
validCaptchaTypes = []string{"hcaptcha"}
|
||||
)
|
||||
|
||||
type captchaJSON struct {
|
||||
CaptchaID string `json:"id"`
|
||||
Base64String string `json:"image"`
|
||||
Result string `json:"-"`
|
||||
TempPostIndex string `json:"-"`
|
||||
EmailCmd string `json:"-"`
|
||||
type CaptchaResult struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Credit bool `json:"credit"`
|
||||
Success bool `json:"success"`
|
||||
Timestamp time.Time `json:"challenge_ts"`
|
||||
}
|
||||
|
||||
// InitCaptcha prepares the captcha driver for use
|
||||
func InitCaptcha() {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
var typeIsValid bool
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if !captchaCfg.UseCaptcha() {
|
||||
return
|
||||
}
|
||||
driver = base64Captcha.NewDriverString(
|
||||
boardConfig.CaptchaHeight, boardConfig.CaptchaWidth, int(0), int(0), int(6),
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
&color.RGBA{0, 0, 0, 0}, nil, nil).ConvertFonts()
|
||||
}
|
||||
|
||||
// ServeCaptcha handles requests to /captcha if UseCaptcha is enabled in gochan.json
|
||||
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if err = request.ParseForm(); err != nil {
|
||||
gcutil.LogError(err).Msg("Failed parsing request form")
|
||||
serverutil.ServeErrorPage(writer, "Error parsing request form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tempPostIndexStr := request.FormValue("temppostindex")
|
||||
var tempPostIndex int
|
||||
if tempPostIndex, err = strconv.Atoi(tempPostIndexStr); err != nil {
|
||||
tempPostIndexStr = "-1"
|
||||
tempPostIndex = 0
|
||||
}
|
||||
emailCommand := request.FormValue("emailcmd")
|
||||
|
||||
id, b64 := getCaptchaImage()
|
||||
captchaStruct := captchaJSON{id, b64, "", tempPostIndexStr, emailCommand}
|
||||
useJSON := request.FormValue("json") == "1"
|
||||
if useJSON {
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
|
||||
str, _ := gcutil.MarshalJSON(captchaStruct, false)
|
||||
serverutil.MinifyWriter(writer, []byte(str), "application/json")
|
||||
return
|
||||
}
|
||||
if request.FormValue("reload") == "Reload" {
|
||||
request.Form.Del("reload")
|
||||
request.Form.Add("didreload", "1")
|
||||
ServeCaptcha(writer, request)
|
||||
return
|
||||
}
|
||||
writer.Header().Add("Content-Type", "text/html")
|
||||
captchaID := request.FormValue("captchaid")
|
||||
boardIDstr := request.FormValue("boardid")
|
||||
boardID, err := strconv.Atoi(boardIDstr)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("ip", gcutil.GetRealIP(request)).
|
||||
Str("boardid", boardIDstr).Send()
|
||||
serverutil.ServeError(writer, fmt.Sprintf("Invalid boardid value %q", boardIDstr),
|
||||
useJSON, map[string]interface{}{
|
||||
"boardid": boardIDstr,
|
||||
})
|
||||
serverutil.ServeErrorPage(writer, fmt.Sprintf("Invalid boardid value %q", boardIDstr))
|
||||
return
|
||||
}
|
||||
captchaAnswer := request.FormValue("captchaanswer")
|
||||
if captchaID != "" && request.FormValue("didreload") != "1" {
|
||||
goodAnswer := base64Captcha.DefaultMemStore.Verify(captchaID, captchaAnswer, true)
|
||||
if goodAnswer {
|
||||
if tempPostIndex > -1 && tempPostIndex < len(gcsql.TempPosts) {
|
||||
// came from a /post redirect, insert the specified temporary post
|
||||
// and redirect to the thread
|
||||
gcsql.TempPosts[tempPostIndex].Insert(emailCommand != "sage", boardID, false, false, false, false)
|
||||
building.BuildBoards(false, boardID)
|
||||
building.BuildFrontPage()
|
||||
|
||||
url := gcsql.TempPosts[tempPostIndex].WebPath()
|
||||
|
||||
// move the end Post to the current index and remove the old end Post. We don't
|
||||
// really care about order as long as tempPost validation doesn't get jumbled up
|
||||
gcsql.TempPosts[tempPostIndex] = gcsql.TempPosts[len(gcsql.TempPosts)-1]
|
||||
gcsql.TempPosts = gcsql.TempPosts[:len(gcsql.TempPosts)-1]
|
||||
http.Redirect(writer, request, url, http.StatusFound)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
captchaStruct.Result = "Incorrect CAPTCHA"
|
||||
for _, vType := range validCaptchaTypes {
|
||||
if captchaCfg.Type == vType {
|
||||
typeIsValid = true
|
||||
}
|
||||
}
|
||||
if err = serverutil.MinifyTemplate(gctemplates.Captcha, captchaStruct, writer, "text/html"); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("template", "captcha").Send()
|
||||
fmt.Fprint(writer, "Error executing captcha template: ", err.Error())
|
||||
if !typeIsValid {
|
||||
fmt.Printf("Unrecognized Captcha.Type value in configuration: %q, valid values: %v\n",
|
||||
captchaCfg.Type, validCaptchaTypes)
|
||||
gcutil.LogFatal().
|
||||
Str("captchaType", captchaCfg.Type).
|
||||
Msg("Unsupported captcha type set in configuration")
|
||||
}
|
||||
}
|
||||
|
||||
func getCaptchaImage() (captchaID, chaptchaB64 string) {
|
||||
boardConfig := config.GetBoardConfig("")
|
||||
if !boardConfig.UseCaptcha {
|
||||
// SubmitCaptchaResponse parses the incoming captcha form values, submits them, and returns the results
|
||||
func SubmitCaptchaResponse(request *http.Request) (bool, error) {
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if !captchaCfg.UseCaptcha() {
|
||||
return true, nil // captcha isn't required, skip the test
|
||||
}
|
||||
var token string
|
||||
switch captchaCfg.Type {
|
||||
case "hcaptcha":
|
||||
token = request.PostFormValue("h-captcha-response")
|
||||
default:
|
||||
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return false, ErrNoCaptchaToken
|
||||
}
|
||||
params := url.Values{
|
||||
"secret": []string{captchaCfg.AccountSecret},
|
||||
"response": []string{token},
|
||||
}
|
||||
resp, err := http.PostForm("https://hcaptcha.com/siteverify", params)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var vals CaptchaResult
|
||||
if err = json.NewDecoder(resp.Body).Decode(&vals); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return vals.Success, nil
|
||||
}
|
||||
|
||||
// ServeCaptcha handles requests to /captcha if the captcha is properly configured
|
||||
func ServeCaptcha(writer http.ResponseWriter, request *http.Request) {
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if request.Method == "GET" && request.FormValue("needcaptcha") != "" {
|
||||
fmt.Fprint(writer, captchaCfg.UseCaptcha())
|
||||
return
|
||||
}
|
||||
captcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
|
||||
captchaID, chaptchaB64, _ = captcha.Generate()
|
||||
return
|
||||
if !captchaCfg.UseCaptcha() {
|
||||
serverutil.ServeErrorPage(writer, "This site is not set up to require a CAPTCHA test")
|
||||
return
|
||||
}
|
||||
if request.Method == "POST" {
|
||||
result, err := SubmitCaptchaResponse(request)
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Error checking results: "+err.Error())
|
||||
}
|
||||
fmt.Println("Success:", result)
|
||||
}
|
||||
err := serverutil.MinifyTemplate(gctemplates.Captcha, map[string]interface{}{
|
||||
"webroot": config.GetSystemCriticalConfig().WebRoot,
|
||||
"siteKey": captchaCfg.SiteKey,
|
||||
}, writer, "text/html")
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Error serving CAPTCHA: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,6 +212,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
delay, err = gcsql.SinceLastThread(post.IP)
|
||||
tooSoon = delay < boardConfig.Cooldowns.NewThread
|
||||
} else {
|
||||
// replying to a thread
|
||||
delay, err = gcsql.SinceLastPost(post.IP)
|
||||
tooSoon = delay < boardConfig.Cooldowns.Reply
|
||||
}
|
||||
|
@ -236,24 +237,16 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
|
||||
post.Sanitize()
|
||||
|
||||
if boardConfig.UseCaptcha {
|
||||
captchaID := request.FormValue("captchaid")
|
||||
captchaAnswer := request.FormValue("captchaanswer")
|
||||
if captchaID == "" && captchaAnswer == "" {
|
||||
// browser isn't using JS, save post data to tempPosts and show captcha
|
||||
request.Form.Add("temppostindex", strconv.Itoa(len(gcsql.TempPosts)))
|
||||
request.Form.Add("emailcmd", emailCommand)
|
||||
gcsql.TempPosts = append(gcsql.TempPosts, post)
|
||||
|
||||
ServeCaptcha(writer, request)
|
||||
return
|
||||
}
|
||||
captchaSuccess, err := SubmitCaptchaResponse(request)
|
||||
if err != nil {
|
||||
serverutil.ServeErrorPage(writer, "Error submitting captcha response:"+err.Error())
|
||||
errEv.Err(err).
|
||||
Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
boardExists := gcsql.DoesBoardExistByID(boardID)
|
||||
if !boardExists {
|
||||
serverutil.ServeErrorPage(writer, "Board does not exist (invalid boardid)")
|
||||
if !captchaSuccess {
|
||||
serverutil.ServeErrorPage(writer, "Missing or invalid captcha response")
|
||||
errEv.Msg("Missing or invalid captcha response")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Gochan CAPTCHA</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{with .board -}}
|
||||
{{with $.op -}}
|
||||
<title>{{$.op.TitleText}}</title>
|
||||
{{- else}}
|
||||
<title>/{{$.board.Dir}}/ - {{$.board.Title}}</title>
|
||||
{{end}}
|
||||
{{- else}}<title>{{with $.page_title}}{{$.page_title}} - {{end}}{{.site_config.SiteName}}</title>{{end}}
|
||||
<link rel="stylesheet" href="{{.webroot}}css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="{{.webroot}}css/{{.board_config.DefaultStyle}}" />
|
||||
<link rel="shortcut icon" href="{{.webroot}}favicon.png">
|
||||
<script type="text/javascript" src="{{.webroot}}js/consts.js"></script>
|
||||
<script type="text/javascript" src="{{.webroot}}js/gochan.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{{with .Result}}{{$.Result}}{{end}}
|
||||
<form action="/captcha" method="POST">
|
||||
<img src="{{.Base64String}}" /><br />
|
||||
<input type="text" name="captchaanswer" autocomplete="off" />
|
||||
<input type="hidden" name="captchaid" value="{{.CaptchaID}}" />
|
||||
{{with .EmailCmd}}<input type="hidden" name="emailcmd" value="{{$.EmailCmd}}" />{{end}}
|
||||
{{with .TempPostIndex}}<input type="hidden" name="temppostindex" value="{{$.TempPostIndex}}" />{{end}}
|
||||
<input type="submit" value="Submit" /><br />
|
||||
<input type="submit" name="reload" value="Reload" />
|
||||
<div id="topbar">
|
||||
<a href="{{$.webroot}}" class="topbar-item">home</a>
|
||||
{{range $i, $board := .boards}}<a href="{{$.webroot}}{{$board.Dir}}/" class="topbar-item" title="{{$board.Title}}">/{{$board.Dir}}/</a>{{end}}
|
||||
</div>
|
||||
{{with $.page_title }}<header>
|
||||
<h1 id="board-title">{{$.page_title}}</h1>
|
||||
{{with $.include_dashboard_link}}<a href="{{$.webroot}}manage" class="board-subtitle">Return to dashboard</a><br/>{{end}}
|
||||
</header>{{end}}
|
||||
<div id="content">
|
||||
<header>
|
||||
<h1 id="board-title">hCAPTCHA test</h1>
|
||||
</header><br />
|
||||
<form method="POST" action="{{.webroot}}captcha">
|
||||
<div class="h-captcha" data-sitekey="{{.siteKey}}"></div>
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
<input type="submit" value="Post">
|
||||
</form>
|
||||
<div id="footer">
|
||||
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan {{version}}</a><br />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -17,6 +17,12 @@
|
|||
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="postmsg" id="postmsg"></textarea></td></tr>
|
||||
<tr><th class="postblock">File</th><td><input name="imagefile" type="file" accept="image/jpeg,image/png,image/gif,video/webm,video/mp4"><input type="checkbox" id="spoiler" name="spoiler"/><label for="spoiler">Spoiler</label></td></tr>
|
||||
<tr><th class="postblock">Password</th><td><input type="password" id="postpassword" name="postpassword" size="14" /> (for post/file deletion)</td></tr>
|
||||
{{if .useCaptcha -}}
|
||||
<tr><th class="postblock">CAPTCHA</th><td>
|
||||
<div class="h-captcha" data-sitekey="{{.captcha.SiteKey}}"></div>
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
</td></tr>
|
||||
{{- end}}
|
||||
</table><input type="password" name="dummy2" style="display:none"/>
|
||||
</form>
|
||||
</div>{{end}}
|
Loading…
Add table
Add a link
Reference in a new issue