1086 lines
27 KiB
Go
1086 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"git.openbsd.org.ru/vasyahacker/openidec/ii"
|
|
"os"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"image"
|
|
"image/png"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const PAGE_SIZE = 100
|
|
|
|
type WebContext struct {
|
|
Echoes []*ii.Echo
|
|
Topics []*Topic
|
|
Topic string
|
|
Msg []*ii.Msg
|
|
Error string
|
|
Echo string
|
|
PfxPath string
|
|
Page int
|
|
Pages int
|
|
Pager []int
|
|
BasePath string
|
|
User *ii.User
|
|
Echolist *ii.EDB
|
|
Selected string
|
|
Template string
|
|
Ref string
|
|
Info string
|
|
Sysname string
|
|
Host string
|
|
www *WWW
|
|
}
|
|
|
|
func www_register(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
ii.Trace.Printf("www register")
|
|
switch r.Method {
|
|
case "GET":
|
|
ctx.Template = "register.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "register.tpl", ctx)
|
|
return err
|
|
case "POST":
|
|
udb := ctx.www.udb
|
|
if err := r.ParseForm(); err != nil {
|
|
ii.Error.Printf("Error in POST request: %s", err)
|
|
return err
|
|
}
|
|
auth := r.FormValue("auth")
|
|
if auth != "" { /* edit form */
|
|
u := udb.UserInfo(auth)
|
|
if u == nil {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
password := r.FormValue("password")
|
|
u.Secret = ii.MakeSecret(u.Name + password)
|
|
if err := udb.Edit(u); err != nil {
|
|
ii.Info.Printf("Can not edit user %s: %s", ctx.User.Name, err)
|
|
return err
|
|
}
|
|
http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
user := r.FormValue("username")
|
|
password := r.FormValue("password")
|
|
email := r.FormValue("email")
|
|
|
|
err := udb.Add(user, email, password)
|
|
if err != nil {
|
|
ii.Info.Printf("Can not register user %s: %s", user, err)
|
|
return err
|
|
}
|
|
ii.Info.Printf("Registered user: %s", user)
|
|
http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther)
|
|
default:
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func www_login(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
ii.Trace.Printf("www login")
|
|
switch r.Method {
|
|
case "GET":
|
|
ctx.Template = "login.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "login.tpl", ctx)
|
|
return err
|
|
case "POST":
|
|
if err := r.ParseForm(); err != nil {
|
|
ii.Error.Printf("Error in POST request: %s", err)
|
|
return err
|
|
}
|
|
user := r.FormValue("username")
|
|
password := r.FormValue("password")
|
|
udb := ctx.www.udb
|
|
if !udb.Auth(user, password) {
|
|
ii.Info.Printf("Access denied for user: %s", user)
|
|
return errors.New("Access denied")
|
|
}
|
|
exp := time.Now().Add(10 * 365 * 24 * time.Hour)
|
|
cookie := http.Cookie{Name: "pauth", Value: udb.Secret(user), Expires: exp}
|
|
http.SetCookie(w, &cookie)
|
|
ii.Info.Printf("User logged in: %s\n", user)
|
|
http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
return errors.New("Wrong method")
|
|
}
|
|
|
|
func www_profile(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
ii.Trace.Printf("www profile")
|
|
if ctx.User.Name == "" {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
ctx.Selected = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id)
|
|
ava, _ := ctx.User.Tags.Get("avatar")
|
|
if ava != "" {
|
|
if data, err := base64.URLEncoding.DecodeString(ava); err == nil {
|
|
ctx.Info = string(data)
|
|
}
|
|
}
|
|
ctx.Template = "profile.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "profile.tpl", ctx)
|
|
return err
|
|
}
|
|
|
|
func www_logout(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
ii.Trace.Printf("www logout: %s", ctx.User.Name)
|
|
if ctx.User.Name == "" {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
cookie := http.Cookie{Name: "pauth", Value: "", Expires: time.Unix(0, 0)}
|
|
http.SetCookie(w, &cookie)
|
|
http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func www_index(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
ii.Trace.Printf("www index")
|
|
ctx.Echoes = ctx.www.db.Echoes(nil, ii.Query{User: *ctx.User})
|
|
ctx.Template = "index.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "index.tpl", ctx)
|
|
return err
|
|
}
|
|
|
|
func parse_ava(txt string) *image.RGBA {
|
|
txt = msg_clean(txt)
|
|
lines := strings.Split(txt, "\n")
|
|
img, _ := ParseXpm(lines)
|
|
return img
|
|
}
|
|
|
|
var magicTable = map[string]string{
|
|
"\xff\xd8\xff": "image/jpeg",
|
|
"\x89PNG\r\n\x1a\n": "image/png",
|
|
"GIF87a": "image/gif",
|
|
"GIF89a": "image/gif",
|
|
}
|
|
|
|
func check_image(incipit []byte) string {
|
|
incipitStr := string(incipit)
|
|
for magic, mime := range magicTable {
|
|
if strings.HasPrefix(incipitStr, magic) {
|
|
return mime
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func www_base64(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
id := ctx.BasePath
|
|
m := ctx.www.db.Get(id)
|
|
if m == nil {
|
|
return errors.New("No such message")
|
|
}
|
|
lines := strings.Split(msg_clean(m.Text), "\n")
|
|
start := false
|
|
b64 := ""
|
|
fname := ""
|
|
pre := false
|
|
for _, v := range lines {
|
|
if !start && strings.Trim(v, " ") == "====" {
|
|
if !pre {
|
|
pre = true
|
|
continue
|
|
}
|
|
pre = false
|
|
continue
|
|
}
|
|
if pre {
|
|
continue
|
|
}
|
|
if !start && !strings.HasPrefix(v, "@base64:") {
|
|
continue
|
|
}
|
|
if start {
|
|
v = strings.Replace(v, " ", "", -1)
|
|
if !base64Regex.MatchString(v) {
|
|
break
|
|
}
|
|
b64 += v
|
|
continue
|
|
}
|
|
v = strings.TrimPrefix(v, "@base64:")
|
|
v = strings.Trim(v, " ")
|
|
fname = v
|
|
if fname == "" {
|
|
fname = "file"
|
|
}
|
|
start = true
|
|
}
|
|
if b64 == "" {
|
|
return nil
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(b64)
|
|
if err != nil {
|
|
if b, err = base64.RawStdEncoding.DecodeString(b64); err != nil {
|
|
if b, err = base64.URLEncoding.DecodeString(b64); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// w.Header().Set("Content-Type", "image/jpeg")
|
|
if check_image(b) != "" {
|
|
w.Header().Set("Content-Disposition", "inline")
|
|
} else {
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fname))
|
|
}
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b)))
|
|
_, err = w.Write(b)
|
|
return err
|
|
}
|
|
func www_avatar(ctx *WebContext, w http.ResponseWriter, r *http.Request, user string) error {
|
|
if r.Method == "POST" { /* upload avatar */
|
|
if ctx.User.Name == "" || ctx.User.Name != user {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
ii.Error.Printf("Error in POST request: %s", err)
|
|
return err
|
|
}
|
|
ava := r.FormValue("avatar")
|
|
if len(ava) > 2048 {
|
|
ii.Error.Printf("Avatar is too big.")
|
|
return errors.New("Avatar is too big (>2048 bytes)")
|
|
}
|
|
if ava == "" {
|
|
ii.Trace.Printf("Delete avatar for %s", ctx.User.Name)
|
|
ctx.User.Tags.Del("avatar")
|
|
} else {
|
|
img := parse_ava(ava)
|
|
if img == nil {
|
|
ii.Error.Printf("Wrong xpm format for avatar: " + user)
|
|
return errors.New("Wrong xpm format")
|
|
}
|
|
b64 := base64.URLEncoding.EncodeToString([]byte(ava))
|
|
ii.Trace.Printf("New avatar for %s: %s", ctx.User.Name, b64)
|
|
ctx.User.Tags.Add("avatar/" + b64)
|
|
}
|
|
if err := ctx.www.udb.Edit(ctx.User); err != nil {
|
|
ii.Error.Printf("Error saving avatar: " + user)
|
|
return errors.New("Error saving avatar")
|
|
}
|
|
http.Redirect(w, r, ctx.PfxPath + "/profile", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
// var id int32
|
|
// if !strings.HasPrefix(user, ctx.www.db.Name) {
|
|
// return nil
|
|
// }
|
|
// user = strings.TrimPrefix(user, ctx.www.db.Name)
|
|
// user = strings.TrimPrefix(user, ",")
|
|
// if _, err := fmt.Sscanf(user, "%d", &id); err != nil {
|
|
// return nil
|
|
// }
|
|
// u := ctx.www.udb.UserInfoId(id)
|
|
u := ctx.www.udb.UserInfoName(user)
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
ava, _ := u.Tags.Get("avatar")
|
|
if ava == "" {
|
|
return nil
|
|
}
|
|
if data, err := base64.URLEncoding.DecodeString(ava); err == nil {
|
|
img := parse_ava(string(data))
|
|
if img == nil {
|
|
ii.Error.Printf("Wrong xpm in avatar: %s\n", u.Name)
|
|
return nil
|
|
}
|
|
b := new(bytes.Buffer)
|
|
if err := png.Encode(b, img); err == nil {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b.Bytes())))
|
|
if _, err := w.Write(b.Bytes()); err != nil {
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
ii.Error.Printf("Can't encode avatar in png: %s\n", u.Name)
|
|
} else {
|
|
ii.Error.Printf("Can't decode avatar: %s\n", u.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Topic struct {
|
|
Ids []string
|
|
Count int
|
|
Last *ii.MsgInfo
|
|
Head *ii.Msg
|
|
Tail *ii.Msg
|
|
}
|
|
|
|
func makePager(ctx *WebContext, count int, page int) int {
|
|
ctx.Pages = count / PAGE_SIZE
|
|
if count%PAGE_SIZE != 0 {
|
|
ctx.Pages++
|
|
}
|
|
if page == 0 {
|
|
page++
|
|
} else if page < 0 {
|
|
page = ctx.Pages + page + 1
|
|
}
|
|
start := (page - 1) * PAGE_SIZE
|
|
if start < 0 {
|
|
start = 0
|
|
page = 1
|
|
}
|
|
ctx.Page = page
|
|
if ctx.Pages > 1 {
|
|
for i := 1; i <= ctx.Pages; i++ {
|
|
ctx.Pager = append(ctx.Pager, i)
|
|
}
|
|
}
|
|
return start
|
|
}
|
|
|
|
func Select(ctx *WebContext, q ii.Query) []string {
|
|
q.User = *ctx.User
|
|
return ctx.www.db.SelectIDS(q)
|
|
}
|
|
|
|
func trunc(str string, limit int) string {
|
|
result := []rune(str)
|
|
if len(result) > limit {
|
|
return string(result[:limit])
|
|
}
|
|
return str
|
|
}
|
|
|
|
func www_query(ctx *WebContext, w http.ResponseWriter, r *http.Request, q ii.Query, page int, rss bool) error {
|
|
db := ctx.www.db
|
|
req := ctx.BasePath
|
|
|
|
if rss {
|
|
q.Start = -PAGE_SIZE
|
|
}
|
|
mis := db.LookupIDS(Select(ctx, q))
|
|
ii.Trace.Printf("www query")
|
|
|
|
sort.SliceStable(mis, func(i, j int) bool {
|
|
return mis[i].Num > mis[j].Num
|
|
})
|
|
count := len(mis)
|
|
start := makePager(ctx, count, page)
|
|
nr := PAGE_SIZE
|
|
for i := start; i < count && nr > 0; i++ {
|
|
m := db.GetFast(mis[i].Id)
|
|
if m == nil {
|
|
ii.Error.Printf("Can't get msg: %s\n", mis[i].Id)
|
|
continue
|
|
}
|
|
ctx.Msg = append(ctx.Msg, m)
|
|
nr--
|
|
}
|
|
if rss {
|
|
ctx.Topic = db.Name + " :: " + req
|
|
fmt.Fprintf(w,
|
|
`<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0"
|
|
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
xmlns:media="http://search.yahoo.com/mrss/"
|
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
xmlns:georss="http://www.georss.org/georss">
|
|
<channel>
|
|
<title>%s</title>
|
|
<link>%s/%s</link>
|
|
<description>
|
|
%s
|
|
</description>
|
|
<language>ru</language>
|
|
`,
|
|
str_esc(ctx.Topic), ctx.www.Host, ctx.BasePath, str_esc(ctx.Topic))
|
|
for _, m := range ctx.Msg {
|
|
fmt.Fprintf(w,
|
|
`<item><title>%s</title><guid>%s</guid><pubDate>%s</pubDate><author>%s</author><link>%s/%s#%s</link>
|
|
<description>
|
|
%s...
|
|
</description>
|
|
<content:encoded>
|
|
<![CDATA[
|
|
%s
|
|
%s
|
|
]]>
|
|
</content:encoded></item>
|
|
`,
|
|
str_esc(m.Subj), m.MsgId, time.Unix(m.Date, 0).Format("2006-01-02 15:04:05"),
|
|
str_esc(m.From), ctx.www.Host + ctx.PfxPath, m.MsgId, m.MsgId,
|
|
str_esc(trunc(m.Text, 280)),
|
|
fmt.Sprintf("%s -> %s<br><br>", m.From, m.To),
|
|
msg_text(m))
|
|
}
|
|
fmt.Fprintf(w, `</channel></rss>
|
|
`)
|
|
return nil
|
|
}
|
|
ctx.Template = "query.tpl"
|
|
return ctx.www.tpl.ExecuteTemplate(w, "query.tpl", ctx)
|
|
}
|
|
|
|
func www_topics(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error {
|
|
db := ctx.www.db
|
|
echo := ctx.Echo
|
|
mis := db.LookupIDS(Select(ctx, ii.Query{Echo: echo}))
|
|
ii.Trace.Printf("www topics: %s", echo)
|
|
topicsIds := db.GetTopics(mis)
|
|
var topics []*Topic
|
|
ii.Trace.Printf("Start to generate topics")
|
|
|
|
db.Sync.RLock()
|
|
defer db.Sync.RUnlock()
|
|
db.LoadIndex()
|
|
for _, t := range topicsIds {
|
|
topic := Topic{}
|
|
topic.Ids = t
|
|
topic.Count = len(topic.Ids) - 1
|
|
if ctx.PfxPath == "/blog" {
|
|
topic.Last = db.LookupFast(topic.Ids[0], false)
|
|
} else {
|
|
topic.Last = db.LookupFast(topic.Ids[topic.Count], false)
|
|
}
|
|
if topic.Last == nil {
|
|
ii.Error.Printf("Skip wrong message: %s\n", t[0])
|
|
continue
|
|
}
|
|
topics = append(topics, &topic)
|
|
}
|
|
sort.SliceStable(topics, func(i, j int) bool {
|
|
return topics[i].Last.Num > topics[j].Last.Num
|
|
})
|
|
tcount := len(topics)
|
|
start := makePager(ctx, tcount, page)
|
|
nr := PAGE_SIZE
|
|
for i := start; i < tcount && nr > 0; i++ {
|
|
t := topics[i]
|
|
t.Head = db.GetFast(t.Ids[0])
|
|
t.Tail = db.GetFast(t.Ids[t.Count])
|
|
if t.Head == nil || t.Tail == nil {
|
|
ii.Error.Printf("Skip wrong message: %s\n", t.Ids[0])
|
|
continue
|
|
}
|
|
ctx.Topics = append(ctx.Topics, topics[i])
|
|
nr--
|
|
}
|
|
ii.Trace.Printf("Stop to generate topics")
|
|
|
|
if ctx.PfxPath == "/blog" {
|
|
ctx.Template = "blog.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "blog.tpl", ctx)
|
|
return err
|
|
}
|
|
ctx.Template = "topics.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "topics.tpl", ctx)
|
|
return err
|
|
}
|
|
|
|
func www_topic(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error {
|
|
id := ctx.BasePath
|
|
db := ctx.www.db
|
|
|
|
mi := db.Lookup(id)
|
|
if mi == nil {
|
|
return errors.New("No such message")
|
|
}
|
|
|
|
if !db.Access(mi, ctx.User) {
|
|
return errors.New("Access denied")
|
|
}
|
|
|
|
if page == 0 {
|
|
ctx.Selected = id
|
|
}
|
|
ctx.Echo = mi.Echo
|
|
mis := db.LookupIDS(Select(ctx, ii.Query{Echo: mi.Echo}))
|
|
|
|
topics := db.GetTopics(mis)
|
|
topic := mi.Topic
|
|
ctx.Topic = topic
|
|
ids := topics[topic]
|
|
|
|
if len(ids) == 0 {
|
|
ids = append(ids, id)
|
|
} else if topic != mi.Id {
|
|
for k, v := range ids {
|
|
if v == mi.Id {
|
|
page = k/PAGE_SIZE + 1
|
|
ctx.Selected = mi.Id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
ii.Trace.Printf("www topic: %s", id)
|
|
start := makePager(ctx, len(ids), page)
|
|
nr := PAGE_SIZE
|
|
for i := start; i < len(ids) && nr > 0; i++ {
|
|
id := ids[i]
|
|
m := db.Get(id)
|
|
if m == nil {
|
|
ii.Error.Printf("Skip wrong message: %s", id)
|
|
continue
|
|
}
|
|
ctx.Msg = append(ctx.Msg, m)
|
|
nr--
|
|
}
|
|
ctx.Template = "topic.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "topic.tpl", ctx)
|
|
return err
|
|
}
|
|
|
|
func www_blacklist(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
id := ctx.BasePath
|
|
m := ctx.www.db.Get(id)
|
|
ii.Trace.Printf("www blacklist: %s", id)
|
|
if m == nil {
|
|
ii.Error.Printf("No such msg: %s", id)
|
|
return errors.New("No such msg")
|
|
}
|
|
if !msg_access(ctx.www, *m, *ctx.User) {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
err := ctx.www.db.Blacklist(m)
|
|
if err != nil {
|
|
ii.Error.Printf("Error blacklisting: %s", id)
|
|
return err
|
|
}
|
|
http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
|
|
func www_edit(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
id := ctx.BasePath
|
|
switch r.Method {
|
|
case "GET":
|
|
m := ctx.www.db.Get(id)
|
|
if m == nil {
|
|
ii.Error.Printf("No such msg: %s", id)
|
|
return errors.New("No such msg")
|
|
}
|
|
msg := *m
|
|
ln := strings.Split(msg_clean(msg.Text), "\n")
|
|
if len(ln) > 0 {
|
|
if strings.HasPrefix(ln[len(ln)-1], "P.S. Edited: ") {
|
|
msg.Text = strings.Join(ln[:len(ln)-1], "\n")
|
|
}
|
|
}
|
|
msg.Text = msg.Text + "\nP.S. Edited: " + time.Now().Format("2006-01-02 15:04:05")
|
|
ctx.Msg = append(ctx.Msg, &msg)
|
|
ctx.Template = "edit.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "edit.tpl", ctx)
|
|
return err
|
|
case "POST":
|
|
ctx.BasePath = ""
|
|
return www_new(ctx, w, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func www_new(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
echo := ctx.BasePath
|
|
ctx.Echo = echo
|
|
|
|
switch r.Method {
|
|
case "GET":
|
|
ctx.Template = "new.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "new.tpl", ctx)
|
|
return err
|
|
case "POST":
|
|
edit := (echo == "")
|
|
ii.Trace.Printf("www new topic in %s", echo)
|
|
if err := r.ParseForm(); err != nil {
|
|
ii.Error.Printf("Error in POST request: %s", err)
|
|
return err
|
|
}
|
|
if ctx.User.Name == "" {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
subj := r.FormValue("subj")
|
|
to := r.FormValue("to")
|
|
msg := r.FormValue("msg")
|
|
repto := r.FormValue("repto")
|
|
id := r.FormValue("id")
|
|
if repto == id {
|
|
repto = ""
|
|
}
|
|
newecho := r.FormValue("echo")
|
|
if newecho != "" {
|
|
echo = newecho
|
|
}
|
|
if !ctx.www.edb.Allowed(echo) && ctx.User.Id != 1 {
|
|
ii.Error.Printf("This echo is disallowed")
|
|
return errors.New("This echo is disallowed")
|
|
}
|
|
action := r.FormValue("action")
|
|
text := fmt.Sprintf("%s\n%s\n%s\n\n%s", echo, to, subj, msg)
|
|
m, err := ii.DecodeMsgline(text, false)
|
|
if err != nil {
|
|
ii.Error.Printf("Error while posting new topic: %s", err)
|
|
return err
|
|
}
|
|
m.From = ctx.User.Name
|
|
m.Addr = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id)
|
|
if repto != "" {
|
|
m.Tags.Add("repto/" + repto)
|
|
}
|
|
if id != "" {
|
|
om := ctx.www.db.Get(id)
|
|
if (om == nil || m.Addr != om.Addr) && ctx.User.Id != 1 {
|
|
ii.Error.Printf("Access denied")
|
|
return errors.New("Access denied")
|
|
}
|
|
m.Date = om.Date
|
|
m.MsgId = id
|
|
m.From = om.From
|
|
m.Addr = om.Addr
|
|
}
|
|
if action == "Submit" { // submit
|
|
if edit {
|
|
err = ctx.www.db.Edit(m)
|
|
} else {
|
|
err = ctx.www.db.Store(m)
|
|
}
|
|
if err != nil {
|
|
ii.Error.Printf("Error while storig new topic %s: %s", m.MsgId, err)
|
|
return err
|
|
}
|
|
http.Redirect(w, r, ctx.PfxPath + "/"+m.MsgId+"#"+m.MsgId, http.StatusSeeOther)
|
|
return nil
|
|
}
|
|
if !edit {
|
|
m.MsgId = ""
|
|
}
|
|
ctx.Msg = append(ctx.Msg, m)
|
|
ctx.Template = "preview.tpl"
|
|
err = ctx.www.tpl.ExecuteTemplate(w, "preview.tpl", ctx)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func www_reply(ctx *WebContext, w http.ResponseWriter, r *http.Request, quote bool) error {
|
|
id := ctx.BasePath
|
|
m := ctx.www.db.Get(id)
|
|
if m == nil {
|
|
ii.Error.Printf("No such msg: %s", id)
|
|
return errors.New("No such msg")
|
|
}
|
|
msg := *m
|
|
msg.To = msg.From
|
|
msg.Subj = "Re: " + strings.TrimPrefix(msg.Subj, "Re: ")
|
|
msg.Tags.Add("repto/" + id)
|
|
if quote {
|
|
msg.Text = msg_quote(msg.Text, msg.From)
|
|
} else {
|
|
msg.Text = ""
|
|
}
|
|
ctx.Msg = append(ctx.Msg, &msg)
|
|
ctx.Echo = msg.Echo
|
|
ctx.Template = "reply.tpl"
|
|
err := ctx.www.tpl.ExecuteTemplate(w, "reply.tpl", ctx)
|
|
return err
|
|
}
|
|
|
|
func str_esc(l string) string {
|
|
l = strings.Replace(l, "&", "&", -1)
|
|
l = strings.Replace(l, "<", "<", -1)
|
|
l = strings.Replace(l, ">", ">", -1)
|
|
return l
|
|
}
|
|
|
|
var quoteRegex = regexp.MustCompile("^[^ >]*>")
|
|
var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`)
|
|
var url2Regex = regexp.MustCompile(`{{{href=[0-9]+}}}`)
|
|
var urlIIRegex = regexp.MustCompile(`ii://[a-zA-Z0-9_\-.]+`)
|
|
var base64Regex = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
|
|
|
|
func msg_clean(txt string) string {
|
|
txt = strings.Replace(txt, "\r", "", -1)
|
|
txt = strings.TrimLeft(txt, "\n")
|
|
txt = strings.TrimRight(txt, "\n")
|
|
return txt
|
|
}
|
|
func msg_quote(txt string, from string) string {
|
|
txt = msg_clean(txt)
|
|
f := ""
|
|
names := strings.Split(from, " ")
|
|
if len(names) >= 2 {
|
|
from = fmt.Sprintf("%v%v",
|
|
string([]rune(names[0])[0]),
|
|
string([]rune(names[1])[0]))
|
|
}
|
|
for _, l := range strings.Split(txt, "\n") {
|
|
if strings.Trim(l, " ") == "" {
|
|
f += l + "\n"
|
|
continue
|
|
}
|
|
if quoteRegex.MatchString(l) {
|
|
s := strings.Index(l, ">")
|
|
f += l[:s] + ">>" + l[s+1:] + "\n"
|
|
} else {
|
|
f += from + "> " + l + "\n"
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func ReverseStr(s string) string {
|
|
runes := []rune(s)
|
|
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
|
|
runes[i], runes[j] = runes[j], runes[i]
|
|
}
|
|
return string(runes)
|
|
}
|
|
|
|
func msg_esc(l string) string {
|
|
var links []string
|
|
link := 0
|
|
l = string(urlIIRegex.ReplaceAllFunc([]byte(l),
|
|
func(line []byte) []byte {
|
|
s := string(line)
|
|
url := strings.TrimPrefix(s, "ii://")
|
|
links = append(links, fmt.Sprintf(`<a href="/%s#%s" class="url">%s</a>`,
|
|
url, url, str_esc(s)))
|
|
link++
|
|
return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1))
|
|
}))
|
|
l = string(urlRegex.ReplaceAllFunc([]byte(l),
|
|
func(line []byte) []byte {
|
|
s := string(line)
|
|
links = append(links, fmt.Sprintf(`<a href="%s" class="url">%s</a>`,
|
|
s, str_esc(s)))
|
|
link++
|
|
return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1))
|
|
}))
|
|
l = str_esc(l)
|
|
l = string(url2Regex.ReplaceAllFunc([]byte(l),
|
|
func(line []byte) []byte {
|
|
s := string(line)
|
|
var n int
|
|
fmt.Sscanf(s, "{{{href=%d}}}", &n)
|
|
return []byte(links[n])
|
|
}))
|
|
|
|
return l
|
|
}
|
|
|
|
func msg_text(m *ii.Msg) string {
|
|
return msg_trunc(m, 0, "")
|
|
}
|
|
|
|
func msg_trunc(m *ii.Msg, maxlen int, more string) string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
txt := m.Text
|
|
txt = msg_clean(txt)
|
|
f := ""
|
|
pre := false
|
|
skip := 0
|
|
lines := strings.Split(txt, "\n")
|
|
for k, l := range lines {
|
|
if skip > 0 {
|
|
skip--
|
|
continue
|
|
}
|
|
if strings.Trim(l, " ") == "====" {
|
|
if !pre {
|
|
pre = true
|
|
f += "<pre class=\"code\">\n"
|
|
continue
|
|
}
|
|
pre = false
|
|
f += "</pre>\n"
|
|
continue
|
|
}
|
|
if pre {
|
|
f += str_esc(l) + "\n"
|
|
continue
|
|
}
|
|
if strings.HasPrefix(l, "/* XPM */") || strings.HasPrefix(l, "! XPM2") {
|
|
var img *image.RGBA
|
|
img, skip = ParseXpm(lines[k:])
|
|
if img != nil {
|
|
skip--
|
|
/* embed xpm */
|
|
b := new(bytes.Buffer)
|
|
if err := png.Encode(b, img); err == nil {
|
|
b64 := base64.StdEncoding.EncodeToString(b.Bytes())
|
|
l = fmt.Sprintf("<img class=\"img\" src=\"data:image/png;base64,%s\"><br>\n",
|
|
b64)
|
|
f += l
|
|
continue
|
|
}
|
|
}
|
|
skip = 0
|
|
l = msg_esc(l)
|
|
} else if strings.HasPrefix(l, "P.S.") || strings.HasPrefix(l, "PS:") ||
|
|
strings.HasPrefix(l, "//") || strings.HasPrefix(l, "+++ ") {
|
|
l = fmt.Sprintf("<span class=\"comment\">%s</span>", str_esc(l))
|
|
} else if strings.HasPrefix(l, "# ") || strings.HasPrefix(l, "= ") ||
|
|
strings.HasPrefix(l, "## ") || strings.HasPrefix(l, "== ") ||
|
|
strings.HasPrefix(l, "### ") || strings.HasPrefix(l, "=== ") {
|
|
l = fmt.Sprintf("<span class=\"header\">%s</span>", str_esc(l))
|
|
} else if strings.HasPrefix(l, "@spoiler:") {
|
|
l = fmt.Sprintf("<span class=\"spoiler\">%s</span>", str_esc(ReverseStr(l)))
|
|
} else if quoteRegex.MatchString(l) {
|
|
l = fmt.Sprintf("<span class=\"quote\">%s</span>", str_esc(l))
|
|
} else if strings.HasPrefix(l, "@base64:") {
|
|
fname := strings.TrimPrefix(l, "@base64:")
|
|
fname = strings.Trim(fname, " ")
|
|
if fname == "" {
|
|
fname = "file"
|
|
}
|
|
f += fmt.Sprintf("<a class=\"attach\" href=\"/%s/base64\">%s</a><br>\n", m.MsgId, str_esc(fname))
|
|
return f
|
|
} else {
|
|
l = msg_esc(l)
|
|
}
|
|
f += l
|
|
if maxlen > 0 && len(f) > maxlen {
|
|
f += more + "<br>\n"
|
|
break
|
|
} else {
|
|
f += "<br>\n"
|
|
}
|
|
}
|
|
if pre {
|
|
pre = false
|
|
f += "</pre>\n"
|
|
}
|
|
return f
|
|
}
|
|
|
|
func msg_access(www *WWW, m ii.Msg, u ii.User) bool {
|
|
addr := fmt.Sprintf("%s,%d", www.db.Name, u.Id)
|
|
return addr == m.Addr || u.Id == 1
|
|
}
|
|
|
|
func WebInit(www *WWW) {
|
|
funcMap := template.FuncMap{
|
|
"fdate": func(date int64) template.HTML {
|
|
if time.Now().Unix()-date < 60*60*24 {
|
|
return template.HTML("<span class='today'>" + time.Unix(date, 0).Format("2006-01-02 15:04:05") + "</span>")
|
|
}
|
|
return template.HTML(time.Unix(date, 0).Format("2006-01-02 15:04:05"))
|
|
},
|
|
"msg_text": func(m *ii.Msg) template.HTML {
|
|
return template.HTML(msg_text(m))
|
|
},
|
|
"msg_trunc": func(m *ii.Msg, len int, more string) template.HTML {
|
|
return template.HTML(msg_trunc(m, len, more))
|
|
},
|
|
"repto": func(m ii.Msg) string {
|
|
r, _ := m.Tag("repto")
|
|
if r == "" {
|
|
return m.MsgId
|
|
}
|
|
return r
|
|
},
|
|
"msg_quote": msg_quote,
|
|
"msg_access": func(m ii.Msg, u ii.User) bool {
|
|
return msg_access(www, m, u)
|
|
},
|
|
"is_even": func(i int) bool {
|
|
return i%2 == 0
|
|
},
|
|
"unescape": func(s string) template.HTML {
|
|
return template.HTML(s)
|
|
},
|
|
"has_avatar": func(user string) bool {
|
|
ui := www.udb.UserInfoName(user)
|
|
if ui != nil {
|
|
_, ok := ui.Tags.Get("avatar")
|
|
return ok
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
|
|
tpl_path := "tpl"
|
|
if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) {
|
|
fmt.Println("./tpl not found, trying /usr/local/share/openidec/tpl..")
|
|
tpl_path = "/usr/local/share/openidec/tpl"
|
|
if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) {
|
|
fmt.Println(tpl_path, "not found")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
www.tpl = template.Must(
|
|
template.New("main").Funcs(funcMap).ParseGlob(tpl_path + "/*.tpl"))
|
|
}
|
|
|
|
func handleErr(ctx *WebContext, w http.ResponseWriter, err error) {
|
|
ctx.Error = err.Error()
|
|
ctx.Template = "error.tpl"
|
|
ctx.www.tpl.ExecuteTemplate(w, "error.tpl", ctx)
|
|
}
|
|
|
|
func handleWWW(www *WWW, w http.ResponseWriter, r *http.Request) {
|
|
var ctx WebContext
|
|
var user *ii.User = &ii.User{}
|
|
ctx.User = user
|
|
ctx.www = www
|
|
ctx.Sysname = www.db.Name
|
|
ctx.Host = www.Host
|
|
www.udb.LoadUsers()
|
|
err := _handleWWW(&ctx, w, r)
|
|
if err != nil {
|
|
handleErr(&ctx, w, err)
|
|
}
|
|
}
|
|
|
|
func _handleWWW(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
|
|
cookie, err := r.Cookie("pauth")
|
|
if err == nil {
|
|
udb := ctx.www.udb
|
|
if udb.Access(cookie.Value) {
|
|
if user := udb.UserInfo(cookie.Value); user != nil {
|
|
ctx.User = user
|
|
}
|
|
}
|
|
}
|
|
ii.Trace.Printf("[%s] GET %s", ctx.User.Name, r.URL.Path)
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
args := strings.Split(path, "/")
|
|
ctx.Echolist = ctx.www.edb
|
|
ctx.Ref = r.Header.Get("Referer")
|
|
if len(args) > 1 && args[0] == "blog" {
|
|
ctx.PfxPath = "/blog"
|
|
args = args[1:]
|
|
}
|
|
if args[0] == "" {
|
|
ctx.BasePath = ""
|
|
return www_index(ctx, w, r)
|
|
} else if args[0] == "login" {
|
|
ctx.BasePath = "login"
|
|
return www_login(ctx, w, r)
|
|
} else if args[0] == "logout" {
|
|
ctx.BasePath = "logout"
|
|
return www_logout(ctx, w, r)
|
|
} else if args[0] == "profile" {
|
|
ctx.BasePath = "profile"
|
|
return www_profile(ctx, w, r)
|
|
} else if args[0] == "register" {
|
|
ctx.BasePath = "register"
|
|
return www_register(ctx, w, r)
|
|
} else if args[0] == "reset" {
|
|
ctx.Template = "reset.tpl"
|
|
return ctx.www.tpl.ExecuteTemplate(w, "reset.tpl", ctx)
|
|
} else if args[0] == "avatar" {
|
|
ctx.BasePath = "avatar"
|
|
if len(args) < 2 {
|
|
return errors.New("Wrong request")
|
|
}
|
|
return www_avatar(ctx, w, r, args[1])
|
|
} else if ii.IsMsgId(args[0]) {
|
|
page := 0
|
|
ctx.BasePath = args[0]
|
|
if len(args) > 1 {
|
|
if args[1] == "reply" {
|
|
return www_reply(ctx, w, r, !(len(args) > 2 && args[2] == "new"))
|
|
} else if args[1] == "edit" {
|
|
return www_edit(ctx, w, r)
|
|
} else if args[1] == "blacklist" {
|
|
return www_blacklist(ctx, w, r)
|
|
} else if args[1] == "base64" {
|
|
return www_base64(ctx, w, r)
|
|
}
|
|
fmt.Sscanf(args[1], "%d", &page)
|
|
}
|
|
return www_topic(ctx, w, r, page)
|
|
} else if args[0] == "new" {
|
|
ctx.BasePath = ""
|
|
return www_new(ctx, w, r)
|
|
} else if args[0] == "to" {
|
|
page := 1
|
|
rss := false
|
|
if len(args) < 2 {
|
|
return errors.New("Wrong request")
|
|
}
|
|
if len(args) > 2 {
|
|
if args[2] == "rss" {
|
|
rss = true
|
|
} else {
|
|
fmt.Sscanf(args[2], "%d", &page)
|
|
}
|
|
}
|
|
ctx.BasePath = "to/" + args[1]
|
|
return www_query(ctx, w, r, ii.Query{To: args[1]}, page, rss)
|
|
} else if args[0] == "from" {
|
|
page := 1
|
|
rss := false
|
|
if len(args) < 2 {
|
|
return errors.New("Wrong request")
|
|
}
|
|
if len(args) > 2 {
|
|
if args[2] == "rss" {
|
|
rss = true
|
|
} else {
|
|
fmt.Sscanf(args[2], "%d", &page)
|
|
}
|
|
}
|
|
ctx.BasePath = "from/" + args[1]
|
|
return www_query(ctx, w, r, ii.Query{From: args[1]}, page, rss)
|
|
} else if args[0] == "echo" || args[0] == "echo+topics" {
|
|
page := 1
|
|
rss := false
|
|
if len(args) < 2 {
|
|
return errors.New("Wrong request")
|
|
}
|
|
if len(args) > 2 {
|
|
if args[2] == "rss" {
|
|
rss = true
|
|
} else {
|
|
fmt.Sscanf(args[2], "%d", &page)
|
|
}
|
|
}
|
|
q := ii.Query{Echo: args[1]}
|
|
if args[1] == "all" {
|
|
q.Echo = ""
|
|
}
|
|
ctx.Echo = q.Echo
|
|
q.Start = -PAGE_SIZE
|
|
if args[0] == "echo+topics" {
|
|
q.Repto = "!"
|
|
ctx.BasePath = "echo+topics/" + args[1]
|
|
} else {
|
|
ctx.BasePath = "echo/" + args[1]
|
|
}
|
|
return www_query(ctx, w, r, q, page, rss)
|
|
} else if ii.IsEcho(args[0]) {
|
|
page := 1
|
|
ctx.Echo = args[0]
|
|
ctx.BasePath = args[0]
|
|
if len(args) > 1 {
|
|
if args[1] == "new" {
|
|
ctx.BasePath = args[0]
|
|
return www_new(ctx, w, r)
|
|
}
|
|
fmt.Sscanf(args[1], "%d", &page)
|
|
}
|
|
return www_topics(ctx, w, r, page)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
fmt.Fprintf(w, "404\n")
|
|
}
|
|
return nil
|
|
}
|