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"
2018-11-17 02:36:02 +00:00
"time"
2017-09-10 17:05:13 +00:00
)
// 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-11-12 09:23:36 +00:00
var ForumUserCheck func ( header * Header , w http . ResponseWriter , r * http . Request , user * User , fid int ) ( 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
2018-10-21 13:54:32 +00:00
func simpleForumUserCheck ( w http . ResponseWriter , r * http . Request , user * User , fid int ) ( header * HeaderLite , rerr RouteError ) {
header , rerr = SimpleUserCheck ( w , r , user )
if rerr != nil {
return header , rerr
}
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?
2018-10-21 13:54:32 +00:00
skip , rerr := header . Hooks . VhookSkippable ( "simple_forum_check_pre_perms" , w , r , user , & fid , & header )
if skip || rerr != nil {
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-10-21 13:54:32 +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-10-21 13:54:32 +00:00
return header , nil
2017-09-10 17:05:13 +00:00
}
2018-11-12 09:23:36 +00:00
func forumUserCheck ( header * Header , w http . ResponseWriter , r * http . Request , user * User , fid int ) ( rerr RouteError ) {
2017-11-23 05:37:08 +00:00
if ! Forums . Exists ( fid ) {
2018-11-12 09:23:36 +00:00
return NotFound ( w , r , header )
2017-09-10 17:05:13 +00:00
}
2018-10-21 13:54:32 +00:00
skip , rerr := header . Hooks . VhookSkippable ( "forum_check_pre_perms" , w , r , user , & fid , & header )
if skip || rerr != nil {
2018-11-12 09:23:36 +00:00
return 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-11-12 09:23:36 +00:00
return 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-11-12 09:23:36 +00:00
return 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 ,
2018-10-21 13:54:32 +00:00
Hooks : GetHookTable ( ) ,
2018-04-22 12:33:56 +00:00
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
2018-11-17 02:36:02 +00:00
// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
if user . IsAdmin {
header . StartedAt = time . Now ( )
}
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-08-11 15:53:42 +00:00
//h := w.Header()
//h.Set("Content-Security-Policy", "default-src 'self'")
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-08-04 11:46:36 +00:00
stats . Forums = Forums . GlobalCount ( )
2018-06-06 00:21:22 +00:00
stats . Pages = Pages . GlobalCount ( )
2018-04-22 12:33:56 +00:00
stats . Settings = len ( header . Settings )
2018-08-04 11:46:36 +00:00
stats . WordFilters = WordFilters . EstCount ( )
2017-11-11 04:06:16 +00:00
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-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-10-21 13:54:32 +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-10-21 13:54:32 +00:00
Hooks : GetHookTable ( ) ,
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-10-21 13:54:32 +00:00
Hooks : GetHookTable ( ) ,
2018-04-22 12:33:56 +00:00
Zone : "frontend" ,
Writer : w ,
2017-09-10 17:05:13 +00:00
}
if user . IsBanned {
2018-06-06 00:29:12 +00:00
header . AddNotice ( "account_banned" )
2017-09-10 17:05:13 +00:00
}
2018-03-08 03:59:47 +00:00
if user . Loggedin && ! user . Active {
2018-06-06 00:29:12 +00:00
header . AddNotice ( "account_inactive" )
2018-03-08 03:59:47 +00:00
}
2018-11-17 02:36:02 +00:00
// An optimisation so we don't populate StartedAt for users who shouldn't see the stat anyway
// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
if user . IsAdmin {
header . StartedAt = time . Now ( )
}
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
}
}
}
}
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 ) {
2018-10-14 06:09:25 +00:00
userptr , halt := Auth . SessionCheck ( w , r )
2017-11-10 03:33:11 +00:00
if halt {
2018-10-14 06:09:25 +00:00
return * userptr , false
2017-11-10 03:33:11 +00:00
}
2017-11-11 05:22:33 +00:00
var usercpy * User = BlankUser ( )
2018-10-14 06:09:25 +00:00
* usercpy = * userptr
2018-10-14 06:16:42 +00:00
usercpy . Init ( ) // TODO: Can we reduce the amount of work we do here?
2017-11-10 03:33:11 +00:00
2018-11-29 07:27:17 +00:00
// TODO: Add a config setting to disable this header
// TODO: Have this header cover more things
if Site . EnableSsl {
w . Header ( ) . Set ( "Content-Security-Policy" , "upgrade-insecure-requests" )
}
2017-11-10 00:16:15 +00:00
// TODO: WIP. Refactor this to eliminate the unnecessary query
2018-07-03 10:01:49 +00:00
// TODO: Better take proxies into consideration
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-07-03 10:01:49 +00:00
// TODO: Prefer Cf-Connecting-Ip header, fewer shenanigans
if Site . HasProxy {
// TODO: Check the right-most IP, might get tricky with multiple proxies, maybe have a setting for the number of hops we jump through
xForwardedFor := r . Header . Get ( "X-Forwarded-For" )
if xForwardedFor != "" {
forwardedFor := strings . Split ( xForwardedFor , "," )
// TODO: Check if this is a valid IP Address, reject if not
host = forwardedFor [ len ( forwardedFor ) - 1 ]
}
}
2018-11-29 07:27:17 +00:00
usercpy . LastIP = host
2018-08-30 05:57:07 +00:00
2018-11-29 07:27:17 +00:00
if usercpy . Loggedin && 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
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
}