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, "&", "&", -1) 736 l = strings.Replace(l, "<", "<", -1) 737 l = strings.Replace(l, ">", ">", -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 }