2017-11-11 04:06:16 +00:00
package common
2017-09-10 16:57:22 +00:00
2017-09-10 17:05:13 +00:00
import (
"html"
"net"
"net/http"
2018-01-14 12:03:20 +00:00
"strconv"
2017-09-10 17:05:13 +00:00
"strings"
)
// nolint
var PreRoute func ( http . ResponseWriter , * http . Request ) ( User , bool ) = preRoute
2017-09-15 22:20:01 +00:00
// TODO: Come up with a better middleware solution
2017-09-10 17:05:13 +00:00
// nolint We need these types so people can tell what they are without scrolling to the bottom of the file
2018-04-22 12:33:56 +00:00
var PanelUserCheck func ( http . ResponseWriter , * http . Request , * User ) ( * Header , PanelStats , RouteError ) = panelUserCheck
2017-11-11 04:06:16 +00:00
var SimplePanelUserCheck func ( http . ResponseWriter , * http . Request , * User ) ( * HeaderLite , RouteError ) = simplePanelUserCheck
var SimpleForumUserCheck func ( w http . ResponseWriter , r * http . Request , user * User , fid int ) ( headerLite * HeaderLite , err RouteError ) = simpleForumUserCheck
2018-04-22 12:33:56 +00:00
var ForumUserCheck func ( w http . ResponseWriter , r * http . Request , user * User , fid int ) ( header * Header , err RouteError ) = forumUserCheck
2017-11-11 04:06:16 +00:00
var SimpleUserCheck func ( w http . ResponseWriter , r * http . Request , user * User ) ( headerLite * HeaderLite , err RouteError ) = simpleUserCheck
2018-04-22 12:33:56 +00:00
var UserCheck func ( w http . ResponseWriter , r * http . Request , user * User ) ( header * Header , err RouteError ) = userCheck
2017-09-22 02:21:17 +00:00
2017-11-11 04:06:16 +00:00
func simpleForumUserCheck ( w http . ResponseWriter , r * http . Request , user * User , fid int ) ( headerLite * HeaderLite , rerr RouteError ) {
2017-11-23 05:37:08 +00:00
if ! Forums . Exists ( fid ) {
2017-10-30 09:57:08 +00:00
return nil , PreError ( "The target forum doesn't exist." , w , r )
2017-09-10 17:05:13 +00:00
}
// Is there a better way of doing the skip AND the success flag on this hook like multiple returns?
2017-11-11 04:06:16 +00:00
if VhookSkippable [ "simple_forum_check_pre_perms" ] != nil {
2017-11-02 02:52:21 +00:00
var skip bool
2017-11-11 04:06:16 +00:00
skip , rerr = RunVhookSkippable ( "simple_forum_check_pre_perms" , w , r , user , & fid , & headerLite )
2017-11-02 04:12:51 +00:00
if skip || rerr != nil {
2017-10-30 09:57:08 +00:00
return headerLite , rerr
2017-09-10 17:05:13 +00:00
}
}
2017-11-23 05:37:08 +00:00
fperms , err := FPStore . Get ( fid , user . Group )
2018-04-24 04:09:31 +00:00
if err == ErrNoRows {
fperms = BlankForumPerms ( )
} else if err != nil {
2018-04-24 03:47:39 +00:00
return headerLite , InternalError ( err , w , r )
2017-09-15 22:20:01 +00:00
}
2017-10-31 07:26:44 +00:00
cascadeForumPerms ( fperms , user )
2017-10-30 09:57:08 +00:00
return headerLite , nil
2017-09-10 17:05:13 +00:00
}
2018-04-22 12:33:56 +00:00
func forumUserCheck ( w http . ResponseWriter , r * http . Request , user * User , fid int ) ( header * Header , rerr RouteError ) {
header , rerr = UserCheck ( w , r , user )
2017-11-02 02:52:21 +00:00
if rerr != nil {
2018-04-22 12:33:56 +00:00
return header , rerr
2017-10-30 09:57:08 +00:00
}
2017-11-23 05:37:08 +00:00
if ! Forums . Exists ( fid ) {
2018-04-22 12:33:56 +00:00
return header , NotFound ( w , r , header )
2017-09-10 17:05:13 +00:00
}
2017-11-11 04:06:16 +00:00
if VhookSkippable [ "forum_check_pre_perms" ] != nil {
2017-11-02 02:52:21 +00:00
var skip bool
2018-04-22 12:33:56 +00:00
skip , rerr = RunVhookSkippable ( "forum_check_pre_perms" , w , r , user , & fid , & header )
2017-11-02 04:12:51 +00:00
if skip || rerr != nil {
2018-04-22 12:33:56 +00:00
return header , rerr
2017-09-10 17:05:13 +00:00
}
}
2017-11-23 05:37:08 +00:00
fperms , err := FPStore . Get ( fid , user . Group )
2018-04-24 04:09:31 +00:00
if err == ErrNoRows {
fperms = BlankForumPerms ( )
} else if err != nil {
2018-04-24 03:47:39 +00:00
return header , InternalError ( err , w , r )
2017-09-15 22:20:01 +00:00
}
2017-10-31 07:26:44 +00:00
cascadeForumPerms ( fperms , user )
2018-06-01 05:02:29 +00:00
header . CurrentUser = * user // TODO: Use a pointer instead for CurrentUser, so we don't have to do this
2018-04-22 12:33:56 +00:00
return header , rerr
2017-10-31 07:26:44 +00:00
}
// TODO: Put this on the user instance? Do we really want forum specific logic in there? Maybe, a method which spits a new pointer with the same contents as user?
2017-11-13 05:22:37 +00:00
func cascadeForumPerms ( fperms * ForumPerms , user * User ) {
2017-09-10 17:05:13 +00:00
if fperms . Overrides && ! user . IsSuperAdmin {
user . Perms . ViewTopic = fperms . ViewTopic
user . Perms . LikeItem = fperms . LikeItem
user . Perms . CreateTopic = fperms . CreateTopic
user . Perms . EditTopic = fperms . EditTopic
user . Perms . DeleteTopic = fperms . DeleteTopic
user . Perms . CreateReply = fperms . CreateReply
user . Perms . EditReply = fperms . EditReply
user . Perms . DeleteReply = fperms . DeleteReply
user . Perms . PinTopic = fperms . PinTopic
user . Perms . CloseTopic = fperms . CloseTopic
2018-01-15 08:24:18 +00:00
user . Perms . MoveTopic = fperms . MoveTopic
2017-09-10 17:05:13 +00:00
if len ( fperms . ExtData ) != 0 {
for name , perm := range fperms . ExtData {
user . PluginPerms [ name ] = perm
}
}
}
}
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
// TODO: Do a panel specific theme?
2018-04-22 12:33:56 +00:00
func panelUserCheck ( w http . ResponseWriter , r * http . Request , user * User ) ( header * Header , stats PanelStats , rerr RouteError ) {
2017-12-01 02:04:29 +00:00
var theme = & Theme { Name : "" }
2017-09-10 17:05:13 +00:00
cookie , err := r . Cookie ( "current_theme" )
if err == nil {
2017-11-23 05:37:08 +00:00
inTheme , ok := Themes [ html . EscapeString ( cookie . Value ) ]
2017-09-10 17:05:13 +00:00
if ok && ! theme . HideFromThemes {
2017-11-23 05:37:08 +00:00
theme = inTheme
2017-09-10 17:05:13 +00:00
}
}
2017-11-23 05:37:08 +00:00
if theme . Name == "" {
theme = Themes [ DefaultThemeBox . Load ( ) . ( string ) ]
}
2017-09-10 17:05:13 +00:00
2018-04-22 12:33:56 +00:00
header = & Header {
Site : Site ,
Settings : SettingBox . Load ( ) . ( SettingMap ) ,
Themes : Themes ,
Theme : theme ,
CurrentUser : * user ,
Zone : "panel" ,
Writer : w ,
2017-09-10 17:05:13 +00:00
}
2018-04-22 12:33:56 +00:00
// TODO: We should probably initialise header.ExtData
2017-09-10 17:05:13 +00:00
2018-04-22 12:33:56 +00:00
header . AddSheet ( theme . Name + "/panel.css" )
2017-11-23 05:37:08 +00:00
if len ( theme . Resources ) > 0 {
rlist := theme . Resources
2017-09-10 17:05:13 +00:00
for _ , resource := range rlist {
if resource . Location == "global" || resource . Location == "panel" {
2017-10-12 03:24:14 +00:00
extarr := strings . Split ( resource . Name , "." )
ext := extarr [ len ( extarr ) - 1 ]
if ext == "css" {
2018-04-22 12:33:56 +00:00
header . AddSheet ( resource . Name )
2017-10-12 03:24:14 +00:00
} else if ext == "js" {
2018-04-22 12:33:56 +00:00
header . AddScript ( resource . Name )
2017-09-10 17:05:13 +00:00
}
}
}
}
2018-06-06 00:21:22 +00:00
// TODO: GDPR. Add a global control panel notice warning the admins of staff members who don't have 2FA enabled
2017-11-11 04:06:16 +00:00
stats . Users = Users . GlobalCount ( )
2017-11-23 05:37:08 +00:00
stats . Groups = Groups . GlobalCount ( )
2018-06-06 00:21:22 +00:00
stats . Forums = Forums . GlobalCount ( ) // TODO: Stop it from showing the blanked forums, do we still have those? I think we removed that
stats . Pages = Pages . GlobalCount ( )
2018-04-22 12:33:56 +00:00
stats . Settings = len ( header . Settings )
2017-11-11 04:06:16 +00:00
stats . WordFilters = len ( WordFilterBox . Load ( ) . ( WordFilterMap ) )
stats . Themes = len ( Themes )
2017-09-10 17:05:13 +00:00
stats . Reports = 0 // TODO: Do the report count. Only show open threads?
2018-06-06 00:21:22 +00:00
// TODO: Remove this as it might be counter-productive
2017-09-10 17:05:13 +00:00
pusher , ok := w . ( http . Pusher )
if ok {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + theme . Name + "/main.css" , nil )
pusher . Push ( "/static/" + theme . Name + "/panel.css" , nil )
2017-09-10 17:05:13 +00:00
pusher . Push ( "/static/global.js" , nil )
pusher . Push ( "/static/jquery-3.1.1.min.js" , nil )
2017-11-23 05:37:08 +00:00
// TODO: Test these
2018-04-22 12:33:56 +00:00
for _ , sheet := range header . Stylesheets {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + sheet , nil )
}
2018-04-22 12:33:56 +00:00
for _ , script := range header . Scripts {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + script , nil )
}
2017-09-10 17:05:13 +00:00
// TODO: Push avatars?
}
2018-04-22 12:33:56 +00:00
return header , stats , nil
2017-09-10 17:05:13 +00:00
}
2017-11-11 04:06:16 +00:00
func simplePanelUserCheck ( w http . ResponseWriter , r * http . Request , user * User ) ( headerLite * HeaderLite , rerr RouteError ) {
2018-05-27 09:36:35 +00:00
return simpleUserCheck ( w , r , user )
2017-09-15 22:20:01 +00:00
}
2017-09-10 17:39:16 +00:00
// SimpleUserCheck is back from the grave, yay :D
2017-11-11 04:06:16 +00:00
func simpleUserCheck ( w http . ResponseWriter , r * http . Request , user * User ) ( headerLite * HeaderLite , rerr RouteError ) {
2018-05-27 09:36:35 +00:00
return & HeaderLite {
2017-11-11 04:06:16 +00:00
Site : Site ,
Settings : SettingBox . Load ( ) . ( SettingMap ) ,
2018-05-27 09:36:35 +00:00
} , nil
2017-09-10 17:05:13 +00:00
}
// TODO: Add the ability for admins to restrict certain themes to certain groups?
2018-04-22 12:33:56 +00:00
func userCheck ( w http . ResponseWriter , r * http . Request , user * User ) ( header * Header , rerr RouteError ) {
2017-12-01 02:04:29 +00:00
var theme = & Theme { Name : "" }
2017-09-10 17:05:13 +00:00
cookie , err := r . Cookie ( "current_theme" )
if err == nil {
2017-11-23 05:37:08 +00:00
inTheme , ok := Themes [ html . EscapeString ( cookie . Value ) ]
2017-09-10 17:05:13 +00:00
if ok && ! theme . HideFromThemes {
2017-11-23 05:37:08 +00:00
theme = inTheme
2017-09-10 17:05:13 +00:00
}
}
2017-11-23 05:37:08 +00:00
if theme . Name == "" {
theme = Themes [ DefaultThemeBox . Load ( ) . ( string ) ]
}
2017-09-10 17:05:13 +00:00
2018-04-22 12:33:56 +00:00
header = & Header {
Site : Site ,
Settings : SettingBox . Load ( ) . ( SettingMap ) ,
Themes : Themes ,
Theme : theme ,
2018-06-01 05:02:29 +00:00
CurrentUser : * user , // ! Some things rely on this being a pointer downstream from this function
2018-04-22 12:33:56 +00:00
Zone : "frontend" ,
Writer : w ,
2017-09-10 17:05:13 +00:00
}
if user . IsBanned {
2018-04-22 12:33:56 +00:00
header . NoticeList = append ( header . NoticeList , GetNoticePhrase ( "account_banned" ) )
2017-09-10 17:05:13 +00:00
}
2018-03-08 03:59:47 +00:00
if user . Loggedin && ! user . Active {
2018-04-22 12:33:56 +00:00
header . NoticeList = append ( header . NoticeList , GetNoticePhrase ( "account_inactive" ) )
2018-03-08 03:59:47 +00:00
}
2017-09-10 17:05:13 +00:00
2017-11-23 05:37:08 +00:00
if len ( theme . Resources ) > 0 {
rlist := theme . Resources
2017-09-10 17:05:13 +00:00
for _ , resource := range rlist {
2018-03-21 05:56:33 +00:00
if resource . Loggedin && ! user . Loggedin {
continue
}
2017-09-10 17:05:13 +00:00
if resource . Location == "global" || resource . Location == "frontend" {
2017-10-12 03:24:14 +00:00
extarr := strings . Split ( resource . Name , "." )
ext := extarr [ len ( extarr ) - 1 ]
if ext == "css" {
2018-04-22 12:33:56 +00:00
header . AddSheet ( resource . Name )
2017-10-12 03:24:14 +00:00
} else if ext == "js" {
2018-04-22 12:33:56 +00:00
header . AddScript ( resource . Name )
2017-09-10 17:05:13 +00:00
}
}
}
}
pusher , ok := w . ( http . Pusher )
if ok {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + theme . Name + "/main.css" , nil )
2017-09-10 17:05:13 +00:00
pusher . Push ( "/static/global.js" , nil )
pusher . Push ( "/static/jquery-3.1.1.min.js" , nil )
2017-11-23 05:37:08 +00:00
// TODO: Test these
2018-04-22 12:33:56 +00:00
for _ , sheet := range header . Stylesheets {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + sheet , nil )
}
2018-04-22 12:33:56 +00:00
for _ , script := range header . Scripts {
2017-11-23 05:37:08 +00:00
pusher . Push ( "/static/" + script , nil )
}
2017-09-10 17:05:13 +00:00
// TODO: Push avatars?
}
2018-04-22 12:33:56 +00:00
return header , nil
2017-09-10 17:05:13 +00:00
}
func preRoute ( w http . ResponseWriter , r * http . Request ) ( User , bool ) {
2017-11-11 04:06:16 +00:00
user , halt := Auth . SessionCheck ( w , r )
2017-11-10 03:33:11 +00:00
if halt {
return * user , false
}
2017-11-11 05:22:33 +00:00
var usercpy * User = BlankUser ( )
2017-11-11 04:06:16 +00:00
* usercpy = * user
2017-11-10 03:33:11 +00:00
2017-11-10 00:16:15 +00:00
// TODO: WIP. Refactor this to eliminate the unnecessary query
2017-09-10 17:05:13 +00:00
host , _ , err := net . SplitHostPort ( r . RemoteAddr )
if err != nil {
PreError ( "Bad IP" , w , r )
2017-11-11 04:06:16 +00:00
return * usercpy , false
2017-09-10 17:05:13 +00:00
}
2018-05-16 10:46:14 +00:00
if user == & GuestUser {
usercpy . LastIP = host
return * usercpy , true
}
2017-11-10 03:33:11 +00:00
if host != usercpy . LastIP {
2017-11-11 04:06:16 +00:00
err = usercpy . UpdateIP ( host )
2017-09-10 17:05:13 +00:00
if err != nil {
2017-10-30 09:57:08 +00:00
InternalError ( err , w , r )
2017-11-11 04:06:16 +00:00
return * usercpy , false
2017-09-10 17:05:13 +00:00
}
2017-11-10 03:33:11 +00:00
usercpy . LastIP = host
2017-09-10 17:05:13 +00:00
}
2017-11-10 03:33:11 +00:00
h := w . Header ( )
h . Set ( "X-Frame-Options" , "deny" )
2018-01-08 08:53:51 +00:00
h . Set ( "X-XSS-Protection" , "1; mode=block" ) // TODO: Remove when we add a CSP? CSP's are horrendously glitchy things, tread with caution before removing
2017-11-10 03:33:11 +00:00
// TODO: Set the content policy header
2017-11-11 04:06:16 +00:00
return * usercpy , true
2017-09-10 17:05:13 +00:00
}
2017-10-30 09:57:08 +00:00
2017-12-19 03:53:13 +00:00
// SuperAdminOnly makes sure that only super admin can access certain critical panel routes
func SuperAdminOnly ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if ! user . IsSuperAdmin {
return NoPermissions ( w , r , user )
}
return nil
}
2017-11-11 23:34:27 +00:00
// AdminOnly makes sure that only admins can access certain panel routes
func AdminOnly ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if ! user . IsAdmin {
return NoPermissions ( w , r , user )
}
return nil
}
2017-10-30 09:57:08 +00:00
// SuperModeOnly makes sure that only super mods or higher can access the panel routes
func SuperModOnly ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if ! user . IsSuperMod {
return NoPermissions ( w , r , user )
}
return nil
}
2017-11-05 09:55:34 +00:00
// MemberOnly makes sure that only logged in users can access this route
func MemberOnly ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if ! user . Loggedin {
2017-11-08 07:28:33 +00:00
return LoginRequired ( w , r , user )
}
return nil
}
// NoBanned stops any banned users from accessing this route
func NoBanned ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if user . IsBanned {
return Banned ( w , r , user )
}
return nil
}
func ParseForm ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
err := r . ParseForm ( )
if err != nil {
return LocalError ( "Bad Form" , w , r , user )
}
return nil
}
func NoSessionMismatch ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
err := r . ParseForm ( )
if err != nil {
return LocalError ( "Bad Form" , w , r , user )
}
if r . FormValue ( "session" ) != user . Session {
return SecurityError ( w , r , user )
2017-11-05 09:55:34 +00:00
}
return nil
}
2017-11-23 05:37:08 +00:00
func ReqIsJson ( r * http . Request ) bool {
return r . Header . Get ( "Content-type" ) == "application/json"
}
2018-01-14 12:03:20 +00:00
func HandleUploadRoute ( w http . ResponseWriter , r * http . Request , user User , maxFileSize int ) RouteError {
// TODO: Reuse this code more
if r . ContentLength > int64 ( maxFileSize ) {
size , unit := ConvertByteUnit ( float64 ( maxFileSize ) )
2018-02-19 04:26:01 +00:00
return CustomError ( "Your upload is too big. Your files need to be smaller than " + strconv . Itoa ( int ( size ) ) + unit + "." , http . StatusExpectationFailed , "Error" , w , r , nil , user )
2018-01-14 12:03:20 +00:00
}
r . Body = http . MaxBytesReader ( w , r . Body , int64 ( maxFileSize ) )
err := r . ParseMultipartForm ( int64 ( Megabyte ) )
if err != nil {
return LocalError ( "Bad Form" , w , r , user )
}
return nil
}
func NoUploadSessionMismatch ( w http . ResponseWriter , r * http . Request , user User ) RouteError {
if r . FormValue ( "session" ) != user . Session {
return SecurityError ( w , r , user )
}
return nil
}