openidec

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

web.go (28198B)


      1 package main
      2 
      3 import (
      4 	"bytes"
      5 	"encoding/base64"
      6 	"errors"
      7 	"fmt"
      8 	"html/template"
      9 	"image"
     10 	"image/png"
     11 	"net/http"
     12 	"regexp"
     13 	"sort"
     14 	"strings"
     15 	"time"
     16 	"encoding/hex"
     17 	"crypto/rand"
     18 
     19 	"git.openbsd.org.ru/vasyahacker/openidec/ii"
     20 )
     21 
     22 const PAGE_SIZE = 100
     23 
     24 type WebContext struct {
     25 	Echoes   []*ii.Echo
     26 	Topics   []*Topic
     27 	Topic    string
     28 	Msg      []*ii.Msg
     29 	Error    string
     30 	Echo     string
     31 	PfxPath  string
     32 	Page     int
     33 	Pages    int
     34 	Pager    []int
     35 	BasePath string
     36 	User     *ii.User
     37 	Echolist *ii.EDB
     38 	Selected string
     39 	Template string
     40 	Ref      string
     41 	Info     string
     42 	Sysname  string
     43 	Host     string
     44 	www      *WWW
     45 }
     46 
     47 func www_register(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
     48 	ii.Trace.Printf("www register")
     49 	switch r.Method {
     50 	case "GET":
     51 		ctx.Template = "register.tpl"
     52 		err := ctx.www.tpl.ExecuteTemplate(w, "register.tpl", ctx)
     53 		return err
     54 	case "POST":
     55 		udb := ctx.www.udb
     56 		if err := r.ParseForm(); err != nil {
     57 			ii.Error.Printf("Error in POST request: %s", err)
     58 			return err
     59 		}
     60 		token := r.FormValue("token")
     61 		if token != "" { /* edit form */
     62 			u := udb.UserInfo(token)
     63 			if u == nil {
     64 				ii.Error.Printf("Access denied")
     65 				return errors.New("Access denied")
     66 			}
     67 			old_password := r.FormValue("old_password")
     68 			if !udb.Auth(u.Name, old_password) {
     69 				ii.Error.Printf("Old password missmatch (%s)", u.Name)
     70 				return errors.New("Old password missmatch")
     71 			}
     72 			password := r.FormValue("password")
     73 			u.Secret = ii.MakeSecret(u.Name + password)
     74 			if err := udb.Edit(u); err != nil {
     75 				ii.Info.Printf("Can not edit user %s: %s", ctx.User.Name, err)
     76 				return err
     77 			}
     78 			http.Redirect(w, r, ctx.PfxPath+"/login", http.StatusSeeOther)
     79 			return nil
     80 		}
     81 		user := r.FormValue("username")
     82 		password := r.FormValue("password")
     83 		email := r.FormValue("email")
     84 
     85 		err := udb.Add(user, email, password)
     86 		if err != nil {
     87 			ii.Info.Printf("Can not register user %s: %s", user, err)
     88 			return err
     89 		}
     90 		ii.Info.Printf("Registered user: %s", user)
     91 		http.Redirect(w, r, ctx.PfxPath+"/login", http.StatusSeeOther)
     92 	default:
     93 		return nil
     94 	}
     95 	return nil
     96 }
     97 
     98 func www_login(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
     99 	ii.Trace.Printf("www login")
    100 	switch r.Method {
    101 	case "GET":
    102 		ctx.Template = "login.tpl"
    103 		err := ctx.www.tpl.ExecuteTemplate(w, "login.tpl", ctx)
    104 		return err
    105 	case "POST":
    106 		if err := r.ParseForm(); err != nil {
    107 			ii.Error.Printf("Error in POST request: %s", err)
    108 			return err
    109 		}
    110 		user := r.FormValue("username")
    111 		password := r.FormValue("password")
    112 		udb := ctx.www.udb
    113 		if !udb.Auth(user, password) {
    114 			ii.Info.Printf("Access denied for user: %s", user)
    115 			return errors.New("Access denied")
    116 		}
    117 		exp := time.Now().Add(10 * 365 * 24 * time.Hour)
    118 		u := udb.UserInfoName(user)
    119 		if len(u.Token) == 0 {
    120 			token := generateSecureToken(16)
    121 			u.Token = token
    122 			udb.Tokens[token] = user
    123 			udb.Names[user] = *u
    124 		}
    125 		cookie := http.Cookie{Name: "token", Value: u.Token, Expires: exp}
    126 		http.SetCookie(w, &cookie)
    127 		ii.Info.Printf("User logged in: %s\n", user)
    128 		http.Redirect(w, r, ctx.PfxPath+"/", http.StatusSeeOther)
    129 		return nil
    130 	}
    131 	return errors.New("Wrong method")
    132 }
    133 
    134 func www_profile(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    135 	ii.Trace.Printf("www profile")
    136 	if ctx.User.Name == "" {
    137 		ii.Error.Printf("Access denied")
    138 		return errors.New("Access denied")
    139 	}
    140 	ctx.Selected = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id)
    141 	ava, _ := ctx.User.Tags.Get("avatar")
    142 	if ava != "" {
    143 		if data, err := base64.URLEncoding.DecodeString(ava); err == nil {
    144 			ctx.Info = string(data)
    145 		}
    146 	}
    147 	ctx.Template = "profile.tpl"
    148 	err := ctx.www.tpl.ExecuteTemplate(w, "profile.tpl", ctx)
    149 	return err
    150 }
    151 
    152 func www_logout(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    153 	ii.Trace.Printf("www logout: %s", ctx.User.Name)
    154 	if ctx.User.Name == "" {
    155 		ii.Error.Printf("Access denied")
    156 		return errors.New("Access denied")
    157 	}
    158 	cookie, err := r.Cookie("token")
    159 	if err == nil {
    160 		token := cookie.Value
    161 		udb := ctx.www.udb
    162 		if udb.Access(token) {
    163 			//ui := udb.UserInfoName(ctx.User.Name)
    164 			//ui.Token = ""
    165 			ctx.User.Token = ""
    166 			udb.Names[ctx.User.Name] = *ctx.User
    167 			delete(udb.Tokens, token)
    168 		}
    169 	}
    170 	rmcookie := http.Cookie{Name: "token", Value: "", Expires: time.Unix(0, 0)}
    171 	http.SetCookie(w, &rmcookie)
    172 	http.Redirect(w, r, ctx.PfxPath+"/", http.StatusSeeOther)
    173 	return nil
    174 }
    175 
    176 func www_index(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    177 	ii.Trace.Printf("www index")
    178 	ctx.Echoes = ctx.www.db.Echoes(nil, ii.Query{User: *ctx.User})
    179 	ctx.Template = "index.tpl"
    180 	err := ctx.www.tpl.ExecuteTemplate(w, "index.tpl", ctx)
    181 	return err
    182 }
    183 
    184 func parse_ava(txt string) *image.RGBA {
    185 	txt = msg_clean(txt)
    186 	lines := strings.Split(txt, "\n")
    187 	img, _ := ParseXpm(lines)
    188 	return img
    189 }
    190 
    191 var magicTable = map[string]string{
    192 	"\xff\xd8\xff":      "image/jpeg",
    193 	"\x89PNG\r\n\x1a\n": "image/png",
    194 	"GIF87a":            "image/gif",
    195 	"GIF89a":            "image/gif",
    196 }
    197 
    198 func check_image(incipit []byte) string {
    199 	incipitStr := string(incipit)
    200 	for magic, mime := range magicTable {
    201 		if strings.HasPrefix(incipitStr, magic) {
    202 			return mime
    203 		}
    204 	}
    205 	return ""
    206 }
    207 
    208 func www_base64(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    209 	id := ctx.BasePath
    210 	m := ctx.www.db.Get(id)
    211 	if m == nil {
    212 		return errors.New("No such message")
    213 	}
    214 	lines := strings.Split(msg_clean(m.Text), "\n")
    215 	start := false
    216 	b64 := ""
    217 	fname := ""
    218 	pre := false
    219 	for _, v := range lines {
    220 		if !start && strings.Trim(v, " ") == "====" {
    221 			if !pre {
    222 				pre = true
    223 				continue
    224 			}
    225 			pre = false
    226 			continue
    227 		}
    228 		if pre {
    229 			continue
    230 		}
    231 		if !start && !strings.HasPrefix(v, "@base64:") {
    232 			continue
    233 		}
    234 		if start {
    235 			v = strings.Replace(v, " ", "", -1)
    236 			if !base64Regex.MatchString(v) {
    237 				break
    238 			}
    239 			b64 += v
    240 			continue
    241 		}
    242 		v = strings.TrimPrefix(v, "@base64:")
    243 		v = strings.Trim(v, " ")
    244 		fname = v
    245 		if fname == "" {
    246 			fname = "file"
    247 		}
    248 		start = true
    249 	}
    250 	if b64 == "" {
    251 		return nil
    252 	}
    253 	b, err := base64.StdEncoding.DecodeString(b64)
    254 	if err != nil {
    255 		if b, err = base64.RawStdEncoding.DecodeString(b64); err != nil {
    256 			if b, err = base64.URLEncoding.DecodeString(b64); err != nil {
    257 				return err
    258 			}
    259 		}
    260 	}
    261 	//	w.Header().Set("Content-Type", "image/jpeg")
    262 	if check_image(b) != "" {
    263 		w.Header().Set("Content-Disposition", "inline")
    264 	} else {
    265 		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fname))
    266 	}
    267 	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b)))
    268 	_, err = w.Write(b)
    269 	return err
    270 }
    271 func www_avatar(ctx *WebContext, w http.ResponseWriter, r *http.Request, user string) error {
    272 	if r.Method == "POST" { /* upload avatar */
    273 		if ctx.User.Name == "" || ctx.User.Name != user {
    274 			ii.Error.Printf("Access denied")
    275 			return errors.New("Access denied")
    276 		}
    277 		if err := r.ParseForm(); err != nil {
    278 			ii.Error.Printf("Error in POST request: %s", err)
    279 			return err
    280 		}
    281 		ava := r.FormValue("avatar")
    282 		if len(ava) > 2048 {
    283 			ii.Error.Printf("Avatar is too big.")
    284 			return errors.New("Avatar is too big (>2048 bytes)")
    285 		}
    286 		if ava == "" {
    287 			ii.Trace.Printf("Delete avatar for %s", ctx.User.Name)
    288 			ctx.User.Tags.Del("avatar")
    289 		} else {
    290 			img := parse_ava(ava)
    291 			if img == nil {
    292 				ii.Error.Printf("Wrong xpm format for avatar: " + user)
    293 				return errors.New("Wrong xpm format")
    294 			}
    295 			b64 := base64.URLEncoding.EncodeToString([]byte(ava))
    296 			ii.Trace.Printf("New avatar for %s: %s", ctx.User.Name, b64)
    297 			if err := ctx.User.Tags.Add("avatar/" + b64); err != nil {
    298 				return err
    299 			}
    300 		}
    301 		if err := ctx.www.udb.Edit(ctx.User); err != nil {
    302 			ii.Error.Printf("Error saving avatar: " + user)
    303 			return errors.New("Error saving avatar")
    304 		}
    305 		http.Redirect(w, r, ctx.PfxPath+"/profile", http.StatusSeeOther)
    306 		return nil
    307 	}
    308 	// var id int32
    309 	// if !strings.HasPrefix(user, ctx.www.db.Name) {
    310 	// 	return nil
    311 	// }
    312 	// user = strings.TrimPrefix(user, ctx.www.db.Name)
    313 	// user = strings.TrimPrefix(user, ",")
    314 	// if _, err := fmt.Sscanf(user, "%d", &id); err != nil {
    315 	// 	return nil
    316 	// }
    317 	// u := ctx.www.udb.UserInfoId(id)
    318 	u := ctx.www.udb.UserInfoName(user)
    319 	if u == nil {
    320 		return nil
    321 	}
    322 	ava, _ := u.Tags.Get("avatar")
    323 	if ava == "" {
    324 		return nil
    325 	}
    326 	if data, err := base64.URLEncoding.DecodeString(ava); err == nil {
    327 		img := parse_ava(string(data))
    328 		if img == nil {
    329 			ii.Error.Printf("Wrong xpm in avatar: %s\n", u.Name)
    330 			return nil
    331 		}
    332 		b := new(bytes.Buffer)
    333 		if err := png.Encode(b, img); err == nil {
    334 			w.Header().Set("Content-Type", "image/png")
    335 			w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b.Bytes())))
    336 			if _, err := w.Write(b.Bytes()); err != nil {
    337 				return nil
    338 			}
    339 			return nil
    340 		}
    341 		ii.Error.Printf("Can't encode avatar in png: %s\n", u.Name)
    342 	} else {
    343 		ii.Error.Printf("Can't decode avatar: %s\n", u.Name)
    344 	}
    345 	return nil
    346 }
    347 
    348 type Topic struct {
    349 	Ids   []string
    350 	Count int
    351 	Last  *ii.MsgInfo
    352 	Head  *ii.Msg
    353 	Tail  *ii.Msg
    354 }
    355 
    356 func makePager(ctx *WebContext, count int, page int) int {
    357 	ctx.Pages = count / PAGE_SIZE
    358 	if count%PAGE_SIZE != 0 {
    359 		ctx.Pages++
    360 	}
    361 	if page == 0 {
    362 		page++
    363 	} else if page < 0 {
    364 		page = ctx.Pages + page + 1
    365 	}
    366 	start := (page - 1) * PAGE_SIZE
    367 	if start < 0 {
    368 		start = 0
    369 		page = 1
    370 	}
    371 	ctx.Page = page
    372 	if ctx.Pages > 1 {
    373 		for i := 1; i <= ctx.Pages; i++ {
    374 			ctx.Pager = append(ctx.Pager, i)
    375 		}
    376 	}
    377 	return start
    378 }
    379 
    380 func Select(ctx *WebContext, q ii.Query) []string {
    381 	q.User = *ctx.User
    382 	return ctx.www.db.SelectIDS(q)
    383 }
    384 
    385 func trunc(str string, limit int) string {
    386 	result := []rune(str)
    387 	if len(result) > limit {
    388 		return string(result[:limit])
    389 	}
    390 	return str
    391 }
    392 
    393 func www_query(ctx *WebContext, w http.ResponseWriter, r *http.Request, q ii.Query, page int, rss bool) error {
    394 	db := ctx.www.db
    395 	req := ctx.BasePath
    396 
    397 	if rss {
    398 		q.Start = -PAGE_SIZE
    399 	}
    400 	mis := db.LookupIDS(Select(ctx, q))
    401 	ii.Trace.Printf("www query")
    402 
    403 	sort.SliceStable(mis, func(i, j int) bool {
    404 		return mis[i].Num > mis[j].Num
    405 	})
    406 	count := len(mis)
    407 	start := makePager(ctx, count, page)
    408 	nr := PAGE_SIZE
    409 	for i := start; i < count && nr > 0; i++ {
    410 		m := db.GetFast(mis[i].Id)
    411 		if m == nil {
    412 			ii.Error.Printf("Can't get msg: %s\n", mis[i].Id)
    413 			continue
    414 		}
    415 		ctx.Msg = append(ctx.Msg, m)
    416 		nr--
    417 	}
    418 	if rss {
    419 		ctx.Topic = db.Name + " :: " + req
    420 		fmt.Fprintf(w,
    421 			`<?xml version="1.0" encoding="UTF-8"?>
    422 	<rss version="2.0"
    423 	xmlns:content="http://purl.org/rss/1.0/modules/content/"
    424 	xmlns:dc="http://purl.org/dc/elements/1.1/"
    425 	xmlns:media="http://search.yahoo.com/mrss/"
    426 	xmlns:atom="http://www.w3.org/2005/Atom"
    427 	xmlns:georss="http://www.georss.org/georss">
    428 	<channel>
    429 	<title>%s</title>
    430 	<link>%s/%s</link>
    431 	<description>
    432 	%s
    433 	</description>
    434 	<language>ru</language>
    435 `,
    436 			str_esc(ctx.Topic), ctx.www.Host, ctx.BasePath, str_esc(ctx.Topic))
    437 		for _, m := range ctx.Msg {
    438 			fmt.Fprintf(w,
    439 				`<item><title>%s</title><guid>%s</guid><pubDate>%s</pubDate><author>%s</author><link>%s/%s#%s</link>
    440 		<description>
    441 		%s...
    442 		</description>
    443 		<content:encoded>
    444 <![CDATA[
    445 %s
    446 %s
    447 ]]>
    448 </content:encoded></item>
    449 `,
    450 				str_esc(m.Subj), m.MsgId, time.Unix(m.Date, 0).Format("2006-01-02 15:04:05"),
    451 				str_esc(m.From), ctx.www.Host+ctx.PfxPath, m.MsgId, m.MsgId,
    452 				str_esc(trunc(m.Text, 280)),
    453 				fmt.Sprintf("%s -> %s<br><br>", m.From, m.To),
    454 				msg_text(m))
    455 		}
    456 		fmt.Fprintf(w, `</channel></rss>
    457 `)
    458 		return nil
    459 	}
    460 	ctx.Template = "query.tpl"
    461 	return ctx.www.tpl.ExecuteTemplate(w, "query.tpl", ctx)
    462 }
    463 
    464 func www_topics(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error {
    465 	db := ctx.www.db
    466 	echo := ctx.Echo
    467 	mis := db.LookupIDS(Select(ctx, ii.Query{Echo: echo}))
    468 	ii.Trace.Printf("www topics: %s", echo)
    469 	topicsIds := db.GetTopics(mis)
    470 	var topics []*Topic
    471 	ii.Trace.Printf("Start to generate topics")
    472 
    473 	db.Sync.RLock()
    474 	defer db.Sync.RUnlock()
    475 	if err := db.LoadIndex(); err != nil {
    476 		return err
    477 	}
    478 	for _, t := range topicsIds {
    479 		topic := Topic{}
    480 		topic.Ids = t
    481 		topic.Count = len(topic.Ids) - 1
    482 		if ctx.PfxPath == "/blog" {
    483 			topic.Last = db.LookupFast(topic.Ids[0], false)
    484 		} else {
    485 			topic.Last = db.LookupFast(topic.Ids[topic.Count], false)
    486 		}
    487 		if topic.Last == nil {
    488 			ii.Error.Printf("Skip wrong message: %s\n", t[0])
    489 			continue
    490 		}
    491 		topics = append(topics, &topic)
    492 	}
    493 	sort.SliceStable(topics, func(i, j int) bool {
    494 		return topics[i].Last.Num > topics[j].Last.Num
    495 	})
    496 	tcount := len(topics)
    497 	start := makePager(ctx, tcount, page)
    498 	nr := PAGE_SIZE
    499 	for i := start; i < tcount && nr > 0; i++ {
    500 		t := topics[i]
    501 		t.Head = db.GetFast(t.Ids[0])
    502 		t.Tail = db.GetFast(t.Ids[t.Count])
    503 		if t.Head == nil || t.Tail == nil {
    504 			ii.Error.Printf("Skip wrong message: %s\n", t.Ids[0])
    505 			continue
    506 		}
    507 		ctx.Topics = append(ctx.Topics, topics[i])
    508 		nr--
    509 	}
    510 	ii.Trace.Printf("Stop to generate topics")
    511 
    512 	if ctx.PfxPath == "/blog" {
    513 		ctx.Template = "blog.tpl"
    514 		err := ctx.www.tpl.ExecuteTemplate(w, "blog.tpl", ctx)
    515 		return err
    516 	}
    517 	ctx.Template = "topics.tpl"
    518 	err := ctx.www.tpl.ExecuteTemplate(w, "topics.tpl", ctx)
    519 	return err
    520 }
    521 
    522 func www_topic(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error {
    523 	id := ctx.BasePath
    524 	db := ctx.www.db
    525 
    526 	mi := db.Lookup(id)
    527 	if mi == nil {
    528 		return errors.New("No such message")
    529 	}
    530 
    531 	if !db.Access(mi, ctx.User) {
    532 		return errors.New("Access denied")
    533 	}
    534 
    535 	if page == 0 {
    536 		ctx.Selected = id
    537 	}
    538 	ctx.Echo = mi.Echo
    539 	mis := db.LookupIDS(Select(ctx, ii.Query{Echo: mi.Echo}))
    540 
    541 	topics := db.GetTopics(mis)
    542 	topic := mi.Topic
    543 	ctx.Topic = topic
    544 	ids := topics[topic]
    545 
    546 	if len(ids) == 0 {
    547 		ids = append(ids, id)
    548 	} else if topic != mi.Id {
    549 		for k, v := range ids {
    550 			if v == mi.Id {
    551 				page = k/PAGE_SIZE + 1
    552 				ctx.Selected = mi.Id
    553 				break
    554 			}
    555 		}
    556 	}
    557 	ii.Trace.Printf("www topic: %s", id)
    558 	start := makePager(ctx, len(ids), page)
    559 	nr := PAGE_SIZE
    560 	for i := start; i < len(ids) && nr > 0; i++ {
    561 		id := ids[i]
    562 		m := db.Get(id)
    563 		if m == nil {
    564 			ii.Error.Printf("Skip wrong message: %s", id)
    565 			continue
    566 		}
    567 		ctx.Msg = append(ctx.Msg, m)
    568 		nr--
    569 	}
    570 	ctx.Template = "topic.tpl"
    571 	err := ctx.www.tpl.ExecuteTemplate(w, "topic.tpl", ctx)
    572 	return err
    573 }
    574 
    575 func www_blacklist(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    576 	id := ctx.BasePath
    577 	m := ctx.www.db.Get(id)
    578 	ii.Trace.Printf("www blacklist: %s", id)
    579 	if m == nil {
    580 		ii.Error.Printf("No such msg: %s", id)
    581 		return errors.New("No such msg")
    582 	}
    583 	if !msg_access(ctx.www, *m, *ctx.User) {
    584 		ii.Error.Printf("Access denied")
    585 		return errors.New("Access denied")
    586 	}
    587 	err := ctx.www.db.Blacklist(m)
    588 	if err != nil {
    589 		ii.Error.Printf("Error blacklisting: %s", id)
    590 		return err
    591 	}
    592 	http.Redirect(w, r, ctx.PfxPath+"/", http.StatusSeeOther)
    593 	return nil
    594 }
    595 
    596 func www_edit(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    597 	id := ctx.BasePath
    598 	switch r.Method {
    599 	case "GET":
    600 		m := ctx.www.db.Get(id)
    601 		if m == nil {
    602 			ii.Error.Printf("No such msg: %s", id)
    603 			return errors.New("No such msg")
    604 		}
    605 		msg := *m
    606 		ln := strings.Split(msg_clean(msg.Text), "\n")
    607 		if len(ln) > 0 {
    608 			if strings.HasPrefix(ln[len(ln)-1], "P.S. Edited: ") {
    609 				msg.Text = strings.Join(ln[:len(ln)-1], "\n")
    610 			}
    611 		}
    612 		msg.Text = msg.Text + "\nP.S. Edited: " + time.Now().Format("2006-01-02 15:04:05")
    613 		ctx.Msg = append(ctx.Msg, &msg)
    614 		ctx.Template = "edit.tpl"
    615 		err := ctx.www.tpl.ExecuteTemplate(w, "edit.tpl", ctx)
    616 		return err
    617 	case "POST":
    618 		ctx.BasePath = ""
    619 		return www_new(ctx, w, r)
    620 	}
    621 	return nil
    622 }
    623 
    624 func www_new(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    625 	echo := ctx.BasePath
    626 	ctx.Echo = echo
    627 
    628 	switch r.Method {
    629 	case "GET":
    630 		ctx.Template = "new.tpl"
    631 		err := ctx.www.tpl.ExecuteTemplate(w, "new.tpl", ctx)
    632 		return err
    633 	case "POST":
    634 		edit := (echo == "")
    635 		ii.Trace.Printf("www new topic in %s", echo)
    636 		if err := r.ParseForm(); err != nil {
    637 			ii.Error.Printf("Error in POST request: %s", err)
    638 			return err
    639 		}
    640 		if ctx.User.Name == "" {
    641 			ii.Error.Printf("Access denied")
    642 			return errors.New("Access denied")
    643 		}
    644 		subj := r.FormValue("subj")
    645 		to := r.FormValue("to")
    646 		msg := r.FormValue("msg")
    647 		repto := r.FormValue("repto")
    648 		id := r.FormValue("id")
    649 		if repto == id {
    650 			repto = ""
    651 		}
    652 		newecho := r.FormValue("echo")
    653 		if newecho != "" {
    654 			echo = newecho
    655 		}
    656 		if !ctx.www.edb.Allowed(echo) && ctx.User.Id != 1 {
    657 			ii.Error.Printf("This echo is disallowed")
    658 			return errors.New("This echo is disallowed")
    659 		}
    660 		action := r.FormValue("action")
    661 		text := fmt.Sprintf("%s\n%s\n%s\n\n%s", echo, to, subj, msg)
    662 		m, err := ii.DecodeMsgline(text, false)
    663 		if err != nil {
    664 			ii.Error.Printf("Error while posting new topic: %s", err)
    665 			return err
    666 		}
    667 		m.From = ctx.User.Name
    668 		m.Addr = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id)
    669 		if repto != "" {
    670 			if err := m.Tags.Add("repto/" + repto); err != nil {
    671 				return err
    672 			}
    673 		}
    674 		if id != "" {
    675 			om := ctx.www.db.Get(id)
    676 			if (om == nil || m.Addr != om.Addr) && ctx.User.Id != 1 {
    677 				ii.Error.Printf("Access denied")
    678 				return errors.New("Access denied")
    679 			}
    680 			m.Date = om.Date
    681 			m.MsgId = id
    682 			m.From = om.From
    683 			m.Addr = om.Addr
    684 		}
    685 		if action == "Submit" { // submit
    686 			if edit {
    687 				err = ctx.www.db.Edit(m)
    688 			} else {
    689 				err = ctx.www.db.Store(m)
    690 			}
    691 			if err != nil {
    692 				ii.Error.Printf("Error while storig new topic %s: %s", m.MsgId, err)
    693 				return err
    694 			}
    695 			http.Redirect(w, r, ctx.PfxPath+"/"+m.MsgId+"#"+m.MsgId, http.StatusSeeOther)
    696 			return nil
    697 		}
    698 		if !edit {
    699 			m.MsgId = ""
    700 		}
    701 		ctx.Msg = append(ctx.Msg, m)
    702 		ctx.Template = "preview.tpl"
    703 		err = ctx.www.tpl.ExecuteTemplate(w, "preview.tpl", ctx)
    704 		return err
    705 	}
    706 	return nil
    707 }
    708 
    709 func www_reply(ctx *WebContext, w http.ResponseWriter, r *http.Request, quote bool) error {
    710 	id := ctx.BasePath
    711 	m := ctx.www.db.Get(id)
    712 	if m == nil {
    713 		ii.Error.Printf("No such msg: %s", id)
    714 		return errors.New("No such msg")
    715 	}
    716 	msg := *m
    717 	msg.To = msg.From
    718 	msg.Subj = "Re: " + strings.TrimPrefix(msg.Subj, "Re: ")
    719 	if err := msg.Tags.Add("repto/" + id); err != nil {
    720 		return err
    721 	}
    722 	if quote {
    723 		msg.Text = msg_quote(msg.Text, msg.From)
    724 	} else {
    725 		msg.Text = ""
    726 	}
    727 	ctx.Msg = append(ctx.Msg, &msg)
    728 	ctx.Echo = msg.Echo
    729 	ctx.Template = "reply.tpl"
    730 	err := ctx.www.tpl.ExecuteTemplate(w, "reply.tpl", ctx)
    731 	return err
    732 }
    733 
    734 func str_esc(l string) string {
    735 	l = strings.Replace(l, "&", "&amp;", -1)
    736 	l = strings.Replace(l, "<", "&lt;", -1)
    737 	l = strings.Replace(l, ">", "&gt;", -1)
    738 	return l
    739 }
    740 
    741 var quoteRegex = regexp.MustCompile("^[^ >]*>")
    742 var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`)
    743 var url2Regex = regexp.MustCompile(`{{{href=[0-9]+}}}`)
    744 var urlIIRegex = regexp.MustCompile(`ii://[a-zA-Z0-9_\-.]+`)
    745 var base64Regex = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
    746 
    747 func msg_clean(txt string) string {
    748 	txt = strings.Replace(txt, "\r", "", -1)
    749 	txt = strings.TrimLeft(txt, "\n")
    750 	txt = strings.TrimRight(txt, "\n")
    751 	return txt
    752 }
    753 func msg_quote(txt string, from string) string {
    754 	txt = msg_clean(txt)
    755 	f := ""
    756 	names := strings.Split(from, " ")
    757 	if len(names) >= 2 {
    758 		from = fmt.Sprintf("%v%v",
    759 			string([]rune(names[0])[0]),
    760 			string([]rune(names[1])[0]))
    761 	}
    762 	for _, l := range strings.Split(txt, "\n") {
    763 		if strings.Trim(l, " ") == "" {
    764 			f += l + "\n"
    765 			continue
    766 		}
    767 		if quoteRegex.MatchString(l) {
    768 			s := strings.Index(l, ">")
    769 			f += l[:s] + ">>" + l[s+1:] + "\n"
    770 		} else {
    771 			f += from + "> " + l + "\n"
    772 		}
    773 	}
    774 	return f
    775 }
    776 
    777 func ReverseStr(s string) string {
    778 	runes := []rune(s)
    779 	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
    780 		runes[i], runes[j] = runes[j], runes[i]
    781 	}
    782 	return string(runes)
    783 }
    784 
    785 func msg_esc(l string) string {
    786 	var links []string
    787 	link := 0
    788 	l = string(urlIIRegex.ReplaceAllFunc([]byte(l),
    789 		func(line []byte) []byte {
    790 			s := string(line)
    791 			url := strings.TrimPrefix(s, "ii://")
    792 			links = append(links, fmt.Sprintf(`<a href="/%s#%s" class="url">%s</a>`,
    793 				url, url, str_esc(s)))
    794 			link++
    795 			return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1))
    796 		}))
    797 	l = string(urlRegex.ReplaceAllFunc([]byte(l),
    798 		func(line []byte) []byte {
    799 			s := string(line)
    800 			links = append(links, fmt.Sprintf(`<a href="%s" class="url">%s</a>`,
    801 				s, str_esc(s)))
    802 			link++
    803 			return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1))
    804 		}))
    805 	l = str_esc(l)
    806 	l = string(url2Regex.ReplaceAllFunc([]byte(l),
    807 		func(line []byte) []byte {
    808 			s := string(line)
    809 			var n int
    810 			fmt.Sscanf(s, "{{{href=%d}}}", &n)
    811 			return []byte(links[n])
    812 		}))
    813 
    814 	return l
    815 }
    816 
    817 func msg_text(m *ii.Msg) string {
    818 	return msg_trunc(m, 0, "")
    819 }
    820 
    821 func msg_trunc(m *ii.Msg, maxlen int, more string) string {
    822 	if m == nil {
    823 		return ""
    824 	}
    825 	txt := m.Text
    826 	txt = msg_clean(txt)
    827 	f := ""
    828 	pre := false
    829 	skip := 0
    830 	lines := strings.Split(txt, "\n")
    831 	for k, l := range lines {
    832 		if skip > 0 {
    833 			skip--
    834 			continue
    835 		}
    836 		if strings.Trim(l, " ") == "====" {
    837 			if !pre {
    838 				pre = true
    839 				f += "<pre class=\"code\">\n"
    840 				continue
    841 			}
    842 			pre = false
    843 			f += "</pre>\n"
    844 			continue
    845 		}
    846 		if pre {
    847 			f += str_esc(l) + "\n"
    848 			continue
    849 		}
    850 		if strings.HasPrefix(l, "/* XPM */") || strings.HasPrefix(l, "! XPM2") {
    851 			var img *image.RGBA
    852 			img, skip = ParseXpm(lines[k:])
    853 			if img != nil {
    854 				skip--
    855 				/* embed xpm */
    856 				b := new(bytes.Buffer)
    857 				if err := png.Encode(b, img); err == nil {
    858 					b64 := base64.StdEncoding.EncodeToString(b.Bytes())
    859 					l = fmt.Sprintf("<img class=\"img\" src=\"data:image/png;base64,%s\"><br>\n",
    860 						b64)
    861 					f += l
    862 					continue
    863 				}
    864 			}
    865 			skip = 0
    866 			l = msg_esc(l)
    867 		} else if strings.HasPrefix(l, "P.S.") || strings.HasPrefix(l, "PS:") ||
    868 			strings.HasPrefix(l, "//") || strings.HasPrefix(l, "+++ ") {
    869 			l = fmt.Sprintf("<span class=\"comment\">%s</span>", str_esc(l))
    870 		} else if strings.HasPrefix(l, "# ") || strings.HasPrefix(l, "= ") ||
    871 			strings.HasPrefix(l, "## ") || strings.HasPrefix(l, "== ") ||
    872 			strings.HasPrefix(l, "### ") || strings.HasPrefix(l, "=== ") {
    873 			l = fmt.Sprintf("<span class=\"header\">%s</span>", str_esc(l))
    874 		} else if strings.HasPrefix(l, "@spoiler:") {
    875 			l = fmt.Sprintf("<span class=\"spoiler\">%s</span>", str_esc(ReverseStr(l)))
    876 		} else if quoteRegex.MatchString(l) {
    877 			l = fmt.Sprintf("<span class=\"quote\">%s</span>", str_esc(l))
    878 		} else if strings.HasPrefix(l, "@base64:") {
    879 			fname := strings.TrimPrefix(l, "@base64:")
    880 			fname = strings.Trim(fname, " ")
    881 			if fname == "" {
    882 				fname = "file"
    883 			}
    884 			f += fmt.Sprintf("<a class=\"attach\" href=\"/%s/base64\">%s</a><br>\n", m.MsgId, str_esc(fname))
    885 			return f
    886 		} else {
    887 			l = msg_esc(l)
    888 		}
    889 		f += l
    890 		if maxlen > 0 && len(f) > maxlen {
    891 			f += more + "<br>\n"
    892 			break
    893 		} else {
    894 			f += "<br>\n"
    895 		}
    896 	}
    897 	if pre {
    898 		f += "</pre>\n"
    899 	}
    900 	return f
    901 }
    902 
    903 func msg_access(www *WWW, m ii.Msg, u ii.User) bool {
    904 	addr := fmt.Sprintf("%s,%d", www.db.Name, u.Id)
    905 	return addr == m.Addr || u.Id == 1
    906 }
    907 
    908 func WebInit(www *WWW) {
    909 	funcMap := template.FuncMap{
    910 		"fdate": func(date int64) template.HTML {
    911 			if time.Now().Unix()-date < 60*60*24 {
    912 				return template.HTML("<span class='today'>" + time.Unix(date, 0).Format("2006-01-02 15:04:05") + "</span>")
    913 			}
    914 			return template.HTML(time.Unix(date, 0).Format("2006-01-02 15:04:05"))
    915 		},
    916 		"msg_text": func(m *ii.Msg) template.HTML {
    917 			return template.HTML(msg_text(m))
    918 		},
    919 		"msg_trunc": func(m *ii.Msg, len int, more string) template.HTML {
    920 			return template.HTML(msg_trunc(m, len, more))
    921 		},
    922 		"repto": func(m ii.Msg) string {
    923 			r, _ := m.Tag("repto")
    924 			if r == "" {
    925 				return m.MsgId
    926 			}
    927 			return r
    928 		},
    929 		"msg_quote": msg_quote,
    930 		"msg_access": func(m ii.Msg, u ii.User) bool {
    931 			return msg_access(www, m, u)
    932 		},
    933 		"is_even": func(i int) bool {
    934 			return i%2 == 0
    935 		},
    936 		"unescape": func(s string) template.HTML {
    937 			return template.HTML(s)
    938 		},
    939 		"has_avatar": func(user string) bool {
    940 			ui := www.udb.UserInfoName(user)
    941 			if ui != nil {
    942 				_, ok := ui.Tags.Get("avatar")
    943 				return ok
    944 			}
    945 			return false
    946 		},
    947 	}
    948 
    949 	www.tpl = template.Must(
    950 		template.New("main").Funcs(funcMap).ParseGlob(www.tplp + "/*.tpl"))
    951 }
    952 
    953 func handleErr(ctx *WebContext, w http.ResponseWriter, err error) {
    954 	ctx.Error = err.Error()
    955 	ctx.Template = "error.tpl"
    956 	if err := ctx.www.tpl.ExecuteTemplate(w, "error.tpl", ctx); err != nil {
    957 		// FIXME: correctly handle this error
    958 		panic(err)
    959 	}
    960 }
    961 
    962 func handleWWW(www *WWW, w http.ResponseWriter, r *http.Request) {
    963 	var ctx WebContext
    964 	var user *ii.User = &ii.User{}
    965 	ctx.User = user
    966 	ctx.www = www
    967 	ctx.Sysname = www.db.Name
    968 	ctx.Host = www.Host
    969 	err := www.udb.LoadUsers()
    970 	if err != nil {
    971 		handleErr(&ctx, w, err)
    972 	}
    973 	err = _handleWWW(&ctx, w, r)
    974 	if err != nil {
    975 		handleErr(&ctx, w, err)
    976 	}
    977 }
    978 
    979 func _handleWWW(ctx *WebContext, w http.ResponseWriter, r *http.Request) error {
    980 	cookie, err := r.Cookie("token")
    981 	if err == nil {
    982 		udb := ctx.www.udb
    983 		if udb.Access(cookie.Value) {
    984 			if user := udb.UserInfo(cookie.Value); user != nil {
    985 				ctx.User = user
    986 			}
    987 		}
    988 	}
    989 	ii.Trace.Printf("[%s] GET %s", ctx.User.Name, r.URL.Path)
    990 	path := strings.TrimPrefix(r.URL.Path, "/")
    991 	args := strings.Split(path, "/")
    992 	ctx.Echolist = ctx.www.edb
    993 	ctx.Ref = r.Header.Get("Referer")
    994 	if len(args) > 1 && args[0] == "blog" {
    995 		ctx.PfxPath = "/blog"
    996 		args = args[1:]
    997 	}
    998 	if args[0] == "" {
    999 		ctx.BasePath = ""
   1000 		return www_index(ctx, w, r)
   1001 	} else if args[0] == "login" {
   1002 		ctx.BasePath = "login"
   1003 		return www_login(ctx, w, r)
   1004 	} else if args[0] == "logout" {
   1005 		ctx.BasePath = "logout"
   1006 		return www_logout(ctx, w, r)
   1007 	} else if args[0] == "profile" {
   1008 		ctx.BasePath = "profile"
   1009 		return www_profile(ctx, w, r)
   1010 	} else if !ctx.www.noreg && args[0] == "register" {
   1011 		ctx.BasePath = "register"
   1012 		return www_register(ctx, w, r)
   1013 	} else if args[0] == "reset" {
   1014 		ctx.Template = "reset.tpl"
   1015 		return ctx.www.tpl.ExecuteTemplate(w, "reset.tpl", ctx)
   1016 	} else if args[0] == "avatar" {
   1017 		ctx.BasePath = "avatar"
   1018 		if len(args) < 2 {
   1019 			return errors.New("Wrong request")
   1020 		}
   1021 		return www_avatar(ctx, w, r, args[1])
   1022 	} else if ii.IsMsgId(args[0]) {
   1023 		page := 0
   1024 		ctx.BasePath = args[0]
   1025 		if len(args) > 1 {
   1026 			if args[1] == "reply" {
   1027 				return www_reply(ctx, w, r, !(len(args) > 2 && args[2] == "new"))
   1028 			} else if args[1] == "edit" {
   1029 				return www_edit(ctx, w, r)
   1030 			} else if args[1] == "blacklist" {
   1031 				return www_blacklist(ctx, w, r)
   1032 			} else if args[1] == "base64" {
   1033 				return www_base64(ctx, w, r)
   1034 			}
   1035 			fmt.Sscanf(args[1], "%d", &page)
   1036 		}
   1037 		return www_topic(ctx, w, r, page)
   1038 	} else if args[0] == "new" {
   1039 		ctx.BasePath = ""
   1040 		return www_new(ctx, w, r)
   1041 	} else if args[0] == "to" {
   1042 		page := 1
   1043 		rss := false
   1044 		if len(args) < 2 {
   1045 			return errors.New("Wrong request")
   1046 		}
   1047 		if len(args) > 2 {
   1048 			if args[2] == "rss" {
   1049 				rss = true
   1050 			} else {
   1051 				fmt.Sscanf(args[2], "%d", &page)
   1052 			}
   1053 		}
   1054 		ctx.BasePath = "to/" + args[1]
   1055 		return www_query(ctx, w, r, ii.Query{To: args[1]}, page, rss)
   1056 	} else if args[0] == "from" {
   1057 		page := 1
   1058 		rss := false
   1059 		if len(args) < 2 {
   1060 			return errors.New("Wrong request")
   1061 		}
   1062 		if len(args) > 2 {
   1063 			if args[2] == "rss" {
   1064 				rss = true
   1065 			} else {
   1066 				fmt.Sscanf(args[2], "%d", &page)
   1067 			}
   1068 		}
   1069 		ctx.BasePath = "from/" + args[1]
   1070 		return www_query(ctx, w, r, ii.Query{From: args[1]}, page, rss)
   1071 	} else if args[0] == "echo" || args[0] == "echo+topics" {
   1072 		page := 1
   1073 		rss := false
   1074 		if len(args) < 2 {
   1075 			return errors.New("Wrong request")
   1076 		}
   1077 		if len(args) > 2 {
   1078 			if args[2] == "rss" {
   1079 				rss = true
   1080 			} else {
   1081 				fmt.Sscanf(args[2], "%d", &page)
   1082 			}
   1083 		}
   1084 		q := ii.Query{Echo: args[1]}
   1085 		if args[1] == "all" {
   1086 			q.Echo = ""
   1087 		}
   1088 		ctx.Echo = q.Echo
   1089 		q.Start = -PAGE_SIZE
   1090 		if args[0] == "echo+topics" {
   1091 			q.Repto = "!"
   1092 			ctx.BasePath = "echo+topics/" + args[1]
   1093 		} else {
   1094 			ctx.BasePath = "echo/" + args[1]
   1095 		}
   1096 		return www_query(ctx, w, r, q, page, rss)
   1097 	} else if ii.IsEcho(args[0]) {
   1098 		page := 1
   1099 		ctx.Echo = args[0]
   1100 		ctx.BasePath = args[0]
   1101 		if len(args) > 1 {
   1102 			if args[1] == "new" {
   1103 				ctx.BasePath = args[0]
   1104 				return www_new(ctx, w, r)
   1105 			}
   1106 			fmt.Sscanf(args[1], "%d", &page)
   1107 		}
   1108 		return www_topics(ctx, w, r, page)
   1109 	} else {
   1110 		w.WriteHeader(http.StatusNotFound)
   1111 		fmt.Fprintf(w, "404\n")
   1112 	}
   1113 	return nil
   1114 }
   1115 
   1116 func generateSecureToken(length int) string {
   1117   b := make([]byte, length)
   1118   if _, err := rand.Read(b); err != nil {
   1119     return ""
   1120   }
   1121   return hex.EncodeToString(b)
   1122 }