wip allow for more cdns
add res template function add ExtraCSPOrigins config setting add StaticResBase config setting skip flush directives
This commit is contained in:
parent
d4fd85f75c
commit
43d72e6f3b
@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -23,14 +24,16 @@ import (
|
||||
//type SFileList map[string]*SFile
|
||||
//type SFileListShort map[string]*SFile
|
||||
|
||||
var StaticFiles = SFileList{make(map[string]*SFile),make(map[string]*SFile)}
|
||||
var StaticFiles = SFileList{"/s/", make(map[string]*SFile), make(map[string]*SFile)}
|
||||
|
||||
//var StaticFilesShort SFileList = make(map[string]*SFile)
|
||||
var staticFileMutex sync.RWMutex
|
||||
|
||||
// ? Is it efficient to have two maps for this?
|
||||
type SFileList struct {
|
||||
Long map[string]*SFile
|
||||
Short map[string]*SFile
|
||||
Prefix string
|
||||
Long map[string]*SFile
|
||||
Short map[string]*SFile
|
||||
}
|
||||
|
||||
type SFile struct {
|
||||
@ -305,7 +308,7 @@ func (l SFileList) JSTmplInit() error {
|
||||
hasher.Write(data)
|
||||
checksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file.", path)
|
||||
return nil
|
||||
@ -367,7 +370,7 @@ func (l SFileList) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file.", path)
|
||||
return nil
|
||||
@ -424,7 +427,7 @@ func (l SFileList) Add(path, prefix string) error {
|
||||
hasher.Write(data)
|
||||
checksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLogf("Added the '%s' static file", path)
|
||||
return nil
|
||||
@ -448,8 +451,13 @@ func (l SFileList) GetShort(name string) (file *SFile, exists bool) {
|
||||
func (l SFileList) Set(name string, data *SFile) {
|
||||
staticFileMutex.Lock()
|
||||
defer staticFileMutex.Unlock()
|
||||
l.Long[name] = data
|
||||
l.Short[strings.TrimPrefix("/s/",name)] = data
|
||||
// TODO: Propagate errors back up
|
||||
uurl, err := url.Parse(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
l.Long[uurl.Path] = data
|
||||
l.Short[strings.TrimPrefix(strings.TrimPrefix(name, l.Prefix), "/")] = data
|
||||
}
|
||||
|
||||
var gzipBestCompress sync.Pool
|
||||
|
@ -2,11 +2,13 @@ package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Site holds the basic settings which should be tweaked when setting up a site, we might move them to the settings table at some point
|
||||
@ -119,6 +121,9 @@ type config struct {
|
||||
RefNoRef bool
|
||||
NoEmbed bool
|
||||
|
||||
ExtraCSPOrigins string
|
||||
StaticResBase string // /s/
|
||||
|
||||
Noavatar string // ? - Move this into the settings table?
|
||||
ItemsPerPage int // ? - Move this into the settings table?
|
||||
MaxTopicTitleLength int
|
||||
@ -238,6 +243,21 @@ func ProcessConfig() (err error) {
|
||||
}
|
||||
Site.MaxRequestSize = Config.MaxRequestSize
|
||||
|
||||
local := func(h string) bool {
|
||||
return h == "localhost" || h == "127.0.0.1" || h == "::1" || h == Site.URL
|
||||
}
|
||||
uurl, err := url.Parse(Config.StaticResBase)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse Config.StaticResBase: ")
|
||||
}
|
||||
host := uurl.Hostname()
|
||||
if !local(host) {
|
||||
Config.ExtraCSPOrigins += " " + host
|
||||
}
|
||||
if Config.StaticResBase != "" {
|
||||
StaticFiles.Prefix = Config.StaticResBase
|
||||
}
|
||||
|
||||
if Config.PostIPCutoff == 0 {
|
||||
Config.PostIPCutoff = 120 // Default cutoff
|
||||
}
|
||||
|
@ -878,6 +878,17 @@ func initDefaultTmplFuncMap() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmap["res"] = func(nameInt interface{}) interface{} {
|
||||
n := nameInt.(string)
|
||||
if n[0] == '/' && n[1] == '/' {
|
||||
} else {
|
||||
if f, ok := StaticFiles.GetShort(n); ok {
|
||||
n = f.OName
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
DefaultTemplateFuncMap = fmap
|
||||
}
|
||||
|
||||
|
@ -119,6 +119,7 @@ func NewCTemplateSet(in string) *CTemplateSet {
|
||||
"js": true,
|
||||
"index": true,
|
||||
"flush": true,
|
||||
"res": true,
|
||||
},
|
||||
logger: log.New(f, "", log.LstdFlags),
|
||||
loggerf: f,
|
||||
@ -683,8 +684,9 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
|
||||
for _, cmd := range node.Pipe.Cmds {
|
||||
c.detail("If Node Bit:", cmd)
|
||||
c.detail("Bit Type:", reflect.ValueOf(cmd).Type().Name())
|
||||
expr += c.compileExprSwitch(con, cmd)
|
||||
c.detail("Expression Step:", c.compileExprSwitch(con, cmd))
|
||||
exprStep := c.compileExprSwitch(con, cmd)
|
||||
expr += exprStep
|
||||
c.detail("Expression Step:", exprStep)
|
||||
}
|
||||
|
||||
c.detail("Expression:", expr)
|
||||
@ -1031,8 +1033,7 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode)
|
||||
case *parse.NilNode:
|
||||
panic("Nil is not a command x.x")
|
||||
case *parse.PipeNode:
|
||||
c.detail("Pipe Node!")
|
||||
c.detail(n)
|
||||
c.detail("Pipe Node:", n)
|
||||
c.detail("Node Args:", node.Args)
|
||||
out += c.compileIdentSwitchN(con, node)
|
||||
default:
|
||||
@ -1043,9 +1044,9 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode)
|
||||
}
|
||||
|
||||
func (c *CTemplateSet) unknownNode(n parse.Node) {
|
||||
elem := reflect.ValueOf(n).Elem()
|
||||
c.logger.Println("Unknown Kind:", elem.Kind())
|
||||
c.logger.Println("Unknown Type:", elem.Type().Name())
|
||||
el := reflect.ValueOf(n).Elem()
|
||||
c.logger.Println("Unknown Kind:", el.Kind())
|
||||
c.logger.Println("Unknown Type:", el.Type().Name())
|
||||
panic("I don't know what node this is! Grr...")
|
||||
}
|
||||
|
||||
@ -1114,7 +1115,7 @@ func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNod
|
||||
return pos, out
|
||||
}
|
||||
|
||||
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal, notident bool) {
|
||||
func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal, notIdent bool) {
|
||||
c.dumpCall("compileIdentSwitch", con, node)
|
||||
litString := func(inner string, bytes bool) {
|
||||
if !bytes {
|
||||
@ -1135,9 +1136,16 @@ ArgLoop:
|
||||
var rout string
|
||||
pos, rout = c.compareJoin(con, pos, node, c.funcMap[id.String()].(string)) // TODO: Test this
|
||||
out += rout
|
||||
case "le", "lt", "gt", "ge", "eq", "ne":
|
||||
case "le", "lt", "gt", "ge":
|
||||
out += c.compareFunc(con, pos, node, c.funcMap[id.String()].(string))
|
||||
break ArgLoop
|
||||
case "eq", "ne":
|
||||
o := c.compareFunc(con, pos, node, c.funcMap[id.String()].(string))
|
||||
if out == "!" {
|
||||
o = "(" + o + ")"
|
||||
}
|
||||
out += o
|
||||
break ArgLoop
|
||||
case "add", "subtract", "divide", "multiply":
|
||||
rout, rval := c.simpleMath(con, pos, node, c.funcMap[id.String()].(string))
|
||||
out += rout
|
||||
@ -1236,7 +1244,7 @@ ArgLoop:
|
||||
// ! Slightly crude but it does the job
|
||||
leftParam := strings.Replace(leftOp, "\"", "", -1)
|
||||
c.langIndexToName = append(c.langIndexToName, leftParam)
|
||||
notident = true
|
||||
notIdent = true
|
||||
con.PushPhrase(len(c.langIndexToName) - 1)
|
||||
} else {
|
||||
leftParam := leftOp
|
||||
@ -1386,17 +1394,37 @@ ArgLoop:
|
||||
|
||||
// TODO: Refactor this
|
||||
// TODO: Call the template function directly rather than going through RunThemeTemplate to eliminate a round of indirection?
|
||||
out = "{\nerr := " + headParam + ".Theme.RunTmpl(" + nameParam + "," + pageParam + ",w)\n"
|
||||
out += "if err != nil {\nreturn err\n}\n}\n"
|
||||
out = "{\ne := " + headParam + ".Theme.RunTmpl(" + nameParam + "," + pageParam + ",w)\n"
|
||||
out += "if e != nil {\nreturn e\n}\n}\n"
|
||||
literal = true
|
||||
break ArgLoop
|
||||
case "flush":
|
||||
if c.lang == "js" {
|
||||
continue
|
||||
}
|
||||
out = "if fl, ok := iw.(http.Flusher); ok {\nfl.Flush()\n}\n"
|
||||
literal = true
|
||||
c.importMap["net/http"] = "net/http"
|
||||
break ArgLoop
|
||||
/*if c.lang == "js" {
|
||||
continue
|
||||
}
|
||||
out = "if fl, ok := iw.(http.Flusher); ok {\nfl.Flush()\n}\n"
|
||||
literal = true
|
||||
c.importMap["net/http"] = "net/http"
|
||||
break ArgLoop*/
|
||||
// TODO: Test this
|
||||
case "res":
|
||||
leftOp := node.Args[pos+1].String()
|
||||
if len(leftOp) == 0 {
|
||||
panic("The leftoperand for function res cannot be left blank")
|
||||
}
|
||||
leftParam, _ := c.compileIfVarSub(con, leftOp)
|
||||
literal = true
|
||||
if leftParam[0] == '"' {
|
||||
if leftParam[1] == '/' && leftParam[2] == '/' {
|
||||
litString(leftParam, false)
|
||||
break ArgLoop
|
||||
}
|
||||
out = "{n := " + leftParam + "\nif f, ok := c.StaticFiles.GetShort(n); ok {\nw.Write(StringToBytes(f.OName))\n} else {\nw.Write(StringToBytes(n))\n}}\n"
|
||||
break ArgLoop
|
||||
}
|
||||
out = "{n := " + leftParam + "\nif n[0] == '/' && n[1] == '/' {\n} else {\nif f, ok := c.StaticFiles.GetShort(n); ok {\nn = f.OName\n}\nw.Write(StringToBytes(n))\n}\n"
|
||||
break ArgLoop
|
||||
default:
|
||||
c.detail("Variable!")
|
||||
@ -1410,7 +1438,7 @@ ArgLoop:
|
||||
}
|
||||
}
|
||||
c.retCall("compileIdentSwitch", out, val, literal)
|
||||
return out, val, literal, notident
|
||||
return out, val, literal, notIdent
|
||||
}
|
||||
|
||||
func (c *CTemplateSet) compileReflectSwitch(con CContext, node *parse.CommandNode) (out string, outVal reflect.Value) {
|
||||
|
@ -284,7 +284,7 @@ func (t *Theme) AddThemeStaticFiles() error {
|
||||
hasher.Write(data)
|
||||
checksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
StaticFiles.Set("/s/"+t.Name+path, &SFile{data, gzipData, brData, checksum, t.Name + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
StaticFiles.Set(StaticFiles.Prefix+t.Name+path, &SFile{data, gzipData, brData, checksum, StaticFiles.Prefix + t.Name + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)})
|
||||
|
||||
DebugLog("Added the '/" + t.Name + path + "' static file for theme " + t.Name + ".")
|
||||
return nil
|
||||
|
@ -126,6 +126,10 @@ RefNoRef - This switch makes it so that if a user clicks on a link, then the inc
|
||||
|
||||
NoEmbed - Don't expand links into videos or images. Default: false
|
||||
|
||||
ExtraCSPOrigins - Extra origins which may want whitelisted in the default Content Security Policy.
|
||||
|
||||
StaticResBase - The default prefix for static resource files. May be a path or an external domain like a CDN domain. Default: /s/
|
||||
|
||||
NoAvatar - The default avatar to use for users when they don't have their own. The default for this may change in the near future to better utilise HTTP/2. Example: https://api.adorable.io/avatars/{width}/{id}.png
|
||||
|
||||
ItemsPerPage - The number of posts, topics, etc. you want on each page.
|
||||
|
@ -139,15 +139,15 @@ func FootHeaders(w http.ResponseWriter, h *c.Header) {
|
||||
he := w.Header()
|
||||
if c.Config.SslSchema {
|
||||
if h.ExternalMedia {
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp;upgrade-insecure-requests")
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp;upgrade-insecure-requests")
|
||||
} else {
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self';upgrade-insecure-requests")
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self';upgrade-insecure-requests")
|
||||
}
|
||||
} else {
|
||||
if h.ExternalMedia {
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp")
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp")
|
||||
} else {
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self'")
|
||||
he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,16 @@
|
||||
<head>
|
||||
<title>{{.Title}} | {{.Header.Site.Name}}</title>
|
||||
{{range .Header.Stylesheets}}
|
||||
<link href="/s/{{.}}"rel="stylesheet"type="text/css">{{end}}
|
||||
<link href="{{.}}"rel="stylesheet"type="text/css">{{end}}
|
||||
{{range .Header.PreScriptsAsync}}
|
||||
<script async src="/s/{{.}}"></script>{{end}}
|
||||
<script async src="{{.}}"></script>{{end}}
|
||||
{{if .CurrentUser.Loggedin}}<meta property="x-mem"content="1">{{end}}
|
||||
<script src="/s/init.js?i=12"></script>
|
||||
<script src="{{res "init.js"}}"></script>
|
||||
{{range .Header.ScriptsAsync}}
|
||||
<script async src="/s/{{.}}"></script>{{end}}
|
||||
<script src="/s/jquery-3.1.1.min.js"></script>
|
||||
<script async src="{{.}}"></script>{{end}}
|
||||
<script src="{{res "jquery-3.1.1.min.js"}}"></script>
|
||||
{{range .Header.Scripts}}
|
||||
<script src="/s/{{.}}"></script>{{end}}
|
||||
<script src="{{.}}"></script>{{end}}
|
||||
<meta name="viewport"content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
{{if .Header.MetaDesc}}<meta name="description"content="{{.Header.MetaDesc}}">{{end}}
|
||||
{{/** TODO: Have page / forum / topic level tags and descriptions below as-well **/}}
|
||||
|
@ -1,37 +1,37 @@
|
||||
<main id="topicPage">
|
||||
|
||||
{{if gt .Page 1}}<link rel="prev"href="{{.Topic.Link}}?page={{subtract .Page 1}}"/>{{end}}
|
||||
{{if ne .LastPage .Page}}<link rel="prerender next"href="{{.Topic.Link}}?page={{add .Page 1}}"/>{{end}}
|
||||
{{if not .CurrentUser.Loggedin}}<link rel="canonical" href="//{{.Site.URL}}{{.Topic.Link}}{{if gt .Page 1}}?page={{.Page}}{{end}}"/>{{end}}
|
||||
{{if gt .Page 1}}<link rel="prev"href="{{.Topic.Link}}?page={{subtract .Page 1}}">{{end}}
|
||||
{{if ne .LastPage .Page}}<link rel="prerender next"href="{{.Topic.Link}}?page={{add .Page 1}}">{{end}}
|
||||
{{if not .CurrentUser.Loggedin}}<link rel="canonical"href="//{{.Site.URL}}{{.Topic.Link}}{{if gt .Page 1}}?page={{.Page}}{{end}}">{{end}}
|
||||
|
||||
<div {{scope "topic_title_block"}} class="rowblock rowhead topic_block" aria-label="{{lang "topic.topic_info_aria"}}">
|
||||
<div {{scope "topic_title_block"}} class="rowblock rowhead topic_block"aria-label="{{lang "topic.topic_info_aria"}}">
|
||||
<div class="rowitem topic_item{{if .Topic.Sticky}} topic_sticky_head{{else if .Topic.IsClosed}} topic_closed_head{{end}}">
|
||||
<h1 class='topic_name hide_on_edit' title='{{.Topic.Title}}'>{{.Topic.Title}}</h1>
|
||||
<h1 class='topic_name hide_on_edit'title='{{.Topic.Title}}'>{{.Topic.Title}}</h1>
|
||||
<span class="topic_name_forum_sep hide_on_edit"> - </span>
|
||||
<a href="{{.Forum.Link}}" class="topic_forum hide_on_edit">{{.Forum.Name}}</a>
|
||||
<a href="{{.Forum.Link}}"class="topic_forum hide_on_edit">{{.Forum.Name}}</a>
|
||||
{{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}}
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||
{{if .CurrentUser.Perms.EditTopic}}
|
||||
<form id="edit_topic_form" action='/topic/edit/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' method="post"></form>
|
||||
<input form="edit_topic_form" class='show_on_edit topic_name_input' name="topic_name" value='{{.Topic.Title}}' type="text" aria-label="{{lang "topic.title_input_aria"}}">
|
||||
<button form="edit_topic_form" name="topic-button" class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
|
||||
<form id="edit_topic_form"action='/topic/edit/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}'method="post"></form>
|
||||
<input form="edit_topic_form"class='show_on_edit topic_name_input'name="topic_name"value='{{.Topic.Title}}'type="text"aria-label="{{lang "topic.title_input_aria"}}">
|
||||
<button form="edit_topic_form"name="topic-button"class="formbutton show_on_edit submit_edit">{{lang "topic.update_button"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<span class="topic_view_count hide_on_edit">{{.Topic.ViewCount}}</span>
|
||||
{{/** TODO: Inline this CSS **/}}
|
||||
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit' title='{{lang "status.closed_tooltip"}}' aria-label='{{lang "topic.status_closed_aria"}}'>🔒︎</span>{{end}}
|
||||
{{if .Topic.IsClosed}}<span class='username hide_on_micro topic_status_e topic_status_closed hide_on_edit'title='{{lang "status.closed_tooltip"}}'aria-label='{{lang "topic.status_closed_aria"}}'>🔒︎</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rowblock post_container">
|
||||
{{if .Poll}}{{template "topic_alt_poll.html" . }}{{end}}
|
||||
<article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item top_post{{if .Topic.Attachments}} has_attachs{{end}}" aria-label="{{lang "topic.opening_post_aria"}}">
|
||||
<article {{scope "opening_post"}} itemscope itemtype="http://schema.org/CreativeWork"class="rowitem passive deletable_block editable_parent post_item top_post{{if .Topic.Attachments}} has_attachs{{end}}"aria-label="{{lang "topic.opening_post_aria"}}">
|
||||
{{template "topic_alt_userinfo.html" .Topic }}
|
||||
<div class="content_container">
|
||||
<div class="hide_on_edit topic_content user_content" itemprop="text">{{.Topic.ContentHTML}}</div>
|
||||
{{if .CurrentUser.Loggedin}}<textarea name="topic_content" class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea>
|
||||
<div class="hide_on_edit topic_content user_content"itemprop="text">{{.Topic.ContentHTML}}</div>
|
||||
{{if .CurrentUser.Loggedin}}<textarea name="topic_content"class="show_on_edit topic_content_input edit_source">{{.Topic.Content}}</textarea>
|
||||
|
||||
{{if .CurrentUser.Perms.EditTopic}}
|
||||
<div class="show_on_edit attach_edit_bay"type="topic"id="{{.Topic.ID}}">
|
||||
@ -45,8 +45,8 @@
|
||||
{{end}}
|
||||
<div class="attach_item attach_item_buttons">
|
||||
{{if .CurrentUser.Perms.UploadFiles}}
|
||||
<input name="upload_files" id="upload_files_op" multiple type="file" class="auto_hide">
|
||||
<label for="upload_files_op" class="formbutton add_file_button">{{lang "topic.upload_button_text"}}</label>{{end}}
|
||||
<input name="upload_files"id="upload_files_op"multiple type="file"class="auto_hide">
|
||||
<label for="upload_files_op"class="formbutton add_file_button">{{lang "topic.upload_button_text"}}</label>{{end}}
|
||||
<button class="attach_item_delete formbutton">{{lang "topic.delete_button_text"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,26 +56,26 @@
|
||||
<div class="action_button_left">
|
||||
{{if .CurrentUser.Loggedin}}
|
||||
{{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}}
|
||||
{{if .Topic.Liked}}<a href="/topic/unlike/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button like_item remove_like" aria-label="{{lang "topic.unlike_aria"}}" data-action="unlike"></a>{{else}}<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button like_item add_like" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}
|
||||
{{if .Topic.Liked}}<a href="/topic/unlike/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}"class="action_button like_item remove_like"aria-label="{{lang "topic.unlike_aria"}}"data-action="unlike"></a>{{else}}<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}"class="action_button like_item add_like"aria-label="{{lang "topic.like_aria"}}"data-action="like"></a>{{end}}
|
||||
{{end}}{{end}}
|
||||
<a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a>
|
||||
<a href=""class="action_button quote_item"aria-label="{{lang "topic.quote_aria"}}"data-action="quote"></a>
|
||||
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
|
||||
{{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit" aria-label="{{lang "topic.edit_aria"}}" data-action="edit"></a>{{end}}
|
||||
{{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}"class="action_button open_edit"aria-label="{{lang "topic.edit_aria"}}"data-action="edit"></a>{{end}}
|
||||
{{end}}
|
||||
{{if .Topic.Deletable}}<a href="/topic/delete/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button delete_item" aria-label="{{lang "topic.delete_aria"}}" data-action="delete"></a>{{end}}
|
||||
{{if .Topic.Deletable}}<a href="/topic/delete/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}"class="action_button delete_item"aria-label="{{lang "topic.delete_aria"}}"data-action="delete"></a>{{end}}
|
||||
{{if .CurrentUser.Perms.CloseTopic}}
|
||||
{{if .Topic.IsClosed}}<a href='/topic/unlock/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' class="action_button unlock_item" data-action="unlock" aria-label="{{lang "topic.unlock_aria"}}"></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' class="action_button lock_item" data-action="lock" aria-label="{{lang "topic.lock_aria"}}"></a>{{end}}{{end}}
|
||||
{{if .Topic.IsClosed}}<a href='/topic/unlock/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}'class="action_button unlock_item"data-action="unlock"aria-label="{{lang "topic.unlock_aria"}}"></a>{{else}}<a href='/topic/lock/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}'class="action_button lock_item"data-action="lock"aria-label="{{lang "topic.lock_aria"}}"></a>{{end}}{{end}}
|
||||
{{if .CurrentUser.Perms.PinTopic}}
|
||||
{{if .Topic.Sticky}}<a href='/topic/unstick/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' class="action_button unpin_item" data-action="unpin" aria-label="{{lang "topic.unpin_aria"}}"></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}' class="action_button pin_item" data-action="pin" aria-label="{{lang "topic.pin_aria"}}"></a>{{end}}{{end}}
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IP}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item_button hide_on_big" aria-label="{{lang "topic.ip_full_aria"}}" data-action="ip"></a>{{end}}
|
||||
<a href="/report/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}&type=topic" class="action_button report_item" aria-label="{{lang "topic.report_aria"}}" data-action="report"></a>
|
||||
<a href="#" class="action_button button_menu"></a>
|
||||
{{if .Topic.Sticky}}<a href='/topic/unstick/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}'class="action_button unpin_item"data-action="unpin"aria-label="{{lang "topic.unpin_aria"}}"></a>{{else}}<a href='/topic/stick/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}'class="action_button pin_item"data-action="pin"aria-label="{{lang "topic.pin_aria"}}"></a>{{end}}{{end}}
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IP}}"title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item_button hide_on_big"aria-label="{{lang "topic.ip_full_aria"}}"data-action="ip"></a>{{end}}
|
||||
<a href="/report/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}&type=topic"class="action_button report_item"aria-label="{{lang "topic.report_aria"}}"data-action="report"></a>
|
||||
<a href="#"class="action_button button_menu"></a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="action_button_right">
|
||||
<a class="action_button like_count hide_on_micro" aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
|
||||
<a class="action_button created_at hide_on_mobile" title="{{abstime .Topic.CreatedAt}}">{{reltime .Topic.CreatedAt}}</a>
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IP}}" title="{{lang "topic.ip_full_tooltip"}}" class="action_button ip_item hide_on_mobile" aria-hidden="true">{{.Topic.IP}}</a>{{end}}
|
||||
<a class="action_button like_count hide_on_micro"aria-label="{{lang "topic.like_count_aria"}}">{{.Topic.LikeCount}}</a>
|
||||
<a class="action_button created_at hide_on_mobile"title="{{abstime .Topic.CreatedAt}}">{{reltime .Topic.CreatedAt}}</a>
|
||||
{{if .CurrentUser.Perms.ViewIPs}}<a href="/users/ips/?ip={{.Topic.IP}}"title="{{lang "topic.ip_full_tooltip"}}"class="action_button ip_item hide_on_mobile"aria-hidden="true">{{.Topic.IP}}</a>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div><div style="clear:both;"></div>
|
||||
|
Loading…
Reference in New Issue
Block a user