views

package
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 15, 2017 License: BSD-3-Clause Imports: 25 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AdminIndexHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if !sess.IsUserSuperAdmin() {
		ErrForbiddenHandler(w, r)
		return
	}

	linkID := r.PostFormValue("linkid")

	if r.Method == "POST" && linkID == "" {
		forumName := strings.TrimSpace(r.PostFormValue("forum_name"))
		headerMsg := strings.TrimSpace(r.PostFormValue("header_msg"))
		censoredWords := r.PostFormValue("censored_words")
		loginMsg := strings.TrimSpace(r.PostFormValue("login_msg"))
		signupMsg := strings.TrimSpace(r.PostFormValue("signup_msg"))
		signupDisabled := "0"
		groupCreationDisabled := "0"
		imageUploadEnabled := "0"
		allowGroupSubscription := "0"
		allowTopicSubscription := "0"
		readOnlyMode := "0"
		dataDir := r.PostFormValue("data_dir")
		bodyAppendage := r.PostFormValue("body_appendage")
		defaultFromEmail := r.PostFormValue("default_from_mail")
		smtpHost := r.PostFormValue("smtp_host")
		smtpPort := r.PostFormValue("smtp_port")
		smtpUser := r.PostFormValue("smtp_user")
		smtpPass := r.PostFormValue("smtp_pass")
		if r.PostFormValue("signup_disabled") != "" {
			signupDisabled = "1"
		}
		if r.PostFormValue("group_creation_disabled") != "" {
			groupCreationDisabled = "1"
		}
		if r.PostFormValue("image_upload_enabled") != "" {
			imageUploadEnabled = "1"
		}
		if r.PostFormValue("allow_group_subscription") != "" {
			allowGroupSubscription = "1"
		}
		if r.PostFormValue("allow_topic_subscription") != "" {
			allowTopicSubscription = "1"
		}
		if r.PostFormValue(models.ReadOnlyMode) != "" {
			readOnlyMode = "1"
		}
		if dataDir != "" {
			if dataDir[len(dataDir)-1] != '/' {
				dataDir = dataDir + "/"
			}
		}

		errMsg := ""
		if forumName == "" {
			errMsg = "Forum name is empty."
		}

		if errMsg == "" {
			models.WriteConfig(models.ForumName, forumName)
			models.WriteConfig(models.HeaderMsg, headerMsg)
			models.WriteConfig(models.LoginMsg, loginMsg)
			models.WriteConfig(models.SignupMsg, signupMsg)
			models.WriteConfig(models.SignupDisabled, signupDisabled)
			models.WriteConfig(models.CensoredWords, censoredWords)
			models.WriteConfig(models.GroupCreationDisabled, groupCreationDisabled)
			models.WriteConfig(models.ImageUploadEnabled, imageUploadEnabled)
			models.WriteConfig(models.AllowGroupSubscription, allowGroupSubscription)
			models.WriteConfig(models.AllowTopicSubscription, allowTopicSubscription)
			models.WriteConfig(models.ReadOnlyMode, readOnlyMode)
			models.WriteConfig(models.DataDir, dataDir)
			models.WriteConfig(models.BodyAppendage, bodyAppendage)
			models.WriteConfig(models.DefaultFromMail, defaultFromEmail)
			models.WriteConfig(models.SMTPHost, smtpHost)
			models.WriteConfig(models.SMTPPort, smtpPort)
			models.WriteConfig(models.SMTPUser, smtpUser)
			models.WriteConfig(models.SMTPPass, smtpPass)
			sess.SetFlashMsg("Update successful.")
		} else {
			sess.SetFlashMsg(errMsg)
		}
		http.Redirect(w, r, "/admin", http.StatusSeeOther)
		return
	}

	if r.Method == "POST" && linkID != "" {
		name := r.PostFormValue("name")
		URL := r.PostFormValue("url")
		content := r.PostFormValue("content")
		if linkID == "new" {
			if name != "" && (URL != "" || content != "") {
				db.Exec(`INSERT INTO extranotes(name, URL, content, created_date, updated_date) VALUES(?, ?, ?, ?, ?);`, name, URL, content, time.Now().Unix(), time.Now().Unix())
			} else {
				sess.SetFlashMsg("Enter an external URL or type some content for the footer link.")
			}
		} else {
			if r.PostFormValue("submit") == "Delete" {
				db.Exec(`DELETE FROM extranotes WHERE id=?;`, linkID)
			} else {
				db.Exec(`UPDATE extranotes SET name=?, URL=?, content=?, updated_date=? WHERE id=?;`, name, URL, content, int64(time.Now().Unix()), linkID)
			}

		}
		http.Redirect(w, r, "/admin", http.StatusSeeOther)
		return
	}

	rows := db.Query(`SELECT id, name, URL, content FROM extranotes;`)
	var extraNotes []ExtraNote
	for rows.Next() {
		var extraNote ExtraNote
		rows.Scan(&extraNote.ID, &extraNote.Name, &extraNote.URL, &extraNote.Content)
		extraNotes = append(extraNotes, extraNote)
	}

	templates.Render(w, "adminindex.html", map[string]interface{}{
		"Common":      readCommonData(r, sess),
		"Config":      models.ConfigAllVals(),
		"ExtraNotes":  extraNotes,
		"NumUsers":    models.NumUsers(),
		"NumGroups":   models.NumGroups(),
		"NumTopics":   models.NumTopics(),
		"NumComments": models.NumComments(),
	})
})
View Source
var ChangePasswdHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	userName := r.FormValue("u")
	commonData := readCommonData(r, sess)
	if !sess.IsUserValid() {
		ErrForbiddenHandler(w, r)
		return
	}
	if userName != commonData.UserName && !commonData.IsSuperAdmin {
		ErrForbiddenHandler(w, r)
		return
	}
	if r.Method == "POST" {
		if !commonData.IsSuperAdmin {
			passwd := r.PostFormValue("passwd")
			if sess.Authenticate(userName, passwd) != nil {
				sess.SetFlashMsg("Current password incorrect.")
				http.Redirect(w, r, "/changepass?u="+userName, http.StatusSeeOther)
				return
			}
		}
		newPasswd := r.PostFormValue("newpass")
		newPasswdConfirm := r.PostFormValue("confirm")
		if err := validatePasswd(newPasswd, newPasswdConfirm); err != nil {
			sess.SetFlashMsg(err.Error())
			http.Redirect(w, r, "/changepass?u="+userName, http.StatusSeeOther)
			return
		}
		if err := models.UpdateUserPasswd(userName, newPasswd); err != nil {
			log.Panicf("[ERROR] Error changing password: %s\n", err)
		}
		if commonData.IsSuperAdmin {
			var userID string
			db.QueryRow(`SELECT id FROM users WHERE username=?;`, userName).Scan(&userID)
			db.Exec(`DELETE FROM sessions WHERE userid=?;`, userID)
		}
		sess.SetFlashMsg("Password change successful.")
		http.Redirect(w, r, "/changepass?u="+userName, http.StatusSeeOther)
		return
	}
	templates.Render(w, "changepass.html", map[string]interface{}{
		"Common":   commonData,
		"UserName": userName,
	})
})
View Source
var CommentCreateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	topicID := r.FormValue("tid")
	quoteID := r.FormValue("quote")
	content := strings.TrimSpace(r.PostFormValue("content"))
	isSticky := r.PostFormValue("is_sticky") != ""
	isImageUploadEnabled := models.Config(models.ImageUploadEnabled) != "0"
	var groupID, groupName, topicName, parentComment, topicOwnerID, topicOwnerName string
	var topicCreatedDate int64

	if db.QueryRow(`SELECT userid, groupid, title, content, created_date FROM topics WHERE id=?;`, topicID).Scan(
		&topicOwnerID, &groupID, &topicName, &parentComment, &topicCreatedDate) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	isClosed := true
	db.QueryRow(`SELECT is_closed FROM groups WHERE id=?;`, groupID).Scan(&isClosed)

	if isClosed {
		ErrForbiddenHandler(w, r)
		return
	}
	db.QueryRow(`SELECT username FROM users WHERE id=?;`, topicOwnerID).Scan(&topicOwnerName)

	var tmp string
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)
	isMod := db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?;`, sess.UserID).Scan(&isSuperAdmin)

	quoteContent := ""
	if quoteID != "" {
		var quotedUser string
		var isDeleted bool
		db.QueryRow(`SELECT comments.content, comments.is_deleted, users.username FROM comments INNER JOIN users ON comments.userid=users.id WHERE comments.id=?;`, quoteID).Scan(&quoteContent, &isDeleted, &quotedUser)
		if !isDeleted {
			quoteContent = formatReply(quotedUser, quoteContent)
		} else {
			quoteContent = ""
		}
	}

	if r.Method == "POST" {
		if !isMod && !isAdmin && !isSuperAdmin {
			isSticky = false
		}
		imageName := ""
		if isImageUploadEnabled {
			imageName = saveImage(r)
		}

		if (len(content) < 2 && imageName == "") || len(content) > 5000 {
			sess.SetFlashMsg("Comment should have 2-5000 characters.")
			http.Redirect(w, r, "/comments/new?tid="+topicID, http.StatusSeeOther)
			return
		}

		var lastPos int
		db.QueryRow(`SELECT pos FROM comments WHERE topicid=? ORDER BY pos DESC LIMIT 1;`, topicID).Scan(&lastPos)
		newPos := lastPos + 1
		if isSticky {
			newPos = -newPos
		}

		db.Exec(`INSERT INTO comments(content, image, topicid, userid, parentid, pos, created_date, updated_date) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
			content, imageName, topicID, sess.UserID, sql.NullInt64{Valid: false}, newPos, int64(time.Now().Unix()), int64(time.Now().Unix()))
		db.Exec(`UPDATE topics SET num_comments=num_comments+1, activity_date=? WHERE id=?;`, int(time.Now().Unix()), topicID)
		if models.Config(models.AllowTopicSubscription) != "0" {
			var userName string
			db.QueryRow(`SELECT username FROM users WHERE id=?;`, sess.UserID).Scan(&userName)
			topicURL := "http://" + r.Host + "/topics?id=" + topicID
			rows := db.Query(`SELECT users.email, topicsubscriptions.token FROM users INNER JOIN topicsubscriptions ON users.id=topicsubscriptions.userid AND topicsubscriptions.topicid=?;`, topicID)
			for rows.Next() {
				var email, token string
				rows.Scan(&email, &token)
				if email != "" {
					unSubURL := "http://" + r.Host + "/topics/unsubscribe?token=" + token
					utils.SendMail(email, `New comment in "`+topicName+`"`,
						"A new comment has been posted by "+userName+" in \""+topicName+"\".\r\nSee the comment at "+topicURL+"\r\n\r\nIf you do not want these emails, unsubscribe by following this link: "+unSubURL)
				}
			}
		}
		page := newPos / numCommentsPerPage
		if page < 0 {
			page = 0
		}
		http.Redirect(w, r, "/topics?id="+topicID+"&p="+strconv.Itoa(page)+"#comment-last", http.StatusSeeOther)
		return
	}

	templates.Render(w, "commentedit.html", map[string]interface{}{
		"Common":               readCommonData(r, sess),
		"TopicID":              topicID,
		"TopicOwnerName":       topicOwnerName,
		"TopicCreatedDate":     timeAgoFromNow(time.Unix(topicCreatedDate, 0)),
		"CommentID":            "",
		"TopicName":            topicName,
		"GroupName":            groupName,
		"ParentComment":        parentComment,
		"Content":              quoteContent,
		"IsSticky":             false,
		"IsMod":                isMod,
		"IsAdmin":              isAdmin,
		"IsSuperAdmin":         isSuperAdmin,
		"IsImageUploadEnabled": isImageUploadEnabled,
	})
})
View Source
var CommentIndexHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	commentID := r.FormValue("id")
	var groupID, topicID, topicName, groupName, ownerID, ownerName, content, imgSrc string
	var cDate int64
	var isDeleted bool

	if db.QueryRow(`SELECT userid, topicid, content, image, is_deleted, created_date FROM comments WHERE id=?;`, commentID).Scan(
		&ownerID, &topicID, &content, &imgSrc, &isDeleted, &cDate) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	db.QueryRow(`SELECT groupid, title FROM topics WHERE id=?;`, topicID).Scan(&groupID, &topicName)
	db.QueryRow(`SELECT username FROM users WHERE id=?;`, ownerID).Scan(&ownerName)
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)

	var tmp string
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)
	isMod := sess.UserID.Valid && db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := sess.UserID.Valid && db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	if sess.UserID.Valid {
		db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?`, sess.UserID).Scan(&isSuperAdmin)
	}
	isOwner := sess.UserID.Valid && db.QueryRow(`SELECT userid FROM comments WHERE id=?;`, commentID).Scan(&tmp) == nil

	templates.Render(w, "commentindex.html", map[string]interface{}{
		"Common":       readCommonData(r, sess),
		"ID":           commentID,
		"TopicID":      topicID,
		"TopicName":    topicName,
		"GroupName":    groupName,
		"OwnerName":    ownerName,
		"Content":      formatComment(content),
		"ImgSrc":       imgSrc,
		"IsMod":        isMod,
		"IsAdmin":      isAdmin,
		"IsSuperAdmin": isSuperAdmin,
		"IsOwner":      isOwner,
		"IsDeleted":    isDeleted,
		"CreatedDate":  timeAgoFromNow(time.Unix(cDate, 0)),
	})
})
View Source
var CommentUpdateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	commentID := r.FormValue("id")
	content := strings.TrimSpace(r.PostFormValue("content"))
	isSticky := r.PostFormValue("is_sticky") != ""

	var groupID, topicID, groupName, topicName, parentComment, topicOwnerName, topicOwnerID string
	var topicCreatedDate int64
	var pos int
	if db.QueryRow(`SELECT topicid, pos FROM comments WHERE id=?;`, commentID).Scan(&topicID, &pos) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	if db.QueryRow(`SELECT userid, groupid, title, content, created_date FROM topics WHERE id=?;`, topicID).Scan(
		&topicOwnerID, &groupID, &topicName, &parentComment, &topicCreatedDate) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	isClosed := true
	db.QueryRow(`SELECT is_closed FROM groups WHERE id=?;`, groupID).Scan(&isClosed)
	if !isClosed {
		db.QueryRow(`SELECT is_closed FROM topics WHERE id=?;`, topicID).Scan(&isClosed)
	}

	if isClosed {
		ErrForbiddenHandler(w, r)
		return
	}

	db.QueryRow(`SELECT username FROM users WHERE id=?;`, topicOwnerID).Scan(&topicOwnerName)

	var tmp string
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)
	isMod := db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?`, sess.UserID).Scan(&isSuperAdmin)
	isOwner := db.QueryRow(`SELECT userid FROM comments WHERE id=?;`, commentID).Scan(&tmp) == nil

	if !isOwner && !isMod && !isAdmin && !isSuperAdmin {
		ErrForbiddenHandler(w, r)
		return
	}

	if r.Method == "POST" {
		action := r.PostFormValue("action")
		if action == "Update" {
			if len(content) < 2 || len(content) > 5000 {
				sess.SetFlashMsg("Comment should have 2-5000 characters.")
				http.Redirect(w, r, "/comments/edit?id="+commentID, http.StatusSeeOther)
				return
			}
			if content == "" {
				http.Redirect(w, r, "/comments/edit?id="+commentID, http.StatusSeeOther)
				return
			}
			if !isMod && !isAdmin && !isSuperAdmin {
				isSticky = (pos < 0)
			}
			if isSticky {
				if pos > 0 {
					pos = -pos
				}
			} else {
				if pos < 0 {
					pos = -pos
				}
			}
			db.Exec(`UPDATE comments SET content=?, pos=?, updated_date=? WHERE id=?;`, content, pos, int64(time.Now().Unix()), commentID)
			page := pos / numCommentsPerPage
			if page < 0 {
				page = 0
			}
			http.Redirect(w, r, "/topics?id="+topicID+"&p="+strconv.Itoa(page)+"#comment-"+commentID, http.StatusSeeOther)
		}
		if action == "Delete" {
			db.Exec(`UPDATE comments SET is_deleted=1 WHERE id=?;`, commentID)
			http.Redirect(w, r, "/comments/edit?id="+commentID, http.StatusSeeOther)
		}
		if action == "Undelete" {
			db.Exec(`UPDATE comments SET is_deleted=0 WHERE id=?;`, commentID)
			http.Redirect(w, r, "/comments/edit?id="+commentID, http.StatusSeeOther)
		}
		return
	}
	isDeleted := false
	db.QueryRow(`SELECT content, is_deleted FROM comments WHERE id=?;`, commentID).Scan(&content, &isDeleted)
	isSticky = (pos < 0)

	templates.Render(w, "commentedit.html", map[string]interface{}{
		"Common":               readCommonData(r, sess),
		"TopicID":              topicID,
		"TopicOwnerName":       topicOwnerName,
		"TopicCreatedDate":     timeAgoFromNow(time.Unix(topicCreatedDate, 0)),
		"CommentID":            commentID,
		"TopicName":            topicName,
		"GroupName":            groupName,
		"ParentComment":        parentComment,
		"Content":              content,
		"IsSticky":             isSticky,
		"IsMod":                isMod,
		"IsAdmin":              isAdmin,
		"IsSuperAdmin":         isSuperAdmin,
		"IsDeleted":            isDeleted,
		"IsImageUploadEnabled": false,
	})
})
View Source
var ErrAuthFail = errors.New("username / password incorrect")
View Source
var ErrNoFlashMsg = errors.New("No flash message")
View Source
var ForgotPasswdHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if r.Method == "POST" {
		userName := r.PostFormValue("username")
		if userName == "" || len(userName) > 200 || !models.ProbeUser(userName) {
			sess.SetFlashMsg("Username doesn't exist.")
			http.Redirect(w, r, "/forgotpass", http.StatusSeeOther)
			return
		}
		email := models.ReadUserEmail(userName)
		if !strings.ContainsRune(email, '@') {
			sess.SetFlashMsg("E-mail address not set. Contact site admin to reset the password.")
			http.Redirect(w, r, "/forgotpass", http.StatusSeeOther)
			return
		}
		forumName := models.Config(models.ForumName)

		resetToken := randSeq(40)
		db.Exec(`UPDATE users SET reset_token=?, reset_token_date=? WHERE username=?;`, resetToken, int64(time.Now().Unix()), userName)

		resetLink := "https://" + r.Host + "/resetpass?r=" + resetToken
		sub := forumName + " Password Recovery"
		msg := "Someone (hopefully you) requested we reset your password at " + forumName + ".\r\n" +
			"If you want to change it, visit " + resetLink + "\r\n\r\nIf not, just ignore this message."
		utils.SendMail(email, sub, msg)
		sess.SetFlashMsg("Password reset link sent to your e-mail.")
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return

	}
	templates.Render(w, "forgotpass.html", map[string]interface{}{
		"Common": readCommonData(r, sess),
	})
})
View Source
var GroupEditHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if models.Config(models.GroupCreationDisabled) == "1" {
		ErrForbiddenHandler(w, r)
		return
	}
	commonData := readCommonData(r, sess)

	userName := commonData.UserName

	groupID := r.FormValue("id")
	name := strings.TrimSpace(r.FormValue("name"))
	desc := strings.TrimSpace(r.FormValue("desc"))
	headerMsg := strings.TrimSpace(r.FormValue("header_msg"))
	isSticky := r.FormValue("is_sticky") != ""
	isPrivate := r.FormValue("is_private") != ""
	isDeleted := false
	mods := strings.Split(r.FormValue("mods"), ",")
	for i, mod := range mods {
		mods[i] = strings.TrimSpace(mod)
	}
	admins := strings.Split(r.FormValue("admins"), ",")
	for i, admin := range admins {
		admins[i] = strings.TrimSpace(admin)
	}
	if len(admins) == 1 && admins[0] == "" {
		admins[0] = userName
	}
	action := r.FormValue("action")

	if groupID != "" {
		if !models.IsUserGroupAdmin(strconv.Itoa(int(sess.UserID.Int64)), groupID) && !commonData.IsSuperAdmin {
			ErrForbiddenHandler(w, r)
			return
		}
	}

	if r.Method == "POST" {
		if action == "Create" {
			if len(name) < 3 || len(name) > 40 {
				sess.SetFlashMsg("Group name should have 3-40 characters.")
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			if censored := censor(name); censored != name {
				sess.SetFlashMsg("Fix group name: " + censored)
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			if len(desc) > 160 {
				sess.SetFlashMsg("Group description should have less than 160 characters.")
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			if len(headerMsg) > 160 {
				sess.SetFlashMsg("Announcement should have less than 160 characters.")
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			if err := validateName(name); err != nil {
				sess.SetFlashMsg(err.Error())
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			if len(admins) > 32 || len(mods) > 32 {
				sess.SetFlashMsg("Number of admins/mods should no more than 32.")
				http.Redirect(w, r, "/groups/edit", http.StatusSeeOther)
				return
			}
			db.Exec(`INSERT INTO groups(name, description, header_msg, is_sticky, is_private, created_date, updated_date) VALUES(?, ?, ?, ?, ?, ?, ?);`, name, desc, headerMsg, isSticky, isPrivate, time.Now().Unix(), time.Now().Unix())
			groupID := models.ReadGroupIDByName(name)
			for _, mod := range mods {
				if mod != "" {
					models.CreateGroupMod(mod, groupID)
				}
			}
			for _, admin := range admins {
				if admin != "" {
					models.CreateGroupAdmin(admin, groupID)
				}
			}
			http.Redirect(w, r, "/groups?name="+name, http.StatusSeeOther)
		} else if action == "Update" {
			if len(name) < 3 || len(name) > 40 {
				sess.SetFlashMsg("Group name should have 3-40 characters.")
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			if censored := censor(name); censored != name {
				sess.SetFlashMsg("Fix group name: " + censored)
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			if len(desc) > 160 {
				sess.SetFlashMsg("Group description should have less than 160 characters.")
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			if len(headerMsg) > 160 {
				sess.SetFlashMsg("Announcement should have less than 160 characters.")
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			if err := validateName(name); err != nil {
				sess.SetFlashMsg(err.Error())
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			if len(admins) > 32 || len(mods) > 32 {
				sess.SetFlashMsg("Number of admins/mods should no more than 32.")
				http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
				return
			}
			isUserSuperAdmin := false
			db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?;`, sess.UserID).Scan(&isUserSuperAdmin)
			if !isUserSuperAdmin {
				db.QueryRow(`SELECT is_sticky FROM groups WHERE id=?;`, groupID).Scan(&isSticky)
			}
			db.Exec(`UPDATE groups SET name=?, description=?, header_msg=?, is_sticky=?, is_private=?, updated_date=? WHERE id=?;`, name, desc, headerMsg, isSticky, isPrivate, time.Now().Unix(), groupID)
			db.Exec(`DELETE FROM mods WHERE groupid=?;`, groupID)
			db.Exec(`DELETE FROM admins WHERE groupid=?;`, groupID)
			for _, mod := range mods {
				if mod != "" {
					models.CreateGroupMod(mod, groupID)
				}
			}
			for _, admin := range admins {
				if admin != "" {
					models.CreateGroupAdmin(admin, groupID)
				}
			}
			http.Redirect(w, r, "/groups?name="+name, http.StatusSeeOther)
		} else if action == "Delete" {
			db.Exec(`UPDATE groups SET is_closed=1 WHERE id=?;`, groupID)
			http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
		} else if action == "Undelete" {
			db.Exec(`UPDATE groups SET is_closed=0 WHERE id=?;`, groupID)
			http.Redirect(w, r, "/groups/edit?id="+groupID, http.StatusSeeOther)
		}
		return
	}

	if groupID != "" {

		db.QueryRow(`SELECT name, description, header_msg, is_sticky, is_private, is_closed FROM groups WHERE id=?;`, groupID).Scan(
			&name, &desc, &headerMsg, &isSticky, &isPrivate, &isDeleted,
		)
		mods = models.ReadMods(groupID)
		admins = models.ReadAdmins(groupID)
	}

	templates.Render(w, "groupedit.html", map[string]interface{}{
		"Common":    readCommonData(r, sess),
		"ID":        groupID,
		"GroupName": name,
		"Desc":      desc,
		"HeaderMsg": headerMsg,
		"IsSticky":  isSticky,
		"IsPrivate": isPrivate,
		"IsDeleted": isDeleted,
		"Mods":      strings.Join(mods, ", "),
		"Admins":    strings.Join(admins, ", "),
	})
})
View Source
var GroupIndexHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	name := r.FormValue("name")
	var groupID, groupDesc, headerMsg string
	if db.QueryRow(`SELECT id, description, header_msg FROM groups WHERE name=?;`, name).Scan(&groupID, &groupDesc, &headerMsg) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	subToken := ""
	if sess.UserID.Valid {
		db.QueryRow(`SELECT token FROM groupsubscriptions WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&subToken)
	}

	numTopicsPerPage := 30
	lastTopicDate, err := strconv.ParseInt(r.FormValue("ltd"), 10, 64)
	if err != nil {
		lastTopicDate = 0
	}

	type Topic struct {
		ID          int
		Title       string
		IsDeleted   bool
		IsClosed    bool
		Owner       string
		NumComments int
		CreatedDate string
		cDateUnix   int64
	}
	var topics []Topic
	var rows *db.Rows
	if lastTopicDate == 0 {
		rows = db.Query(`SELECT topics.id, topics.title, topics.is_deleted, topics.is_closed, topics.num_comments, topics.created_date, users.username FROM topics INNER JOIN users ON topics.userid = users.id AND topics.groupid=? ORDER BY topics.is_sticky DESC, topics.activity_date DESC LIMIT ?;`, groupID, numTopicsPerPage)
	} else {
		rows = db.Query(`SELECT topics.id, topics.title, topics.is_deleted, topics.is_closed, topics.num_comments, topics.created_date, users.username FROM topics INNER JOIN users ON topics.userid = users.id AND topics.groupid=? AND topics.is_sticky=0 AND topics.created_date < ? ORDER BY topics.activity_date DESC LIMIT ?;`, groupID, lastTopicDate, numTopicsPerPage)
	}
	for rows.Next() {
		t := Topic{}
		rows.Scan(&t.ID, &t.Title, &t.IsDeleted, &t.IsClosed, &t.NumComments, &t.cDateUnix, &t.Owner)
		t.CreatedDate = timeAgoFromNow(time.Unix(t.cDateUnix, 0))
		t.Title = censor(t.Title)
		topics = append(topics, t)
	}

	isSuperAdmin := false
	isAdmin := false
	isMod := false
	if sess.IsUserValid() {
		db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?;`, sess.UserID).Scan(&isSuperAdmin)
		var tmp string
		isAdmin = db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
		isMod = db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	}

	if len(topics) >= numTopicsPerPage {
		lastTopicDate = topics[len(topics)-1].cDateUnix
	} else {
		lastTopicDate = 0
	}

	commonData := readCommonData(r, sess)
	commonData.PageTitle = name

	templates.Render(w, "groupindex.html", map[string]interface{}{
		"Common":        commonData,
		"GroupName":     name,
		"GroupDesc":     censor(groupDesc),
		"GroupID":       groupID,
		"HeaderMsg":     censor(headerMsg),
		"SubToken":      subToken,
		"Topics":        topics,
		"IsMod":         isMod,
		"IsAdmin":       isAdmin,
		"IsSuperAdmin":  isSuperAdmin,
		"LastTopicDate": lastTopicDate,
	})
})
View Source
var GroupSubscribeHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	groupID := r.FormValue("id")
	if models.Config(models.AllowGroupSubscription) == "0" {
		ErrForbiddenHandler(w, r)
		return
	}
	var groupName string
	if db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	if r.Method == "POST" {
		var tmp string
		if db.QueryRow(`SELECT id FROM groupsubscriptions WHERE userid=? AND groupid=?;`, sess.UserID, groupID).Scan(&tmp) != nil {
			db.Exec(`INSERT INTO groupsubscriptions(userid, groupid, token, created_date) VALUES(?, ?, ?, ?);`,
				sess.UserID, groupID, randSeq(64), time.Now().Unix())
		}
	}
	http.Redirect(w, r, "/groups?name="+groupName, http.StatusSeeOther)
})
View Source
var GroupUnsubscribeHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	token := r.FormValue("token")
	var groupID, groupName string
	if db.QueryRow(`SELECT groupid FROM groupsubscriptions WHERE token=?;`, token).Scan(&groupID) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)
	if r.Method == "POST" {
		db.Exec(`DELETE FROM groupsubscriptions WHERE token=?;`, token)
		if r.PostFormValue("noredirect") != "" {
			w.Write([]byte("Unsubscribed."))
		} else {
			http.Redirect(w, r, "/groups?name="+groupName, http.StatusSeeOther)
		}
		return
	}
	w.Write([]byte(`<!DOCTYPE html><html><head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1"></head>
	<body><form action="/groups/unsubscribe" method="POST">
	Unsubscribe from ` + groupName + `?
	<input type="hidden" name="token" value=` + token + `>
	<input type="hidden" name="csrf" value="` + sess.CSRFToken + `">
	<input type="hidden" name="noredirect" value="1">
	<input type="submit" value="Unsubscribe">
	</form></body></html>`))
})
View Source
var IndexHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if r.URL.Path != "/" {
		ErrNotFoundHandler(w, r)
		return
	}

	type Group struct {
		Name     string
		Desc     string
		IsSticky int
	}
	groups := []Group{}
	rows := db.Query(`SELECT name, description, is_sticky FROM groups WHERE is_closed=0 ORDER BY is_sticky DESC, RANDOM() LIMIT 25;`)
	for rows.Next() {
		groups = append(groups, Group{})
		g := &groups[len(groups)-1]
		rows.Scan(&g.Name, &g.Desc, &g.IsSticky)
		g.Desc = censor(g.Desc)
	}
	sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })
	sort.Slice(groups, func(i, j int) bool { return groups[i].IsSticky > groups[j].IsSticky })

	type Topic struct {
		ID          string
		Title       string
		GroupName   string
		OwnerName   string
		CreatedDate string
		NumComments int
	}
	topics := []Topic{}
	trows := db.Query(`SELECT topics.id, topics.title, topics.num_comments, topics.created_date, topics.is_deleted, topics.is_closed, groups.name, groups.is_closed, users.username FROM topics INNER JOIN groups ON topics.groupid=groups.id INNER JOIN users ON topics.userid=users.id ORDER BY topics.created_date DESC LIMIT 20;`)
	for trows.Next() {
		t := Topic{}
		var cDate int64
		var isTopicDeleted, isTopicClosed, isGroupClosed bool
		trows.Scan(&t.ID, &t.Title, &t.NumComments, &cDate, &isTopicDeleted, &isTopicClosed, &t.GroupName, &isGroupClosed, &t.OwnerName)
		t.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
		t.Title = censor(t.Title)
		if !isTopicDeleted && !isTopicClosed && !isGroupClosed {
			topics = append(topics, t)
		}
	}
	templates.Render(w, "index.html", map[string]interface{}{
		"Common":                readCommonData(r, sess),
		"GroupCreationDisabled": models.Config(models.GroupCreationDisabled) == "1",
		"HeaderMsg":             models.Config(models.HeaderMsg),
		"Groups":                groups,
		"Topics":                topics,
	})
})
View Source
var LoginHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	redirectURL, err := url.QueryUnescape(r.FormValue("next"))
	if redirectURL == "" || err != nil {
		redirectURL = "/"
	}
	if sess.IsUserValid() {
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}

	if r.Method == "POST" {
		userName := r.PostFormValue("username")
		passwd := r.PostFormValue("passwd")
		if len(userName) > 200 || len(passwd) > 200 {
			fmt.Fprint(w, "username / password too long.")
			return
		}
		if err = sess.Authenticate(userName, passwd); err == nil {
			http.Redirect(w, r, redirectURL, http.StatusSeeOther)
			return
		} else {
			sess.SetFlashMsg(err.Error())
			http.Redirect(w, r, "/login?next="+redirectURL, http.StatusSeeOther)
			return
		}
	}
	templates.Render(w, "login.html", map[string]interface{}{
		"Common":   readCommonData(r, sess),
		"next":     template.URL(url.QueryEscape(redirectURL)),
		"LoginMsg": models.Config(models.LoginMsg),
	})
})
View Source
var NoteHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	id := r.FormValue("id")

	row := db.QueryRow(`SELECT name, URL, content, created_date, updated_date FROM extranotes WHERE id=?;`, id)
	var e ExtraNote
	var cDate int64
	var uDate int64
	if err := row.Scan(&e.Name, &e.URL, &e.Content, &cDate, &uDate); err == nil {
		e.CreatedDate = time.Unix(cDate, 0)
		e.UpdatedDate = time.Unix(uDate, 0)
		if e.URL == "" {
			templates.Render(w, "extranote.html", map[string]interface{}{
				"Common":      readCommonData(r, sess),
				"Name":        e.Name,
				"UpdatedDate": e.UpdatedDate,
				"Content":     template.HTML(e.Content),
			})
			return
		} else {
			http.Redirect(w, r, e.URL, http.StatusSeeOther)
			return
		}
	}
	ErrNotFoundHandler(w, r)
})
View Source
var PrivateMessageCreateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if r.Method == "POST" {
		tousers := strings.TrimSpace(r.PostFormValue("to"))
		content := strings.TrimSpace(r.PostFormValue("content"))

		if tousers == "" {
			sess.SetFlashMsg("No users to send the message to.")
			http.Redirect(w, r, "/pm", http.StatusSeeOther)
			return
		}
		if content == "" {
			sess.SetFlashMsg("Content is empty.")
			http.Redirect(w, r, "/pm", http.StatusSeeOther)
			return
		}

		tousernames := strings.Split(tousers, ",")
		touserids := []string{}
		for _, tousername := range tousernames {
			username := strings.TrimSpace(tousername)
			var userid string
			if err := db.QueryRow(`SELECT id FROM users WHERE username=?;`, username).Scan(&userid); err == nil {
				touserids = append(touserids, userid)
			} else {
				sess.SetFlashMsg("Username not found: " + username)
				http.Redirect(w, r, "/pm#end", http.StatusSeeOther)
				return
			}
		}

		for _, userid := range touserids {
			db.Exec(`INSERT INTO messages(fromid, toid, content, created_date) VALUES(?, ?, ?, ?);`, sess.UserID, userid, content, int(time.Now().Unix()))
		}

		sess.SetFlashMsg("Message sent.")
		http.Redirect(w, r, "/pm#end", http.StatusSeeOther)
		return
	}
})
View Source
var PrivateMessageDeleteHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	if r.Method == "POST" {
		id := r.PostFormValue("id")
		db.Exec(`DELETE FROM messages WHERE id=? AND toid=?;`, id, sess.UserID)
		http.Redirect(w, r, "/pm?lmd="+r.PostFormValue("lmd"), http.StatusSeeOther)
		return
	}
})
View Source
var PrivateMessageHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	startDate := time.Now().Unix()
	lmd := r.FormValue("lmd")
	if lmd != "" {
		if d, err := strconv.Atoi(lmd); err == nil {
			startDate = int64(d)
		}
	}

	type Message struct {
		ID          string
		From        string
		To          string
		IsRead      bool
		CreatedDate string
		Content     template.HTML
	}

	var lastMessageDate int64
	var msgs []Message
	var cDate int64
	var content string
	var rows *db.Rows
	rows = db.Query(`SELECT messages.id, fromusers.username, tousers.username, messages.content, messages.is_read, messages.created_date
		FROM messages INNER JOIN users fromusers ON fromusers.id=messages.fromid INNER JOIN users tousers ON tousers.id=messages.toid
		WHERE messages.toid=? AND messages.created_date <= ? ORDER BY messages.created_date DESC LIMIT ?;`, sess.UserID, startDate, messagesPerPage+1)
	for rows.Next() {
		msg := Message{}
		rows.Scan(&msg.ID, &msg.From, &msg.To, &content, &msg.IsRead, &cDate)
		msg.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
		msg.Content = formatComment(content)
		if len(msgs) < messagesPerPage {
			msgs = append(msgs, msg)
		} else {
			lastMessageDate = cDate
		}
	}

	to, cont := "", ""

	if pmid := r.FormValue("quote"); pmid != "" {
		db.QueryRow(`SELECT users.username, messages.content
			FROM messages INNER JOIN users ON messages.fromid=users.id
			WHERE messages.id=?;`, pmid).Scan(&to, &cont)
		cont = formatReply(to, cont)
	}

	if flag := r.FormValue("flag"); flag != "" {
		rows := db.Query(`SELECT users.username FROM mods
			INNER JOIN users ON users.id=mods.userid
			INNER JOIN topics ON topics.groupid=mods.groupid
			INNER JOIN comments ON comments.topicid=topics.id
			WHERE comments.id=?;`, flag)
		for rows.Next() {
			var mod string
			rows.Scan(&mod)
			if to != "" {
				to = to + ", "
			}
			to = to + mod
		}
		cont = "Flagging " + "http://" + r.Host + "/comments?id=" + flag
	}

	if lmd != "" && len(msgs) == 0 {
		http.Redirect(w, r, "/pm", http.StatusSeeOther)
		return
	}

	db.Exec(`UPDATE messages SET is_read=? WHERE toid=?;`, true, sess.UserID)

	templates.Render(w, "pm.html", map[string]interface{}{
		"Common":           readCommonData(r, sess),
		"Messages":         msgs,
		"LastMessageDate":  lastMessageDate,
		"FirstMessageDate": startDate,
		"To":               to,
		"Content":          cont,
	})
})
View Source
var ResetPasswdHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	resetToken := r.FormValue("r")
	userName, err := models.ReadUserNameByToken(resetToken)
	if err != nil {
		ErrForbiddenHandler(w, r)
		return
	}
	if r.Method == "POST" {
		passwd := r.PostFormValue("passwd")
		passwdConfirm := r.PostFormValue("confirm")
		if err := validatePasswd(passwd, passwdConfirm); err != nil {
			sess.SetFlashMsg(err.Error())
			http.Redirect(w, r, "/resetpass?r="+resetToken, http.StatusSeeOther)
			return
		}
		models.UpdateUserPasswd(userName, passwd)
		sess.SetFlashMsg("Password change successful.")
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	templates.Render(w, "resetpass.html", map[string]interface{}{
		"ResetToken": resetToken,
		"Common":     readCommonData(r, sess),
	})
})
View Source
var SignupHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	redirectURL, err := url.QueryUnescape(r.FormValue("next"))
	if redirectURL == "" || err != nil {
		redirectURL = "/"
	}
	if sess.IsUserValid() && !sess.IsUserSuperAdmin() {
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
		return
	}

	isSignupDisabled := models.Config(models.SignupDisabled) != "0"

	if r.Method == "POST" {
		userName := strings.TrimSpace(r.PostFormValue("username"))
		passwd := r.PostFormValue("passwd")
		passwdConfirm := r.PostFormValue("confirm")
		email := strings.TrimSpace(r.PostFormValue("email"))
		if len(userName) < 2 || len(userName) > 32 {
			sess.SetFlashMsg("Username should have 2-32 characters.")
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		if censored := censor(userName); censored != userName {
			sess.SetFlashMsg("Fix username: " + censored)
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		hasSpecial := false
		for _, ch := range userName {
			if (ch < 'A' || ch > 'Z') && (ch < 'a' || ch > 'z') && ch != '_' && (ch < '0' || ch > '9') {
				hasSpecial = true
			}
		}
		if hasSpecial {
			sess.SetFlashMsg("Username can contain only alphabets, numbers, and underscore.")
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		if models.ProbeUser(userName) {
			sess.SetFlashMsg("Username already registered.")
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		if err := validatePasswd(passwd, passwdConfirm); err != nil {
			sess.SetFlashMsg(err.Error())
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		if len(email) > 64 {
			sess.SetFlashMsg("Email should have fewer than 64 characters.")
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		if isSignupDisabled && !sess.IsUserSuperAdmin() {
			ErrForbiddenHandler(w, r)
			return
		}
		models.CreateUser(userName, passwd, email)
		if sess.IsUserSuperAdmin() {
			sess.SetFlashMsg("User " + userName + " created")
			http.Redirect(w, r, "/signup", http.StatusSeeOther)
			return
		}
		sess.Authenticate(userName, passwd)
		http.Redirect(w, r, redirectURL, http.StatusSeeOther)
	}
	templates.Render(w, "signup.html", map[string]interface{}{
		"Common":     readCommonData(r, sess),
		"next":       template.URL(url.QueryEscape(redirectURL)),
		"IsDisabled": isSignupDisabled && !sess.IsUserSuperAdmin(),
		"SignupMsg":  models.Config(models.SignupMsg),
	})
})
View Source
var TopicCreateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	groupID := r.FormValue("gid")
	var groupName string
	isGroupClosed := 1
	db.QueryRow(`SELECT name, is_closed FROM groups WHERE id=?;`, groupID).Scan(&groupName, &isGroupClosed)
	if isGroupClosed == 1 {
		ErrForbiddenHandler(w, r)
		return
	}

	var tmp int
	isMod := db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?`, sess.UserID).Scan(&isSuperAdmin)

	if r.Method == "POST" {
		title := strings.TrimSpace(r.PostFormValue("title"))
		content := strings.TrimSpace(r.PostFormValue("content"))
		isSticky := r.PostFormValue("is_sticky") != ""
		if len(title) < 8 || len(title) > 80 {
			sess.SetFlashMsg("Title should have 8-80 characters.")
			http.Redirect(w, r, "/topics/new?gid="+groupID, http.StatusSeeOther)
			return
		}
		if len(content) > 5000 {
			sess.SetFlashMsg("Content should have less than 5000 characters.")
			http.Redirect(w, r, "/topics/new?gid="+groupID, http.StatusSeeOther)
			return
		}
		db.Exec(`INSERT INTO topics(title, content, userid, groupid, is_sticky, created_date, updated_date, activity_date) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
			title, content, sess.UserID, groupID, isSticky, int(time.Now().Unix()), int(time.Now().Unix()), int(time.Now().Unix()))

		if models.Config(models.AllowGroupSubscription) != "0" {
			groupURL := "http://" + r.Host + "/groups?name=" + groupName
			rows := db.Query(`SELECT users.email, groupsubscriptions.token FROM users INNER JOIN groupsubscriptions ON users.id=groupsubscriptions.userid AND groupsubscriptions.groupid=?;`, groupID)
			for rows.Next() {
				var email, token string
				rows.Scan(&email, &token)
				if email != "" {
					unSubURL := "http://" + r.Host + "/groups/unsubscribe?token=" + token
					utils.SendMail(email, `New topic in `+groupName,
						"A new topic titled \""+title+"\" has been posted to "+groupName+".\r\nSee topics posted to the group at "+groupURL+"\r\n\r\nIf you do not want these emails, unsubscribe by following this link: "+unSubURL)
				}
			}
		}
		http.Redirect(w, r, "/groups?name="+groupName, http.StatusSeeOther)
		return
	}

	templates.Render(w, "topicedit.html", map[string]interface{}{
		"Common":       readCommonData(r, sess),
		"GroupID":      groupID,
		"GroupName":    groupName,
		"TopicID":      "",
		"Title":        "",
		"Content":      "",
		"IsSticky":     false,
		"IsClosed":     false,
		"IsDeleted":    false,
		"IsMod":        isMod,
		"IsAdmin":      isAdmin,
		"IsSuperAdmin": isSuperAdmin,
	})
})
View Source
var TopicIndexHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	topicID := r.FormValue("id")
	page64, err := strconv.ParseInt(r.FormValue("p"), 10, 64)
	if err != nil {
		page64 = 0
	}
	page := int(page64)
	if page < 0 {
		page = 0
	}
	var title, content, groupID, groupName string
	var isDeleted, isClosed bool
	var ownerID, createdDate int64
	if db.QueryRow(`SELECT title, content, userid, groupid, is_deleted, is_closed, created_date FROM topics WHERE id=?;`, topicID).Scan(
		&title, &content, &ownerID, &groupID, &isDeleted, &isClosed, &createdDate) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	if isDeleted {
		ErrNotFoundHandler(w, r)
		return
	}
	var ownerName string
	db.QueryRow(`SELECT username FROM users WHERE id=?;`, ownerID).Scan(&ownerName)

	subToken := ""
	if sess.UserID.Valid {
		db.QueryRow(`SELECT token FROM topicsubscriptions WHERE topicid=? AND userid=?;`, topicID, sess.UserID).Scan(&subToken)
	}

	var lastPos int
	db.QueryRow(`SELECT pos FROM comments WHERE topicid=? ORDER BY pos DESC LIMIT 1;`, topicID).Scan(&lastPos)
	isLastPage := (lastPos < (page+1)*numCommentsPerPage)
	numPages := 0
	if lastPos > 0 {
		numPages = 1 + lastPos/numCommentsPerPage
	}

	type Comment struct {
		ID          string
		Content     template.HTML
		ImgSrc      string
		CreatedDate string
		UserName    string
		IsOwner     bool
		IsDeleted   bool
	}

	var comments []Comment
	var cDate int64
	var rows *db.Rows
	if page == 0 {
		rows = db.Query(`SELECT users.id, users.username, comments.id, comments.content, comments.image, comments.is_deleted, comments.created_date FROM comments INNER JOIN users ON comments.userid=users.id AND comments.topicid=? AND comments.pos < ? ORDER BY comments.pos;`, topicID, numCommentsPerPage)
	} else {
		rows = db.Query(`SELECT users.id, users.username, comments.id, comments.content, comments.image, comments.is_deleted, comments.created_date FROM comments INNER JOIN users ON comments.userid=users.id AND comments.topicid=? AND comments.pos >= ? AND comments.pos < ? ORDER BY comments.pos;`, topicID, page*numCommentsPerPage, (page+1)*numCommentsPerPage)
	}
	for rows.Next() {
		comments = append(comments, Comment{})
		c := &comments[len(comments)-1]
		var ownerID int64
		var content string
		rows.Scan(&ownerID, &c.UserName, &c.ID, &content, &c.ImgSrc, &c.IsDeleted, &cDate)
		c.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
		c.IsOwner = sess.UserID.Valid && (ownerID == sess.UserID.Int64)
		c.Content = formatComment(content)
	}

	var tmp string
	db.QueryRow(`SELECT name FROM groups WHERE id=?;`, groupID).Scan(&groupName)
	isMod := db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?`, sess.UserID).Scan(&isSuperAdmin)
	isOwner := sess.UserID.Valid && ownerID == sess.UserID.Int64

	commonData := readCommonData(r, sess)
	commonData.PageTitle = censor(title)

	templates.Render(w, "topicindex.html", map[string]interface{}{
		"Common":               commonData,
		"GroupID":              groupID,
		"TopicID":              topicID,
		"GroupName":            groupName,
		"TopicName":            censor(title),
		"OwnerName":            ownerName,
		"CreatedDate":          timeAgoFromNow(time.Unix(createdDate, 0)),
		"SubToken":             subToken,
		"Title":                title,
		"Content":              formatComment(content),
		"IsClosed":             isClosed,
		"IsOwner":              isOwner,
		"IsMod":                isMod,
		"IsAdmin":              isAdmin,
		"IsSuperAdmin":         isSuperAdmin,
		"IsImageUploadEnabled": models.Config(models.ImageUploadEnabled) != "0",
		"Comments":             comments,
		"IsLastPage":           isLastPage,
		"NextPage":             page + 1,
		"CurrentPage":          page,
		"Pages":                make([]int, numPages),
		"NumPages":             numPages,
	})
})
View Source
var TopicSubscribeHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	topicID := r.FormValue("id")
	if models.Config(models.AllowTopicSubscription) == "0" {
		ErrForbiddenHandler(w, r)
		return
	}
	var tmp string
	if db.QueryRow(`SELECT id FROM topics WHERE id=?;`, topicID).Scan(&tmp) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	if r.Method == "POST" {
		var tmp string
		if db.QueryRow(`SELECT id FROM topicsubscriptions WHERE userid=? AND topicid=?;`, sess.UserID, topicID).Scan(&tmp) != nil {
			db.Exec(`INSERT INTO topicsubscriptions(userid, topicid, token, created_date) VALUES(?, ?, ?, ?);`,
				sess.UserID, topicID, randSeq(64), time.Now().Unix())
		}
	}
	http.Redirect(w, r, "/topics?id="+topicID, http.StatusSeeOther)
})
View Source
var TopicUnsubscribeHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	token := r.FormValue("token")
	var topicID, topicName string
	if db.QueryRow(`SELECT topicid FROM topicsubscriptions WHERE token=?;`, token).Scan(&topicID) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	db.QueryRow(`SELECT title FROM topics WHERE id=?;`, topicID).Scan(&topicName)
	if r.Method == "POST" {
		db.Exec(`DELETE FROM topicsubscriptions WHERE token=?;`, token)
		if r.PostFormValue("noredirect") != "" {
			w.Write([]byte("Unsubscribed."))
		} else {
			http.Redirect(w, r, "/topics?id="+topicID, http.StatusSeeOther)
		}
		return
	}
	w.Write([]byte(`<!DOCTYPE html><html><head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1"></head>
	<body><form action="/topics/unsubscribe" method="POST">
	Unsubscribe from ` + topicName + `?
	<input type="hidden" name="token" value="` + token + `">
	<input type="hidden" name="csrf" value="` + sess.CSRFToken + `">
	<input type="hidden" name="noredirect" value="1">
	<input type="submit" value="Unsubscribe">
	</form></body></html>`))
})
View Source
var TopicUpdateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	topicID := r.FormValue("id")
	groupID := ""
	title := strings.TrimSpace(r.PostFormValue("title"))
	content := strings.TrimSpace(r.PostFormValue("content"))
	action := r.PostFormValue("action")
	isSticky := r.PostFormValue("is_sticky") != ""
	isClosed := true
	isDeleted := true

	if db.QueryRow(`SELECT groupid FROM topics WHERE id=?;`, topicID).Scan(&groupID) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	isGroupClosed := 1
	var groupName string
	db.QueryRow(`SELECT name, is_closed FROM groups WHERE id=?;`, groupID).Scan(&groupName, &isGroupClosed)
	if isGroupClosed == 1 {
		ErrForbiddenHandler(w, r)
		return
	}

	var tmp int
	var uID int64
	db.QueryRow(`SELECT userid FROM topics WHERE id=?;`, topicID).Scan(&uID)

	isOwner := (uID == sess.UserID.Int64)
	isMod := db.QueryRow(`SELECT id FROM mods WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isAdmin := db.QueryRow(`SELECT id FROM admins WHERE groupid=? AND userid=?;`, groupID, sess.UserID).Scan(&tmp) == nil
	isSuperAdmin := false
	db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?`, sess.UserID).Scan(&isSuperAdmin)

	if !isMod && !isAdmin && !isSuperAdmin {
		db.QueryRow(`SELECT is_sticky FROM topics WHERE id=?;`, topicID).Scan(&isSticky)
		if !isOwner {
			ErrForbiddenHandler(w, r)
			return
		}
	}

	if r.Method == "POST" {
		if len(title) < 8 || len(title) > 80 {
			sess.SetFlashMsg("Title should have 8-80 characters.")
			http.Redirect(w, r, "/topics/edit?id="+topicID, http.StatusSeeOther)
			return
		}
		if len(content) > 5000 {
			sess.SetFlashMsg("Content should have less than 5000 characters.")
			http.Redirect(w, r, "/topics/edit?id="+topicID, http.StatusSeeOther)
			return
		}
		if action == "Update" {
			db.Exec(`UPDATE topics SET title=?, content=?, is_sticky=?, updated_date=? WHERE id=?;`, title, content, isSticky, int(time.Now().Unix()), topicID)
		} else if action == "Close" && (isMod || isAdmin || isSuperAdmin) {
			db.Exec(`UPDATE topics SET is_closed=1 WHERE id=?;`, topicID)
		} else if action == "Reopen" && (isMod || isAdmin || isSuperAdmin) {
			db.Exec(`UPDATE topics SET is_closed=0 WHERE id=?;`, topicID)
		} else if action == "Delete" {
			db.Exec(`UPDATE topics SET is_deleted=1 WHERE id=?;`, topicID)
			http.Redirect(w, r, "/topics/edit?id="+topicID, http.StatusSeeOther)
			return
		} else if action == "Undelete" {
			db.Exec(`UPDATE topics SET is_deleted=0 WHERE id=?;`, topicID)
		}
		http.Redirect(w, r, "/topics?id="+topicID, http.StatusSeeOther)
		return
	}

	if db.QueryRow(`SELECT title, content, is_sticky, is_deleted, is_closed FROM topics WHERE id=?;`, topicID).Scan(&title, &content, &isSticky, &isDeleted, &isClosed) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	templates.Render(w, "topicedit.html", map[string]interface{}{
		"Common":       readCommonData(r, sess),
		"GroupID":      groupID,
		"GroupName":    groupName,
		"TopicID":      topicID,
		"Title":        title,
		"Content":      content,
		"IsSticky":     isSticky,
		"IsClosed":     isClosed,
		"IsDeleted":    isDeleted,
		"IsMod":        isMod,
		"IsAdmin":      isAdmin,
		"IsSuperAdmin": isSuperAdmin,
	})
})
View Source
var UserCommentsHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	ownerName := r.FormValue("u")
	lastCommentDate, err := strconv.ParseInt(r.FormValue("lcd"), 10, 64)

	if err != nil {
		lastCommentDate = 0
	}

	var ownerID string
	if db.QueryRow(`SELECT id FROM users WHERE username=?;`, ownerName).Scan(&ownerID) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	type Comment struct {
		ID          string
		Content     template.HTML
		TopicID     string
		TopicName   string
		CreatedDate string
		ImgSrc      string
		IsDeleted   bool
	}

	commentsPerPage := 50

	var comments []Comment
	var rows *db.Rows
	if lastCommentDate == 0 {
		rows = db.Query(`SELECT topics.title, comments.topicid, comments.id, comments.content, comments.image, comments.created_date, comments.is_deleted FROM comments INNER JOIN topics ON topics.id = comments.topicid AND comments.userid=? ORDER BY comments.created_date DESC LIMIT ?;`, ownerID, commentsPerPage)
	} else {
		rows = db.Query(`SELECT topics.title, comments.topicid, comments.id, comments.content, comments.image, comments.created_date, comments.is_deleted FROM comments INNER JOIN topics ON topics.id = comments.topicid AND comments.userid=? AND comments.created_date < ? ORDER BY comments.created_date DESC LIMIT ?;`, ownerID, lastCommentDate, commentsPerPage)
	}

	var cDate int64
	for rows.Next() {
		comments = append(comments, Comment{})
		c := &comments[len(comments)-1]

		var content string
		rows.Scan(&c.TopicName, &c.TopicID, &c.ID, &content, &c.ImgSrc, &cDate, &c.IsDeleted)
		c.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
		c.Content = formatComment(content)
	}

	if len(comments) >= commentsPerPage {
		lastCommentDate = cDate
	} else {
		lastCommentDate = 0
	}

	templates.Render(w, "profilecomments.html", map[string]interface{}{
		"Common":          readCommonData(r, sess),
		"OwnerName":       ownerName,
		"Comments":        comments,
		"LastCommentDate": lastCommentDate,
	})
})
View Source
var UserGroupsHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	ownerID := sess.UserID.Int64
	var ownerName string

	type Group struct {
		ID          string
		Name        string
		IsClosed    bool
		CreatedDate string
	}
	var adminInGroups []Group
	rows := db.Query(`SELECT groups.id, groups.name, groups.is_closed, groups.created_date FROM groups INNER JOIN admins ON admins.groupid=groups.id AND admins.userid=?;`, ownerID)
	for rows.Next() {
		adminInGroups = append(adminInGroups, Group{})
		g := &adminInGroups[len(adminInGroups)-1]
		var cDate int64
		rows.Scan(&g.ID, &g.Name, &g.IsClosed, &cDate)
		g.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
	}

	var modInGroups []Group
	rows = db.Query(`SELECT groups.id, groups.name, groups.is_closed, groups.created_date FROM groups INNER JOIN mods ON mods.groupid=groups.id AND mods.userid=?;`, ownerID)
	for rows.Next() {
		modInGroups = append(modInGroups, Group{})
		g := &adminInGroups[len(adminInGroups)-1]
		var cDate int64
		rows.Scan(&g.ID, &g.Name, &g.IsClosed, &cDate)
		g.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
	}

	templates.Render(w, "profilegroups.html", map[string]interface{}{
		"Common":        readCommonData(r, sess),
		"OwnerName":     ownerName,
		"AdminInGroups": adminInGroups,
		"ModInGroups":   modInGroups,
	})
})
View Source
var UserProfileHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	userName := r.FormValue("u")
	var about, email string
	var isBanned bool
	var userID int64
	if db.QueryRow(`SELECT id, about, email, is_banned FROM users WHERE username=?;`, userName).Scan(&userID, &about, &email, &isBanned) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	templates.Render(w, "profile.html", map[string]interface{}{
		"Common":   readCommonData(r, sess),
		"UserName": userName,
		"About":    about,
		"Email":    email,
		"IsSelf":   sess.UserID.Valid && (userID == sess.UserID.Int64),
		"IsBanned": isBanned,
	})
})
View Source
var UserProfileUpdateHandler = A(func(w http.ResponseWriter, r *http.Request, sess Session) {
	userName := r.FormValue("u")
	var about, email string
	var isBanned bool
	var userID int64
	if db.QueryRow(`SELECT id, about, email, is_banned FROM users WHERE username=?;`, userName).Scan(&userID, &about, &email, &isBanned) != nil {
		ErrNotFoundHandler(w, r)
		return
	}

	if r.Method == "POST" {
		if !sess.UserID.Valid {
			ErrForbiddenHandler(w, r)
			return
		}
		action := r.PostFormValue("action")
		var isSuperAdmin bool
		db.QueryRow(`SELECT is_superadmin FROM users WHERE id=?;`, sess.UserID).Scan(&isSuperAdmin)
		if action == "Update" {
			if isSuperAdmin || userID == sess.UserID.Int64 {
				email := strings.TrimSpace(r.FormValue("email"))
				about := r.FormValue("about")
				if len(email) > 64 {
					sess.SetFlashMsg("Email should have fewer than 64 characters.")
					http.Redirect(w, r, "/users?u="+userName, http.StatusSeeOther)
					return
				}
				if len(about) > 1024 {
					sess.SetFlashMsg("About should have fewer than 1024 characters.")
					http.Redirect(w, r, "/users?u="+userName, http.StatusSeeOther)
					return
				}
				db.Exec(`UPDATE users SET email=?, about=? WHERE id=?;`, email, about, userID)
			} else {
				ErrForbiddenHandler(w, r)
				return
			}
		} else if action == "Ban" {
			if isSuperAdmin {
				db.Exec(`UPDATE users SET is_banned=1 WHERE id=?;`, userID)
				db.Exec(`DELETE FROM sessions WHERE userid=?;`, userID)
			} else {
				ErrForbiddenHandler(w, r)
				return
			}
		} else if action == "Unban" {
			if isSuperAdmin {
				db.Exec(`UPDATE users SET is_banned=0 WHERE id=?;`, userID)
			} else {
				ErrForbiddenHandler(w, r)
				return
			}
		}
	}
	sess.SetFlashMsg("Update successful.")
	http.Redirect(w, r, "/users?u="+userName, http.StatusSeeOther)
})
View Source
var UserTopicsHandler = UA(func(w http.ResponseWriter, r *http.Request, sess Session) {
	ownerName := r.FormValue("u")
	var ownerID string
	if db.QueryRow(`SELECT id FROM users WHERE username=?;`, ownerName).Scan(&ownerID) != nil {
		ErrNotFoundHandler(w, r)
		return
	}
	lastTopicDate, err := strconv.ParseInt(r.FormValue("ltd"), 10, 64)
	if err != nil {
		lastTopicDate = 0
	}

	numTopicsPerPage := 50
	type Topic struct {
		ID          string
		Title       string
		IsClosed    bool
		IsDeleted   bool
		CreatedDate string
	}
	var topics []Topic
	var rows *db.Rows
	var cDate int64
	if lastTopicDate == 0 {
		rows = db.Query(`SELECT id, title, is_deleted, is_closed, created_date FROM topics WHERE userid=? ORDER BY created_date DESC LIMIT ?;`, ownerID, numTopicsPerPage)
	} else {
		rows = db.Query(`SELECT id, title, is_deleted, is_closed, created_date FROM topics WHERE userid=? AND created_date < ? ORDER BY created_date DESC LIMIT ?;`, ownerID, lastTopicDate, numTopicsPerPage)
	}
	for rows.Next() {
		topics = append(topics, Topic{})
		t := &topics[len(topics)-1]
		rows.Scan(&t.ID, &t.Title, &t.IsDeleted, &t.IsClosed, &cDate)
		t.CreatedDate = timeAgoFromNow(time.Unix(cDate, 0))
		t.Title = censor(t.Title)
	}

	if len(topics) >= numTopicsPerPage {
		lastTopicDate = cDate
	} else {
		lastTopicDate = 0
	}

	templates.Render(w, "profiletopics.html", map[string]interface{}{
		"Common":        readCommonData(r, sess),
		"OwnerName":     ownerName,
		"Topics":        topics,
		"LastTopicDate": lastTopicDate,
	})
})

Functions

func A

func A(handler func(w http.ResponseWriter, r *http.Request, sess Session)) func(w http.ResponseWriter, r *http.Request)

func Authenticate added in v1.1.0

func Authenticate() error

func ClearSession added in v1.1.0

func ClearSession(w http.ResponseWriter, r *http.Request)

func ErrForbiddenHandler

func ErrForbiddenHandler(w http.ResponseWriter, r *http.Request)

func ErrNotFoundHandler

func ErrNotFoundHandler(w http.ResponseWriter, r *http.Request)

func ErrServerHandler

func ErrServerHandler(w http.ResponseWriter, r *http.Request)

func FaviconHandler

func FaviconHandler(w http.ResponseWriter, r *http.Request)

func ImageHandler

func ImageHandler(w http.ResponseWriter, r *http.Request)

func LogoutHandler

func LogoutHandler(w http.ResponseWriter, r *http.Request)

func ScriptHandler added in v1.1.0

func ScriptHandler(w http.ResponseWriter, r *http.Request)

func StyleHandler added in v1.1.0

func StyleHandler(w http.ResponseWriter, r *http.Request)

func TestHandler

func TestHandler(w http.ResponseWriter, r *http.Request)

func UA

func UA(handler func(w http.ResponseWriter, r *http.Request, sess Session)) func(w http.ResponseWriter, r *http.Request)

Types

type CommonData added in v1.1.0

type CommonData struct {
	CSRF              string
	Msg               string
	UserName          string
	IsSuperAdmin      bool
	IsNotification    bool
	ForumName         string
	PageTitle         string
	CurrentURL        template.URL
	BodyAppendage     string
	IsGroupSubAllowed bool
	IsTopicSubAllowed bool
	ExtraNotesShort   []ExtraNote
}

type ExtraNote added in v1.1.0

type ExtraNote struct {
	ID          int
	Name        string
	Content     string
	URL         string
	CreatedDate time.Time
	UpdatedDate time.Time
}

type Session added in v1.1.0

type Session struct {
	SessionID   string
	UserID      sql.NullInt64
	CSRFToken   string
	Msg         string
	CreatedDate time.Time
	UpdatedDate time.Time
}

func OpenSession added in v1.1.0

func OpenSession(w http.ResponseWriter, r *http.Request) Session

func (*Session) Authenticate added in v1.1.0

func (sess *Session) Authenticate(userName string, passwd string) error

func (*Session) FlashMsg added in v1.1.0

func (sess *Session) FlashMsg() string

func (*Session) IsUserSuperAdmin added in v1.1.0

func (sess *Session) IsUserSuperAdmin() bool

func (*Session) IsUserValid added in v1.1.0

func (sess *Session) IsUserValid() bool

func (*Session) SetFlashMsg added in v1.1.0

func (sess *Session) SetFlashMsg(msg string)

func (*Session) UserName added in v1.1.0

func (sess *Session) UserName() (string, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL