From 14a14b7e8047441b29112ff5feb6d2115e2c05f8 Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 30 Oct 2017 09:57:08 +0000 Subject: [PATCH] More work on Cosora, we have a screenshot of it up now, although it's super experimental at the moment. This commit might be a little broken, another is coming to fix things up! The topics list is now paginated. Refactored the error handling system. Added the Trumboyg WYSIWYG editor for Cosora. Moved Delete() out of the TopicStore and into *Topic You can now bulk delete and bulk lock topics on Cosora. h1s are now formatted properly on Firefox. Added more ARIA Labels. SuperModOnly is now a piece of middleware for the Control Panel routes. Refactored and extended the router generator. Improved the SEO for the paginators. Added bits of Microdata to improve SEO further. Wrote benchmarks for users.Get() and users.BypassGet() More errors are caught now. You can now attach pcss files to posts. Improved the error logging for JavaScript. Topic list avatars now link to the associated profiles. Added last poster avatars to the forum list. --- README.md | 2 + auth.go | 2 +- errors.go | 153 +- gen_mssql.go | 8 + gen_mysql.go | 7 + gen_router.go | 217 ++- gen_tables.go | 12 +- general_test.go | 43 + images/cosora-wip.png | Bin 0 -> 63263 bytes main.go | 2 +- member_routes.go | 650 +++---- misc_test.go | 48 +- mod_routes.go | 650 +++---- pages.go | 2 +- panel_routes.go | 980 ++++------ plugin_skeleton.go | 2 +- plugin_socialgroups.go | 99 +- public/EQCSS.js | 651 +++++++ public/global.js | 196 +- public/jquery-1.12.3.min.js | 5 - public/trumbowyg/langs/ar.min.js | 11 + public/trumbowyg/langs/bg.min.js | 8 + public/trumbowyg/langs/by.min.js | 8 + public/trumbowyg/langs/ca.min.js | 11 + public/trumbowyg/langs/cs.min.js | 8 + public/trumbowyg/langs/da.min.js | 9 + public/trumbowyg/langs/de.min.js | 9 + public/trumbowyg/langs/el.min.js | 12 + public/trumbowyg/langs/es.min.js | 9 + public/trumbowyg/langs/es_ar.min.js | 10 + public/trumbowyg/langs/fa.min.js | 10 + public/trumbowyg/langs/fi.min.js | 9 + public/trumbowyg/langs/fr.min.js | 10 + public/trumbowyg/langs/he.min.js | 9 + public/trumbowyg/langs/hr.min.js | 9 + public/trumbowyg/langs/hu.min.js | 10 + public/trumbowyg/langs/id.min.js | 11 + public/trumbowyg/langs/it.min.js | 8 + public/trumbowyg/langs/ja.min.js | 10 + public/trumbowyg/langs/ko.min.js | 10 + public/trumbowyg/langs/mn.min.js | 8 + public/trumbowyg/langs/my.min.js | 8 + public/trumbowyg/langs/nl.min.js | 9 + public/trumbowyg/langs/no_nb.min.js | 10 + public/trumbowyg/langs/ph.min.js | 8 + public/trumbowyg/langs/pl.min.js | 9 + public/trumbowyg/langs/pt.min.js | 11 + public/trumbowyg/langs/pt_br.min.js | 11 + public/trumbowyg/langs/ro.min.js | 12 + public/trumbowyg/langs/rs.min.js | 8 + public/trumbowyg/langs/rs_latin.min.js | 9 + public/trumbowyg/langs/ru.min.js | 8 + public/trumbowyg/langs/sk.min.js | 8 + public/trumbowyg/langs/sv.min.js | 12 + public/trumbowyg/langs/tr.min.js | 9 + public/trumbowyg/langs/ua.min.js | 8 + public/trumbowyg/langs/vi.min.js | 9 + public/trumbowyg/langs/zh_cn.min.js | 11 + public/trumbowyg/langs/zh_tw.min.js | 11 + .../plugins/base64/trumbowyg.base64.js | 121 ++ .../plugins/base64/trumbowyg.base64.min.js | 1 + .../cleanpaste/trumbowyg.cleanpaste.js | 179 ++ .../cleanpaste/trumbowyg.cleanpaste.min.js | 1 + .../plugins/colors/trumbowyg.colors.js | 176 ++ .../plugins/colors/trumbowyg.colors.min.js | 1 + .../colors/ui/sass/trumbowyg.colors.scss | 49 + .../plugins/colors/ui/trumbowyg.colors.css | 43 + .../colors/ui/trumbowyg.colors.min.css | 2 + .../plugins/emoji/trumbowyg.emoji.js | 965 ++++++++++ .../plugins/emoji/trumbowyg.emoji.min.js | 1 + .../emoji/ui/sass/trumbowyg.emoji.scss | 56 + .../plugins/emoji/ui/trumbowyg.emoji.css | 47 + .../plugins/emoji/ui/trumbowyg.emoji.min.css | 2 + .../insertaudio/trumbowyg.insertaudio.js | 83 + .../insertaudio/trumbowyg.insertaudio.min.js | 1 + .../plugins/noembed/trumbowyg.noembed.js | 101 + .../plugins/noembed/trumbowyg.noembed.min.js | 1 + .../pasteimage/trumbowyg.pasteimage.js | 41 + .../pasteimage/trumbowyg.pasteimage.min.js | 1 + .../preformatted/trumbowyg.preformatted.js | 123 ++ .../trumbowyg.preformatted.min.js | 1 + .../plugins/table/trumbowyg.table.js | 154 ++ .../plugins/table/trumbowyg.table.min.js | 1 + .../plugins/template/trumbowyg.template.js | 58 + .../template/trumbowyg.template.min.js | 1 + .../plugins/upload/trumbowyg.upload.js | 242 +++ .../plugins/upload/trumbowyg.upload.min.js | 1 + public/trumbowyg/trumbowyg.js | 1688 +++++++++++++++++ public/trumbowyg/trumbowyg.min.js | 2 + public/trumbowyg/ui/icons.svg | 1 + public/trumbowyg/ui/sass/trumbowyg.scss | 796 ++++++++ public/trumbowyg/ui/trumbowyg.css | 584 ++++++ public/trumbowyg/ui/trumbowyg.custom.css | 550 ++++++ public/trumbowyg/ui/trumbowyg.min.css | 2 + query_gen/lib/builder.go | 13 + query_gen/lib/mssql.go | 50 +- query_gen/lib/mysql.go | 52 +- query_gen/lib/pgsql.go | 46 +- query_gen/main.go | 2 + reply.go | 2 +- router_gen/main.go | 94 +- router_gen/routes.go | 32 +- routes.go | 441 +++-- routes_common.go | 105 +- site.go | 2 +- template_forum.go | 103 +- template_forums.go | 42 +- template_init.go | 46 +- template_list.go | 708 +++---- template_profile.go | 10 +- template_topic.go | 242 +-- template_topic_alt.go | 214 ++- template_topics.go | 119 +- templates.go | 2 +- templates/forum.html | 37 +- templates/forums.html | 24 +- templates/menu.html | 2 +- templates/panel-adminlogs.html | 2 +- templates/panel-group-edit-perms.html | 2 +- templates/panel-group-edit.html | 2 +- templates/panel-menu.html | 2 +- templates/panel-modlogs.html | 2 +- templates/panel-themes.html | 2 +- templates/topic.html | 28 +- templates/topic_alt.html | 29 +- templates/topics.html | 46 +- themes.go | 76 +- themes/cosora/public/main.css | 354 +++- themes/cosora/public/misc.js | 16 + themes/cosora/theme.json | 20 +- themes/tempra-simple/public/main.css | 2 + topic.go | 28 +- topic_store.go | 70 - websockets.go | 24 +- 134 files changed, 10343 insertions(+), 2925 deletions(-) create mode 100644 images/cosora-wip.png create mode 100644 public/EQCSS.js delete mode 100644 public/jquery-1.12.3.min.js create mode 100644 public/trumbowyg/langs/ar.min.js create mode 100644 public/trumbowyg/langs/bg.min.js create mode 100644 public/trumbowyg/langs/by.min.js create mode 100644 public/trumbowyg/langs/ca.min.js create mode 100644 public/trumbowyg/langs/cs.min.js create mode 100644 public/trumbowyg/langs/da.min.js create mode 100644 public/trumbowyg/langs/de.min.js create mode 100644 public/trumbowyg/langs/el.min.js create mode 100644 public/trumbowyg/langs/es.min.js create mode 100644 public/trumbowyg/langs/es_ar.min.js create mode 100644 public/trumbowyg/langs/fa.min.js create mode 100644 public/trumbowyg/langs/fi.min.js create mode 100644 public/trumbowyg/langs/fr.min.js create mode 100644 public/trumbowyg/langs/he.min.js create mode 100644 public/trumbowyg/langs/hr.min.js create mode 100644 public/trumbowyg/langs/hu.min.js create mode 100644 public/trumbowyg/langs/id.min.js create mode 100644 public/trumbowyg/langs/it.min.js create mode 100644 public/trumbowyg/langs/ja.min.js create mode 100644 public/trumbowyg/langs/ko.min.js create mode 100644 public/trumbowyg/langs/mn.min.js create mode 100644 public/trumbowyg/langs/my.min.js create mode 100644 public/trumbowyg/langs/nl.min.js create mode 100644 public/trumbowyg/langs/no_nb.min.js create mode 100644 public/trumbowyg/langs/ph.min.js create mode 100644 public/trumbowyg/langs/pl.min.js create mode 100644 public/trumbowyg/langs/pt.min.js create mode 100644 public/trumbowyg/langs/pt_br.min.js create mode 100644 public/trumbowyg/langs/ro.min.js create mode 100644 public/trumbowyg/langs/rs.min.js create mode 100644 public/trumbowyg/langs/rs_latin.min.js create mode 100644 public/trumbowyg/langs/ru.min.js create mode 100644 public/trumbowyg/langs/sk.min.js create mode 100644 public/trumbowyg/langs/sv.min.js create mode 100644 public/trumbowyg/langs/tr.min.js create mode 100644 public/trumbowyg/langs/ua.min.js create mode 100644 public/trumbowyg/langs/vi.min.js create mode 100644 public/trumbowyg/langs/zh_cn.min.js create mode 100644 public/trumbowyg/langs/zh_tw.min.js create mode 100644 public/trumbowyg/plugins/base64/trumbowyg.base64.js create mode 100644 public/trumbowyg/plugins/base64/trumbowyg.base64.min.js create mode 100644 public/trumbowyg/plugins/cleanpaste/trumbowyg.cleanpaste.js create mode 100644 public/trumbowyg/plugins/cleanpaste/trumbowyg.cleanpaste.min.js create mode 100644 public/trumbowyg/plugins/colors/trumbowyg.colors.js create mode 100644 public/trumbowyg/plugins/colors/trumbowyg.colors.min.js create mode 100644 public/trumbowyg/plugins/colors/ui/sass/trumbowyg.colors.scss create mode 100644 public/trumbowyg/plugins/colors/ui/trumbowyg.colors.css create mode 100644 public/trumbowyg/plugins/colors/ui/trumbowyg.colors.min.css create mode 100644 public/trumbowyg/plugins/emoji/trumbowyg.emoji.js create mode 100644 public/trumbowyg/plugins/emoji/trumbowyg.emoji.min.js create mode 100644 public/trumbowyg/plugins/emoji/ui/sass/trumbowyg.emoji.scss create mode 100644 public/trumbowyg/plugins/emoji/ui/trumbowyg.emoji.css create mode 100644 public/trumbowyg/plugins/emoji/ui/trumbowyg.emoji.min.css create mode 100644 public/trumbowyg/plugins/insertaudio/trumbowyg.insertaudio.js create mode 100644 public/trumbowyg/plugins/insertaudio/trumbowyg.insertaudio.min.js create mode 100644 public/trumbowyg/plugins/noembed/trumbowyg.noembed.js create mode 100644 public/trumbowyg/plugins/noembed/trumbowyg.noembed.min.js create mode 100644 public/trumbowyg/plugins/pasteimage/trumbowyg.pasteimage.js create mode 100644 public/trumbowyg/plugins/pasteimage/trumbowyg.pasteimage.min.js create mode 100644 public/trumbowyg/plugins/preformatted/trumbowyg.preformatted.js create mode 100644 public/trumbowyg/plugins/preformatted/trumbowyg.preformatted.min.js create mode 100644 public/trumbowyg/plugins/table/trumbowyg.table.js create mode 100644 public/trumbowyg/plugins/table/trumbowyg.table.min.js create mode 100644 public/trumbowyg/plugins/template/trumbowyg.template.js create mode 100644 public/trumbowyg/plugins/template/trumbowyg.template.min.js create mode 100644 public/trumbowyg/plugins/upload/trumbowyg.upload.js create mode 100644 public/trumbowyg/plugins/upload/trumbowyg.upload.min.js create mode 100644 public/trumbowyg/trumbowyg.js create mode 100644 public/trumbowyg/trumbowyg.min.js create mode 100644 public/trumbowyg/ui/icons.svg create mode 100644 public/trumbowyg/ui/sass/trumbowyg.scss create mode 100644 public/trumbowyg/ui/trumbowyg.css create mode 100644 public/trumbowyg/ui/trumbowyg.custom.css create mode 100644 public/trumbowyg/ui/trumbowyg.min.css create mode 100644 themes/cosora/public/misc.js diff --git a/README.md b/README.md index b759320b..ebfcdfc6 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ We're looking for ways to clean-up the plugin system so that all of them (except ![Tempra Conflux Mobile](https://github.com/Azareal/Gosora/blob/master/images/tempra-conflux-mobile-320px.png) +![Cosora Prototype WIP](https://github.com/Azareal/Gosora/blob/master/images/cosora-wip.png) + More images in the /images/ folder. Beware though, some of them are *really* outdated. # Dependencies diff --git a/auth.go b/auth.go index 9a5ecfac..fced9676 100644 --- a/auth.go +++ b/auth.go @@ -152,7 +152,7 @@ func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (u if err == ErrNoRows { return &guestUser, false } else if err != nil { - InternalError(err, w) + InternalError(err, w, r) return &guestUser, true } diff --git a/errors.go b/errors.go index 05eca1d6..6a0778cb 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,52 @@ var errorBuffer []error //var notfoundCountPerSecond int //var nopermsCountPerSecond int +// WIP, a new system to propagate errors up from routes +type RouteError interface { + Type() string + Error() string + Json() bool + Handled() bool +} + +type RouteErrorImpl struct { + text string + system bool + json bool + handled bool +} + +/*func NewRouteError(msg string, system bool, json bool) RouteError { + return &RouteErrorImpl{msg, system, json, false} +}*/ + +func (err *RouteErrorImpl) Type() string { + // System errors may contain sensitive information we don't want the user to see + if err.system { + return "system" + } else { + return "user" + } +} + +func (err *RouteErrorImpl) Error() string { + return err.text +} + +// Respond with JSON? +func (err *RouteErrorImpl) Json() bool { + return err.json +} + +// Has this error been dealt with elsewhere? +func (err *RouteErrorImpl) Handled() bool { + return err.handled +} + +func HandledRouteError() RouteError { + return &RouteErrorImpl{"", false, false, true} +} + // LogError logs internal handler errors which can't be handled with InternalError() as a wrapper for log.Fatal(), we might do more with it in the future. func LogError(err error) { LogWarning(err) @@ -28,9 +74,10 @@ func LogWarning(err error) { errorBuffer = append(errorBuffer, err) } +// TODO: Dump the request? // InternalError is the main function for handling internal errors, while simultaneously printing out a page for the end-user to let them know that *something* has gone wrong // ? - Add a user parameter? -func InternalError(err error, w http.ResponseWriter) { +func InternalError(err error, w http.ResponseWriter, r *http.Request) RouteError { log.Print(err) debug.PrintStack() @@ -46,167 +93,180 @@ func InternalError(err error, w http.ResponseWriter) { defer errorBufferMutex.Unlock() errorBuffer = append(errorBuffer, err) log.Fatal("") + return HandledRouteError() } // InternalErrorJSQ is the JSON "maybe" version of InternalError which can handle both JSON and normal requests // ? - Add a user parameter? -func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bool) { +func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bool) RouteError { if !isJs { - InternalError(err, w) + return InternalError(err, w, r) } else { - InternalErrorJS(err, w, r) + return InternalErrorJS(err, w, r) } } // InternalErrorJS is the JSON version of InternalError on routes we know will only be requested via JSON. E.g. An API. // ? - Add a user parameter? -func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) { +func InternalErrorJS(err error, w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(500) - _, _ = w.Write([]byte(`{"errmsg":"A problem has occured in the system."}`)) + _, _ = w.Write([]byte(`{"errmsg":"A problem has occurred in the system."}`)) errorBufferMutex.Lock() defer errorBufferMutex.Unlock() errorBuffer = append(errorBuffer, err) log.Fatal(err) + return HandledRouteError() } -// ? - Where is this used? Is it actually used? Should we use it more? +// ? - Where is this used? Should we use it more? // LoginRequired is an error shown to the end-user when they try to access an area which requires them to login -func LoginRequired(w http.ResponseWriter, r *http.Request, user User) { +func LoginRequired(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(401) pi := Page{"Local Error", user, getDefaultHeaderVar(), tList, "You need to login to do that."} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } -func PreError(errmsg string, w http.ResponseWriter, r *http.Request) { +func PreError(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(500) user := User{ID: 0, Group: 6, Perms: GuestPerms} pi := Page{"Error", user, getDefaultHeaderVar(), tList, errmsg} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } -func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) { +func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(500) _, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) + return HandledRouteError() } -func PreErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, isJs bool) { +func PreErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, isJs bool) RouteError { if !isJs { - PreError(errmsg, w, r) + return PreError(errmsg, w, r) } else { - PreErrorJS(errmsg, w, r) + return PreErrorJS(errmsg, w, r) } } // LocalError is an error shown to the end-user when something goes wrong and it's not the software's fault -func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, user User) { +func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(500) pi := Page{"Local Error", user, getDefaultHeaderVar(), tList, errmsg} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } -func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, user User, isJs bool) { +func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { - LocalError(errmsg, w, r, user) + return LocalError(errmsg, w, r, user) } else { - LocalErrorJS(errmsg, w, r) + return LocalErrorJS(errmsg, w, r) } } -func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) { +func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(500) _, _ = w.Write([]byte(`{'errmsg': '` + errmsg + `'}`)) + return HandledRouteError() } +// TODO: We might want to centralise the error logic in the future and just return what the error handler needs to construct the response rather than handling it here // NoPermissions is an error shown to the end-user when they try to access an area which they aren't authorised to access -func NoPermissions(w http.ResponseWriter, r *http.Request, user User) { +func NoPermissions(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(403) pi := Page{"Local Error", user, getDefaultHeaderVar(), tList, "You don't have permission to do that."} + // TODO: What to do about this hook? if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } -func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) { +func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { - NoPermissions(w, r, user) + return NoPermissions(w, r, user) } else { - NoPermissionsJS(w, r, user) + return NoPermissionsJS(w, r, user) } } -func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) { +func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(403) _, _ = w.Write([]byte(`{"errmsg":"You don't have permission to do that."}`)) + return HandledRouteError() } // ? - Is this actually used? Should it be used? A ban in Gosora should be more of a permission revocation to stop them posting rather than something which spits up an error page, right? -func Banned(w http.ResponseWriter, r *http.Request, user User) { +func Banned(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(403) pi := Page{"Banned", user, getDefaultHeaderVar(), tList, "You have been banned from this site."} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } // nolint // BannedJSQ is the version of the banned error page which handles both JavaScript requests and normal page loads -func BannedJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) { +func BannedJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { - Banned(w, r, user) + return Banned(w, r, user) } else { - BannedJS(w, r, user) + return BannedJS(w, r, user) } } -func BannedJS(w http.ResponseWriter, r *http.Request, user User) { +func BannedJS(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(403) _, _ = w.Write([]byte(`{"errmsg":"You have been banned from this site."}`)) + return HandledRouteError() } // nolint -func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) { +func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { w.WriteHeader(401) if !isJs { pi := Page{"Local Error", user, getDefaultHeaderVar(), tList, "You need to login to do that."} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) @@ -216,28 +276,30 @@ func LoginRequiredJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bo } else { _, _ = w.Write([]byte(`{"errmsg":"You need to login to do that."}`)) } + return HandledRouteError() } // SecurityError is used whenever a session mismatch is found // ? - Should we add JS and JSQ versions of this? -func SecurityError(w http.ResponseWriter, r *http.Request, user User) { +func SecurityError(w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(403) pi := Page{"Security Error", user, getDefaultHeaderVar(), tList, "There was a security issue with your request."} if preRenderHooks["pre_render_security_error"] != nil { if runPreRenderHook("pre_render_security_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } // NotFound is used when the requested page doesn't exist // ? - Add a JSQ and JS version of this? // ? - Add a user parameter? -func NotFound(w http.ResponseWriter, r *http.Request) { +func NotFound(w http.ResponseWriter, r *http.Request) RouteError { w.WriteHeader(404) // TODO: Centralise the user struct somewhere else user := User{0, "guest", "Guest", "", 0, false, false, false, false, false, false, GuestPerms, nil, "", false, "", "", "", "", "", 0, 0, "0.0.0.0.0", 0} @@ -246,34 +308,37 @@ func NotFound(w http.ResponseWriter, r *http.Request) { if err != nil { LogError(err) } + return HandledRouteError() } // CustomError lets us make custom error types which aren't covered by the generic functions above -func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { +func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(errcode) pi := Page{errtitle, user, getDefaultHeaderVar(), tList, errmsg} if preRenderHooks["pre_render_error"] != nil { if runPreRenderHook("pre_render_error", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "error.html", pi) if err != nil { LogError(err) } + return HandledRouteError() } // CustomErrorJSQ is a version of CustomError which lets us handle both JSON and regular pages depending on how it's being accessed -func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) { +func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError { if !isJs { - CustomError(errmsg, errcode, errtitle, w, r, user) + return CustomError(errmsg, errcode, errtitle, w, r, user) } else { - CustomErrorJS(errmsg, errcode, errtitle, w, r, user) + return CustomErrorJS(errmsg, errcode, errtitle, w, r, user) } } // CustomErrorJS is the pure JSON version of CustomError -func CustomErrorJS(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) { +func CustomErrorJS(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User) RouteError { w.WriteHeader(errcode) _, _ = w.Write([]byte(`{"errmsg":"` + errmsg + `"}`)) + return HandledRouteError() } diff --git a/gen_mssql.go b/gen_mssql.go index eed4a3d5..16364b22 100644 --- a/gen_mssql.go +++ b/gen_mssql.go @@ -112,6 +112,7 @@ var setTempGroupStmt *sql.Stmt var updateWordFilterStmt *sql.Stmt var bumpSyncStmt *sql.Stmt var deleteUserStmt *sql.Stmt +var deleteTopicStmt *sql.Stmt var deleteReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt var deleteActivityStreamMatchStmt *sql.Stmt @@ -862,6 +863,13 @@ func _gen_mssql() (err error) { return err } + log.Print("Preparing deleteTopic statement.") + deleteTopicStmt, err = db.Prepare("DELETE FROM [topics] WHERE [tid] = ?") + if err != nil { + log.Print("Bad Query: ","DELETE FROM [topics] WHERE [tid] = ?") + return err + } + log.Print("Preparing deleteReply statement.") deleteReplyStmt, err = db.Prepare("DELETE FROM [replies] WHERE [rid] = ?") if err != nil { diff --git a/gen_mysql.go b/gen_mysql.go index 811e6c74..b14102a0 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -114,6 +114,7 @@ var setTempGroupStmt *sql.Stmt var updateWordFilterStmt *sql.Stmt var bumpSyncStmt *sql.Stmt var deleteUserStmt *sql.Stmt +var deleteTopicStmt *sql.Stmt var deleteReplyStmt *sql.Stmt var deleteProfileReplyStmt *sql.Stmt var deleteActivityStreamMatchStmt *sql.Stmt @@ -759,6 +760,12 @@ func _gen_mysql() (err error) { return err } + log.Print("Preparing deleteTopic statement.") + deleteTopicStmt, err = db.Prepare("DELETE FROM `topics` WHERE `tid` = ?") + if err != nil { + return err + } + log.Print("Preparing deleteReply statement.") deleteReplyStmt, err = db.Prepare("DELETE FROM `replies` WHERE `rid` = ?") if err != nil { diff --git a/gen_router.go b/gen_router.go index 71fe7cf0..ac394f7f 100644 --- a/gen_router.go +++ b/gen_router.go @@ -12,7 +12,7 @@ var ErrNoRoute = errors.New("That route doesn't exist.") type GenRouter struct { UploadHandler func(http.ResponseWriter, *http.Request) - extra_routes map[string]func(http.ResponseWriter, *http.Request, User) + extra_routes map[string]func(http.ResponseWriter, *http.Request, User) RouteError sync.RWMutex } @@ -20,14 +20,26 @@ type GenRouter struct { func NewGenRouter(uploads http.Handler) *GenRouter { return &GenRouter{ UploadHandler: http.StripPrefix("/uploads/",uploads).ServeHTTP, - extra_routes: make(map[string]func(http.ResponseWriter, *http.Request, User)), + extra_routes: make(map[string]func(http.ResponseWriter, *http.Request, User) RouteError), } } +func (router *GenRouter) handleError(err RouteError, w http.ResponseWriter, r *http.Request, user User) { + if err.Handled() { + return + } + + if err.Type() == "system" { + InternalErrorJSQ(err,w,r,err.Json()) + return + } + LocalErrorJSQ(err.Error(),w,r,user,err.Json()) +} + func (router *GenRouter) Handle(_ string, _ http.Handler) { } -func (router *GenRouter) HandleFunc(pattern string, handle func(http.ResponseWriter, *http.Request, User)) { +func (router *GenRouter) HandleFunc(pattern string, handle func(http.ResponseWriter, *http.Request, User) RouteError) { router.Lock() router.extra_routes[pattern] = handle router.Unlock() @@ -91,144 +103,135 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Print("after PreRoute") } + var err RouteError switch(prefix) { case "/api": - routeAPI(w,req,user) - return + err = routeAPI(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + } case "/overview": - routeOverview(w,req,user) - return + err = routeOverview(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + } case "/forums": - routeForums(w,req,user) - return + err = routeForums(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + } case "/forum": - routeForum(w,req,user,extra_data) - return + err = routeForum(w,req,user,extra_data) + if err != nil { + router.handleError(err,w,req,user) + } case "/theme": - routeChangeTheme(w,req,user) - return + err = routeChangeTheme(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + } case "/attachs": - routeShowAttachment(w,req,user,extra_data) - return + err = routeShowAttachment(w,req,user,extra_data) + if err != nil { + router.handleError(err,w,req,user) + } case "/report": switch(req.URL.Path) { case "/report/submit/": - routeReportSubmit(w,req,user,extra_data) - return + err = routeReportSubmit(w,req,user,extra_data) + } + if err != nil { + router.handleError(err,w,req,user) } case "/topics": switch(req.URL.Path) { case "/topics/create/": - routeTopicCreate(w,req,user,extra_data) - return + err = routeTopicCreate(w,req,user,extra_data) default: - routeTopics(w,req,user) - return + err = routeTopics(w,req,user) + } + if err != nil { + router.handleError(err,w,req,user) } case "/panel": + err = SuperModOnly(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + switch(req.URL.Path) { case "/panel/forums/": - routePanelForums(w,req,user) - return + err = routePanelForums(w,req,user) case "/panel/forums/create/": - routePanelForumsCreateSubmit(w,req,user) - return + err = routePanelForumsCreateSubmit(w,req,user) case "/panel/forums/delete/": - routePanelForumsDelete(w,req,user,extra_data) - return + err = routePanelForumsDelete(w,req,user,extra_data) case "/panel/forums/delete/submit/": - routePanelForumsDeleteSubmit(w,req,user,extra_data) - return + err = routePanelForumsDeleteSubmit(w,req,user,extra_data) case "/panel/forums/edit/": - routePanelForumsEdit(w,req,user,extra_data) - return + err = routePanelForumsEdit(w,req,user,extra_data) case "/panel/forums/edit/submit/": - routePanelForumsEditSubmit(w,req,user,extra_data) - return + err = routePanelForumsEditSubmit(w,req,user,extra_data) case "/panel/forums/edit/perms/submit/": - routePanelForumsEditPermsSubmit(w,req,user,extra_data) - return + err = routePanelForumsEditPermsSubmit(w,req,user,extra_data) case "/panel/settings/": - routePanelSettings(w,req,user) - return + err = routePanelSettings(w,req,user) case "/panel/settings/edit/": - routePanelSetting(w,req,user,extra_data) - return + err = routePanelSetting(w,req,user,extra_data) case "/panel/settings/edit/submit/": - routePanelSettingEdit(w,req,user,extra_data) - return + err = routePanelSettingEdit(w,req,user,extra_data) case "/panel/settings/word-filters/": - routePanelWordFilters(w,req,user) - return + err = routePanelWordFilters(w,req,user) case "/panel/settings/word-filters/create/": - routePanelWordFiltersCreate(w,req,user) - return + err = routePanelWordFiltersCreate(w,req,user) case "/panel/settings/word-filters/edit/": - routePanelWordFiltersEdit(w,req,user,extra_data) - return + err = routePanelWordFiltersEdit(w,req,user,extra_data) case "/panel/settings/word-filters/edit/submit/": - routePanelWordFiltersEditSubmit(w,req,user,extra_data) - return + err = routePanelWordFiltersEditSubmit(w,req,user,extra_data) case "/panel/settings/word-filters/delete/submit/": - routePanelWordFiltersDeleteSubmit(w,req,user,extra_data) - return + err = routePanelWordFiltersDeleteSubmit(w,req,user,extra_data) case "/panel/themes/": - routePanelThemes(w,req,user) - return + err = routePanelThemes(w,req,user) case "/panel/themes/default/": - routePanelThemesSetDefault(w,req,user,extra_data) - return + err = routePanelThemesSetDefault(w,req,user,extra_data) case "/panel/plugins/": - routePanelPlugins(w,req,user) - return + err = routePanelPlugins(w,req,user) case "/panel/plugins/activate/": - routePanelPluginsActivate(w,req,user,extra_data) - return + err = routePanelPluginsActivate(w,req,user,extra_data) case "/panel/plugins/deactivate/": - routePanelPluginsDeactivate(w,req,user,extra_data) - return + err = routePanelPluginsDeactivate(w,req,user,extra_data) case "/panel/plugins/install/": - routePanelPluginsInstall(w,req,user,extra_data) - return + err = routePanelPluginsInstall(w,req,user,extra_data) case "/panel/users/": - routePanelUsers(w,req,user) - return + err = routePanelUsers(w,req,user) case "/panel/users/edit/": - routePanelUsersEdit(w,req,user,extra_data) - return + err = routePanelUsersEdit(w,req,user,extra_data) case "/panel/users/edit/submit/": - routePanelUsersEditSubmit(w,req,user,extra_data) - return + err = routePanelUsersEditSubmit(w,req,user,extra_data) case "/panel/groups/": - routePanelGroups(w,req,user) - return + err = routePanelGroups(w,req,user) case "/panel/groups/edit/": - routePanelGroupsEdit(w,req,user,extra_data) - return + err = routePanelGroupsEdit(w,req,user,extra_data) case "/panel/groups/edit/perms/": - routePanelGroupsEditPerms(w,req,user,extra_data) - return + err = routePanelGroupsEditPerms(w,req,user,extra_data) case "/panel/groups/edit/submit/": - routePanelGroupsEditSubmit(w,req,user,extra_data) - return + err = routePanelGroupsEditSubmit(w,req,user,extra_data) case "/panel/groups/edit/perms/submit/": - routePanelGroupsEditPermsSubmit(w,req,user,extra_data) - return + err = routePanelGroupsEditPermsSubmit(w,req,user,extra_data) case "/panel/groups/create/": - routePanelGroupsCreateSubmit(w,req,user) - return + err = routePanelGroupsCreateSubmit(w,req,user) case "/panel/backups/": - routePanelBackups(w,req,user,extra_data) - return + err = routePanelBackups(w,req,user,extra_data) case "/panel/logs/mod/": - routePanelLogsMod(w,req,user) - return + err = routePanelLogsMod(w,req,user) case "/panel/debug/": - routePanelDebug(w,req,user) - return + err = routePanelDebug(w,req,user) default: - routePanel(w,req,user) - return + err = routePanel(w,req,user) + } + if err != nil { + router.handleError(err,w,req,user) } case "/uploads": if extra_data == "" { @@ -236,14 +239,17 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } req.URL.Path += extra_data + // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) - return case "": // Stop the favicons, robots.txt file, etc. resolving to the topics list // TODO: Add support for favicons and robots.txt files switch(extra_data) { case "robots.txt": - routeRobotsTxt(w,req) + err = routeRobotsTxt(w,req) + if err != nil { + router.handleError(err,w,req,user) + } return } @@ -252,19 +258,20 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } config.DefaultRoute(w,req,user) - return - //default: NotFound(w,req) + default: + // A fallback for the routes which haven't been converted to the new router yet or plugins + router.RLock() + handle, ok := router.extra_routes[req.URL.Path] + router.RUnlock() + + if ok { + req.URL.Path += extra_data + err = handle(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + } + return + } + NotFound(w,req) } - - // A fallback for the routes which haven't been converted to the new router yet or plugins - router.RLock() - handle, ok := router.extra_routes[req.URL.Path] - router.RUnlock() - - if ok { - req.URL.Path += extra_data - handle(w,req,user) - return - } - NotFound(w,req) } diff --git a/gen_tables.go b/gen_tables.go index 9af297c0..795a1739 100644 --- a/gen_tables.go +++ b/gen_tables.go @@ -2,14 +2,14 @@ package main var dbTablePrimaryKeys = map[string]string{ - "users_groups":"gid", - "users_groups_scheduler":"uid", - "topics":"tid", + "replies":"rid", + "attachments":"attachID", "users_replies":"rid", "activity_stream":"asid", "word_filters":"wfid", - "users":"uid", "forums":"fid", - "replies":"rid", - "attachments":"attachID", + "users_groups":"gid", + "users_groups_scheduler":"uid", + "topics":"tid", + "users":"uid", } diff --git a/general_test.go b/general_test.go index 9a3df87c..bdc96284 100644 --- a/general_test.go +++ b/general_test.go @@ -550,6 +550,49 @@ func BenchmarkQueryPreparedTopicParallel(b *testing.B) { }) } +func BenchmarkUserGet(b *testing.B) { + b.ReportAllocs() + if !gloinited { + err := gloinit() + if err != nil { + b.Fatal(err) + } + } + + b.RunParallel(func(pb *testing.PB) { + var err error + for pb.Next() { + _, err = users.Get(1) + if err != nil { + b.Fatal(err) + return + } + } + }) +} + +func BenchmarkUserBypassGet(b *testing.B) { + b.ReportAllocs() + if !gloinited { + err := gloinit() + if err != nil { + b.Fatal(err) + } + } + + // Bypass the cache and always hit the database + b.RunParallel(func(pb *testing.PB) { + var err error + for pb.Next() { + _, err = users.BypassGet(1) + if err != nil { + b.Fatal(err) + return + } + } + }) +} + func BenchmarkQueriesSerial(b *testing.B) { b.ReportAllocs() var tu TopicUser diff --git a/images/cosora-wip.png b/images/cosora-wip.png new file mode 100644 index 0000000000000000000000000000000000000000..0a47f3382b0a6768a5d9487f41ac3a5a8bde1019 GIT binary patch literal 63263 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 literal 0 HcmV?d00001 diff --git a/main.go b/main.go index d73124f4..b7af5be8 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,7 @@ type StringList []string var allowedFileExts = StringList{ "png", "jpg", "jpeg", "svg", "bmp", "gif", "tif", "webp", "apng", // images - "txt", "xml", "json", "yaml", "toml", "ini", "md", "html", "rtf", "js", "py", "rb", "css", "scss", "less", "eqcss", "java", "ts", "cs", "c", "cc", "cpp", "cxx", "C", "c++", "h", "hh", "hpp", "hxx", "h++", "rs", "rlib", "htaccess", "gitignore", // text + "txt", "xml", "json", "yaml", "toml", "ini", "md", "html", "rtf", "js", "py", "rb", "css", "scss", "less", "eqcss", "pcss", "java", "ts", "cs", "c", "cc", "cpp", "cxx", "C", "c++", "h", "hh", "hpp", "hxx", "h++", "rs", "rlib", "htaccess", "gitignore", // text "mp3", "mp4", "avi", "wmv", "webm", // video diff --git a/member_routes.go b/member_routes.go index 6711b3c3..2469e8a3 100644 --- a/member_routes.go +++ b/member_routes.go @@ -20,27 +20,25 @@ import ( // TODO: Add a permission to stop certain users from using custom avatars // ? - Log username changes and put restrictions on this? -func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid string) { +func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { var fid int var err error if sfid != "" { fid, err = strconv.Atoi(sfid) if err != nil { - PreError("The provided ForumID is not a valid number.", w, r) - return + return LocalError("You didn't provide a valid number for the forum ID.", w, r, user) } } if fid == 0 { fid = config.DefaultForum } - headerVars, ok := ForumUserCheck(w, r, &user, fid) - if !ok { - return + headerVars, ferr := ForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.CreateTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } BuildWidgets("create_topic", nil, headerVars, r) @@ -58,15 +56,15 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st if user.IsSuperAdmin { canSee, err = fstore.GetAllVisibleIDs() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } else { group, err := gstore.Get(user.Group) if err != nil { + // TODO: Refactor this LocalError("Something weird happened behind the scenes", w, r, user) - log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist, but it's set on User #" + strconv.Itoa(user.ID)) - return + log.Printf("Group #%d doesn't exist, but it's set on User #%d", user.Group, user.ID) + return nil } canSee = group.CanSee } @@ -95,78 +93,74 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st ctpage := CreateTopicPage{"Create Topic", user, headerVars, forumList, fid} if preRenderHooks["pre_render_create_topic"] != nil { if runPreRenderHook("pre_render_create_topic", w, r, &user, &ctpage) { - return + return nil } } - template_create_topic_handle(ctpage, w) + err = template_create_topic_handle(ctpage, w) + if err != nil { + return InternalError(err, w, r) + } + return nil } // POST functions. Authorised users only. -func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { // TODO: Reduce this to 1MB for attachments for each file? if r.ContentLength > int64(config.MaxRequestSize) { size, unit := convertByteUnit(float64(config.MaxRequestSize)) - CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) - return + return CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) } r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) err := r.ParseMultipartForm(int64(megabyte)) if err != nil { - LocalError("Unable to parse the form", w, r, user) - return + return LocalError("Unable to parse the form", w, r, user) } fid, err := strconv.Atoi(r.PostFormValue("topic-board")) if err != nil { - PreError("The provided ForumID is not a valid number.", w, r) - return + return LocalError("The provided ForumID is not a valid number.", w, r, user) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, fid) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.CreateTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } topicName := html.EscapeString(r.PostFormValue("topic-name")) content := html.EscapeString(preparseMessage(r.PostFormValue("topic-content"))) ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } tid, err := topics.Create(fid, topicName, content, user.ID, ipaddress) if err != nil { switch err { case ErrNoRows: - LocalError("Something went wrong, perhaps the forum got deleted?", w, r, user) + return LocalError("Something went wrong, perhaps the forum got deleted?", w, r, user) case ErrNoTitle: - LocalError("This topic doesn't have a title", w, r, user) + return LocalError("This topic doesn't have a title", w, r, user) case ErrNoBody: - LocalError("This topic doesn't have a body", w, r, user) + return LocalError("This topic doesn't have a body", w, r, user) default: - InternalError(err, w) + return InternalError(err, w, r) } - return } _, err = addSubscriptionStmt.Exec(user.ID, tid, "topic") if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = user.increasePostStats(wordCount(content), true) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Handle the file attachments @@ -175,43 +169,39 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { files, ok := r.MultipartForm.File["upload_files"] if ok { if len(files) > 5 { - LocalError("You can't attach more than five files", w, r, user) - return + return LocalError("You can't attach more than five files", w, r, user) } for _, file := range files { - log.Print("file.Filename ", file.Filename) + if dev.DebugMode { + log.Print("file.Filename ", file.Filename) + } extarr := strings.Split(file.Filename, ".") if len(extarr) < 2 { - LocalError("Bad file", w, r, user) - return + return LocalError("Bad file", w, r, user) } ext := extarr[len(extarr)-1] // TODO: Can we do this without a regex? reg, err := regexp.Compile("[^A-Za-z0-9]+") if err != nil { - LocalError("Bad file extension", w, r, user) - return + return LocalError("Bad file extension", w, r, user) } ext = strings.ToLower(reg.ReplaceAllString(ext, "")) if !allowedFileExts.Contains(ext) { - LocalError("You're not allowed to upload files with this extension", w, r, user) - return + return LocalError("You're not allowed to upload files with this extension", w, r, user) } infile, err := file.Open() if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } defer infile.Close() hasher := sha256.New() _, err = io.Copy(hasher, infile) if err != nil { - LocalError("Upload failed [Hashing Failed]", w, r, user) - return + return LocalError("Upload failed [Hashing Failed]", w, r, user) } infile.Close() @@ -219,74 +209,65 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { filename := checksum + "." + ext outfile, err := os.Create("." + "/attachs/" + filename) if err != nil { - LocalError("Upload failed [File Creation Failed]", w, r, user) - return + return LocalError("Upload failed [File Creation Failed]", w, r, user) } defer outfile.Close() infile, err = file.Open() if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } defer infile.Close() _, err = io.Copy(outfile, infile) if err != nil { - LocalError("Upload failed [Copy Failed]", w, r, user) - return + return LocalError("Upload failed [Copy Failed]", w, r, user) } _, err = addAttachmentStmt.Exec(fid, "forums", tid, "topics", user.ID, filename) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } } } http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } -func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { +func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) RouteError { // TODO: Reduce this to 1MB for attachments for each file? if r.ContentLength > int64(config.MaxRequestSize) { size, unit := convertByteUnit(float64(config.MaxRequestSize)) - CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) - return + return CustomError("Your attachments are too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, user) } r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) err := r.ParseMultipartForm(int64(megabyte)) if err != nil { - LocalError("Unable to parse the form", w, r, user) - return + return LocalError("Unable to parse the form", w, r, user) } tid, err := strconv.Atoi(r.PostFormValue("tid")) if err != nil { - PreError("Failed to convert the Topic ID", w, r) - return + return PreError("Failed to convert the Topic ID", w, r) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreError("Couldn't find the parent topic", w, r) - return + return PreError("Couldn't find the parent topic", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.CreateReply { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } // Handle the file attachments @@ -295,43 +276,37 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { files, ok := r.MultipartForm.File["upload_files"] if ok { if len(files) > 5 { - LocalError("You can't attach more than five files", w, r, user) - return + return LocalError("You can't attach more than five files", w, r, user) } for _, file := range files { log.Print("file.Filename ", file.Filename) extarr := strings.Split(file.Filename, ".") if len(extarr) < 2 { - LocalError("Bad file", w, r, user) - return + return LocalError("Bad file", w, r, user) } ext := extarr[len(extarr)-1] // TODO: Can we do this without a regex? reg, err := regexp.Compile("[^A-Za-z0-9]+") if err != nil { - LocalError("Bad file extension", w, r, user) - return + return LocalError("Bad file extension", w, r, user) } ext = strings.ToLower(reg.ReplaceAllString(ext, "")) if !allowedFileExts.Contains(ext) { - LocalError("You're not allowed to upload files with this extension", w, r, user) - return + return LocalError("You're not allowed to upload files with this extension", w, r, user) } infile, err := file.Open() if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } defer infile.Close() hasher := sha256.New() _, err = io.Copy(hasher, infile) if err != nil { - LocalError("Upload failed [Hashing Failed]", w, r, user) - return + return LocalError("Upload failed [Hashing Failed]", w, r, user) } infile.Close() @@ -339,28 +314,24 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { filename := checksum + "." + ext outfile, err := os.Create("." + "/attachs/" + filename) if err != nil { - LocalError("Upload failed [File Creation Failed]", w, r, user) - return + return LocalError("Upload failed [File Creation Failed]", w, r, user) } defer outfile.Close() infile, err = file.Open() if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } defer infile.Close() _, err = io.Copy(outfile, infile) if err != nil { - LocalError("Upload failed [Copy Failed]", w, r, user) - return + return LocalError("Upload failed [Copy Failed]", w, r, user) } _, err = addAttachmentStmt.Exec(topic.ParentID, "forums", tid, "replies", user.ID, filename) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } } @@ -369,37 +340,31 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { content := preparseMessage(html.EscapeString(r.PostFormValue("reply-content"))) ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } _, err = rstore.Create(tid, content, ipaddress, topic.ParentID, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = fstore.UpdateLastTopic(tid, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } res, err := addActivityStmt.Exec(user.ID, topic.CreatedBy, "reply", "topic", tid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } lastID, err := res.LastInsertId() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = notifyWatchersStmt.Exec(lastID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Alert the subscribers about this post without blocking this post from being posted @@ -412,95 +377,80 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { wcount := wordCount(content) err = user.increasePostStats(wcount, false) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } + return nil } // TODO: Refactor this -func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { +func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } tid, err := strconv.Atoi(r.URL.Path[len("/topic/like/submit/"):]) if err != nil { - PreError("Topic IDs can only ever be numbers.", w, r) - return + return PreError("Topic IDs can only ever be numbers.", w, r) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreError("The requested topic doesn't exist.", w, r) - return + return PreError("The requested topic doesn't exist.", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.LikeItem { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if topic.CreatedBy == user.ID { - LocalError("You can't like your own topics", w, r, user) - return + return LocalError("You can't like your own topics", w, r, user) } err = hasLikedTopicStmt.QueryRow(user.ID, tid).Scan(&tid) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } else if err != ErrNoRows { - LocalError("You already liked this!", w, r, user) - return + return LocalError("You already liked this!", w, r, user) } _, err = users.Get(topic.CreatedBy) if err != nil && err == ErrNoRows { - LocalError("The target user doesn't exist", w, r, user) - return + return LocalError("The target user doesn't exist", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } score := 1 _, err = createLikeStmt.Exec(score, tid, "topics", user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = addLikesToTopicStmt.Exec(1, tid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } res, err := addActivityStmt.Exec(user.ID, topic.CreatedBy, "like", "topic", tid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } lastID, err := res.LastInsertId() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = notifyOneStmt.Exec(topic.CreatedBy, lastID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Live alerts, if the poster is online and WebSockets is enabled @@ -512,163 +462,139 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { tcache.CacheRemove(tid) } http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } -func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } rid, err := strconv.Atoi(r.URL.Path[len("/reply/like/submit/"):]) if err != nil { - PreError("The provided Reply ID is not a valid number.", w, r) - return + return PreError("The provided Reply ID is not a valid number.", w, r) } reply, err := rstore.Get(rid) if err == ErrNoRows { - PreError("You can't like something which doesn't exist!", w, r) - return + return PreError("You can't like something which doesn't exist!", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } var fid int err = getTopicFIDStmt.QueryRow(reply.ParentID).Scan(&fid) if err == ErrNoRows { - PreError("The parent topic doesn't exist.", w, r) - return + return PreError("The parent topic doesn't exist.", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, fid) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.LikeItem { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if reply.CreatedBy == user.ID { - LocalError("You can't like your own replies", w, r, user) - return + return LocalError("You can't like your own replies", w, r, user) } _, err = users.Get(reply.CreatedBy) if err != nil && err != ErrNoRows { - LocalError("The target user doesn't exist", w, r, user) - return + return LocalError("The target user doesn't exist", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = reply.Like(user.ID) if err == ErrAlreadyLiked { - LocalError("You've already liked this!", w, r, user) - return + return LocalError("You've already liked this!", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } res, err := addActivityStmt.Exec(user.ID, reply.CreatedBy, "like", "post", rid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } lastID, err := res.LastInsertId() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = notifyOneStmt.Exec(reply.CreatedBy, lastID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Live alerts, if the poster is online and WebSockets is enabled _ = wsHub.pushAlert(reply.CreatedBy, int(lastID), "like", "post", user.ID, reply.CreatedBy, rid) http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther) + return nil } -func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User) { +func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User) RouteError { if !user.Perms.ViewTopic || !user.Perms.CreateReply { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } uid, err := strconv.Atoi(r.PostFormValue("uid")) if err != nil { - LocalError("Invalid UID", w, r, user) - return + return LocalError("Invalid UID", w, r, user) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } content := html.EscapeString(preparseMessage(r.PostFormValue("reply-content"))) _, err = createProfileReplyStmt.Exec(uid, content, parseMessage(content, 0, ""), user.ID, ipaddress) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } var userName string err = getUserNameStmt.QueryRow(uid).Scan(&userName) if err == ErrNoRows { - LocalError("The profile you're trying to post on doesn't exist.", w, r, user) - return + return LocalError("The profile you're trying to post on doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther) + return nil } -func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemID string) { +func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemID string) RouteError { if !user.Loggedin { - LoginRequired(w, r, user) - return + return LoginRequired(w, r, user) } if user.IsBanned { - Banned(w, r, user) - return + return Banned(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } itemID, err := strconv.Atoi(sitemID) if err != nil { - LocalError("Bad ID", w, r, user) - return + return LocalError("Bad ID", w, r, user) } itemType := r.FormValue("type") @@ -678,20 +604,16 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI if itemType == "reply" { reply, err := rstore.Get(itemID) if err == ErrNoRows { - LocalError("We were unable to find the reported post", w, r, user) - return + return LocalError("We were unable to find the reported post", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } topic, err := topics.Get(reply.ParentID) if err == ErrNoRows { - LocalError("We weren't able to find the topic the reported post is supposed to be in", w, r, user) - return + return LocalError("We weren't able to find the topic the reported post is supposed to be in", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } title = "Reply: " + topic.Title @@ -699,124 +621,112 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI } else if itemType == "user-reply" { userReply, err := prstore.Get(itemID) if err == ErrNoRows { - LocalError("We weren't able to find the reported post", w, r, user) - return + return LocalError("We weren't able to find the reported post", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = getUserNameStmt.QueryRow(userReply.ParentID).Scan(&title) if err == ErrNoRows { - LocalError("We weren't able to find the profile the reported post is supposed to be on", w, r, user) - return + return LocalError("We weren't able to find the profile the reported post is supposed to be on", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } title = "Profile: " + title content = userReply.Content + "\n\nOriginal Post: @" + strconv.Itoa(userReply.ParentID) } else if itemType == "topic" { err = getTopicBasicStmt.QueryRow(itemID).Scan(&title, &content) if err == ErrNoRows { - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } title = "Topic: " + title content = content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID) } else { if vhooks["report_preassign"] != nil { runVhookNoreturn("report_preassign", &itemID, &itemType) - return + return nil } // Don't try to guess the type - LocalError("Unknown type", w, r, user) - return + return LocalError("Unknown type", w, r, user) } var count int rows, err := reportExistsStmt.Query(itemType + "_" + strconv.Itoa(itemID)) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } for rows.Next() { err = rows.Scan(&count) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } if count != 0 { - LocalError("Someone has already reported this!", w, r, user) - return + return LocalError("Someone has already reported this!", w, r, user) } // TODO: Repost attachments in the reports forum, so that the mods can see them // ? - Can we do this via the TopicStore? res, err := createReportStmt.Exec(title, content, parseMessage(content, 0, ""), user.ID, user.ID, itemType+"_"+strconv.Itoa(itemID)) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } lastID, err := res.LastInsertId() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = addTopicsToForumStmt.Exec(1, fid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = fstore.UpdateLastTopic(int(lastID), user.ID, fid) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/topic/"+strconv.FormatInt(lastID, 10), http.StatusSeeOther) + return nil } -func routeAccountOwnEditCritical(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditCritical(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } pi := Page{"Edit Password", user, headerVars, tList, nil} if preRenderHooks["pre_render_account_own_edit_critical"] != nil { if runPreRenderHook("pre_render_account_own_edit_critical", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit.html", pi) + err := templates.ExecuteTemplate(w, "account-own-edit.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditCriticalSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } var realPassword, salt string @@ -826,24 +736,19 @@ func routeAccountOwnEditCriticalSubmit(w http.ResponseWriter, r *http.Request, u err = getPasswordStmt.QueryRow(user.ID).Scan(&realPassword, &salt) if err == ErrNoRows { - LocalError("Your account no longer exists.", w, r, user) - return + return LocalError("Your account no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = CheckPassword(realPassword, currentPassword, salt) if err == ErrMismatchedHashAndPassword { - LocalError("That's not the correct password.", w, r, user) - return + return LocalError("That's not the correct password.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if newPassword != confirmPassword { - LocalError("The two passwords don't match.", w, r, user) - return + return LocalError("The two passwords don't match.", w, r, user) } SetPassword(user.ID, newPassword) @@ -854,51 +759,55 @@ func routeAccountOwnEditCriticalSubmit(w http.ResponseWriter, r *http.Request, u pi := Page{"Edit Password", user, headerVars, tList, nil} if preRenderHooks["pre_render_account_own_edit_critical"] != nil { if runPreRenderHook("pre_render_account_own_edit_critical", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit.html", pi) + err = templates.ExecuteTemplate(w, "account-own-edit.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditAvatar(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditAvatar(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } pi := Page{"Edit Avatar", user, headerVars, tList, nil} if preRenderHooks["pre_render_account_own_edit_avatar"] != nil { if runPreRenderHook("pre_render_account_own_edit_avatar", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-avatar.html", pi) + err := templates.ExecuteTemplate(w, "account-own-edit-avatar.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { if r.ContentLength > int64(config.MaxRequestSize) { size, unit := convertByteUnit(float64(config.MaxRequestSize)) - CustomError("Your avatar's too big. Avatars must be smaller than "+strconv.Itoa(int(size))+unit, http.StatusExpectationFailed, "Error", w, r, user) - return + return CustomError("Your avatar's too big. Avatars must be smaller than "+strconv.Itoa(int(size))+unit, http.StatusExpectationFailed, "Error", w, r, user) } r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxRequestSize)) - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } err := r.ParseMultipartForm(int64(megabyte)) if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } var filename, ext string @@ -906,8 +815,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use for _, hdr := range fheaders { infile, err := hdr.Open() if err != nil { - LocalError("Upload failed", w, r, user) - return + return LocalError("Upload failed", w, r, user) } defer infile.Close() @@ -916,8 +824,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use if filename != "" { if filename != hdr.Filename { os.Remove("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext) - LocalError("You may only upload one avatar", w, r, user) - return + return LocalError("You may only upload one avatar", w, r, user) } } else { filename = hdr.Filename @@ -926,16 +833,14 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use if ext == "" { extarr := strings.Split(hdr.Filename, ".") if len(extarr) < 2 { - LocalError("Bad file", w, r, user) - return + return LocalError("Bad file", w, r, user) } ext = extarr[len(extarr)-1] // TODO: Can we do this without a regex? reg, err := regexp.Compile("[^A-Za-z0-9]+") if err != nil { - LocalError("Bad file extension", w, r, user) - return + return LocalError("Bad file extension", w, r, user) } ext = reg.ReplaceAllString(ext, "") ext = strings.ToLower(ext) @@ -943,23 +848,20 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use outfile, err := os.Create("./uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext) if err != nil { - LocalError("Upload failed [File Creation Failed]", w, r, user) - return + return LocalError("Upload failed [File Creation Failed]", w, r, user) } defer outfile.Close() _, err = io.Copy(outfile, infile) if err != nil { - LocalError("Upload failed [Copy Failed]", w, r, user) - return + return LocalError("Upload failed [Copy Failed]", w, r, user) } } } err = user.ChangeAvatar("." + ext) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext @@ -967,50 +869,54 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use pi := Page{"Edit Avatar", user, headerVars, tList, nil} if preRenderHooks["pre_render_account_own_edit_avatar"] != nil { if runPreRenderHook("pre_render_account_own_edit_avatar", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-avatar.html", pi) + err = templates.ExecuteTemplate(w, "account-own-edit-avatar.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditUsername(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditUsername(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } pi := Page{"Edit Username", user, headerVars, tList, user.Name} if preRenderHooks["pre_render_account_own_edit_username"] != nil { if runPreRenderHook("pre_render_account_own_edit_username", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-username.html", pi) + err := templates.ExecuteTemplate(w, "account-own-edit-username.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } newUsername := html.EscapeString(r.PostFormValue("account-new-username")) err = user.ChangeName(newUsername) if err != nil { - LocalError("Unable to change the username. Does someone else already have this name?", w, r, user) - return + return LocalError("Unable to change the username. Does someone else already have this name?", w, r, user) } user.Name = newUsername @@ -1018,20 +924,23 @@ func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, u pi := Page{"Edit Username", user, headerVars, tList, nil} if preRenderHooks["pre_render_account_own_edit_username"] != nil { if runPreRenderHook("pre_render_account_own_edit_username", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-username.html", pi) + err = templates.ExecuteTemplate(w, "account-own-edit-username.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditEmail(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditEmail(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } email := Email{UserID: user.ID} @@ -1073,20 +982,23 @@ func routeAccountOwnEditEmail(w http.ResponseWriter, r *http.Request, user User) pi := Page{"Email Manager", user, headerVars, emailList, nil} if preRenderHooks["pre_render_account_own_edit_email"] != nil { if runPreRenderHook("pre_render_account_own_edit_email", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-email.html", pi) + err = templates.ExecuteTemplate(w, "account-own-edit-email.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeAccountOwnEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeAccountOwnEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Loggedin { - LocalError("You need to login to edit your account.", w, r, user) - return + return LocalError("You need to login to edit your account.", w, r, user) } token := r.URL.Path[len("/user/edit/token/"):] @@ -1095,16 +1007,14 @@ func routeAccountOwnEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, var emailList []interface{} rows, err := getEmailsByUserStmt.Query(user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows.Close() for rows.Next() { err := rows.Scan(&email.Email, &email.Validated, &email.Token) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if email.Email == user.Email { @@ -1117,31 +1027,26 @@ func routeAccountOwnEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if len(emailList) == 0 { - LocalError("A verification email was never sent for you!", w, r, user) - return + return LocalError("A verification email was never sent for you!", w, r, user) } if targetEmail.Token == "" { - LocalError("That's not a valid token!", w, r, user) - return + return LocalError("That's not a valid token!", w, r, user) } _, err = verifyEmailStmt.Exec(user.Email) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // If Email Activation is on, then activate the account while we're here if headerVars.Settings["activation_type"] == 2 { _, err = activateUserStmt.Exec(user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } @@ -1152,26 +1057,29 @@ func routeAccountOwnEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, pi := Page{"Email Manager", user, headerVars, emailList, nil} if preRenderHooks["pre_render_account_own_edit_email"] != nil { if runPreRenderHook("pre_render_account_own_edit_email", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w, "account-own-edit-email.html", pi) + err = templates.ExecuteTemplate(w, "account-own-edit-email.html", pi) + if err != nil { + return InternalError(err, w, r) + } + return nil } -func routeLogout(w http.ResponseWriter, r *http.Request, user User) { +func routeLogout(w http.ResponseWriter, r *http.Request, user User) RouteError { if !user.Loggedin { - LocalError("You can't logout without logging in first.", w, r, user) - return + return LocalError("You can't logout without logging in first.", w, r, user) } auth.Logout(w, user.ID) http.Redirect(w, r, "/", http.StatusSeeOther) + return nil } -func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, filename string) { +func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, filename string) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } filename = Stripslashes(filename) @@ -1179,14 +1087,12 @@ func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, file //log.Print("ext ", ext) //log.Print("filename ", filename) if !allowedFileExts.Contains(strings.TrimPrefix(ext, ".")) { - LocalError("Bad extension", w, r, user) - return + return LocalError("Bad extension", w, r, user) } sectionID, err := strconv.Atoi(r.FormValue("sectionID")) if err != nil { - LocalError("The sectionID is not an integer", w, r, user) - return + return LocalError("The sectionID is not an integer", w, r, user) } var sectionTable = r.FormValue("sectionType") @@ -1194,32 +1100,28 @@ func routeShowAttachment(w http.ResponseWriter, r *http.Request, user User, file var originID, uploadedBy int err = getAttachmentStmt.QueryRow(filename, sectionID, sectionTable).Scan(§ionID, §ionTable, &originID, &originTable, &uploadedBy, &filename) if err == ErrNoRows { - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if sectionTable == "forums" { - _, ok := SimpleForumUserCheck(w, r, &user, sectionID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, sectionID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } } else { - LocalError("Unknown section", w, r, user) - return + return LocalError("Unknown section", w, r, user) } if originTable != "topics" && originTable != "replies" { - LocalError("Unknown origin", w, r, user) - return + return LocalError("Unknown origin", w, r, user) } // TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side http.ServeFile(w, r, "./attachs/"+filename) + return nil } diff --git a/misc_test.go b/misc_test.go index 3a8660f8..f264c6b4 100644 --- a/misc_test.go +++ b/misc_test.go @@ -279,11 +279,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, user.IsMod, "Sam should be a mod") expect(t, !user.IsBanned, "Sam should not be banned") - _, success := forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr := forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user.Perms.ViewTopic, "Admins should be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") err = user.ChangeGroup(2) @@ -302,11 +302,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, user.IsMod, "Sam should be a mod") expect(t, !user.IsBanned, "Sam should not be banned") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user.Perms.ViewTopic, "Mods should be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") err = user.ChangeGroup(3) @@ -325,11 +325,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, !user.IsMod, "Sam should not be a mod") expect(t, !user.IsBanned, "Sam should not be banned") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") expect(t, user.Perms.ViewTopic != user2.Perms.ViewTopic, "user.Perms.ViewTopic and user2.Perms.ViewTopic should never match") @@ -349,11 +349,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, !user.IsMod, "Sam should not be a mod") expect(t, user.IsBanned, "Sam should be banned") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") err = user.ChangeGroup(5) @@ -372,11 +372,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, !user.IsMod, "Sam should not be a mod") expect(t, !user.IsBanned, "Sam should not be banned") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") err = user.ChangeGroup(6) @@ -395,11 +395,11 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, !user.IsMod, "Sam should not be a mod") expect(t, !user.IsBanned, "Sam should not be banned") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest1, user, reportsForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, !user.Perms.ViewTopic, "Members shouldn't be able to access the reports forum") - _, success = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) - expect(t, success, "There shouldn't be any errors in forumUserCheck") + _, ferr = forumUserCheck(dummyResponseRecorder, dummyRequest2, user2, generalForumID) + expect(t, ferr == nil, "There shouldn't be any errors in forumUserCheck") expect(t, user2.Perms.ViewTopic, "Sam should be able to access the general forum") err = user.ChangeGroup(config.DefaultGroup) diff --git a/mod_routes.go b/mod_routes.go index ea655b22..37f770e2 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -3,7 +3,9 @@ package main import ( //"log" //"fmt" + "encoding/json" "html" + "log" "net" "net/http" "strconv" @@ -12,51 +14,44 @@ import ( // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Disable stat updates in posts handled by plugin_socialgroups -func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { +func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } isJs := (r.PostFormValue("js") == "1") tid, err := strconv.Atoi(r.URL.Path[len("/topic/edit/submit/"):]) if err != nil { - PreErrorJSQ("The provided TopicID is not a valid number.", w, r, isJs) - return + return PreErrorJSQ("The provided TopicID is not a valid number.", w, r, isJs) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs) - return + return PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.EditTopic { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } topicName := r.PostFormValue("topic_name") topicContent := html.EscapeString(r.PostFormValue("topic_content")) err = topic.Update(topicName, topicContent) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } err = fstore.UpdateLastTopic(topic.ID, user.ID, topic.ParentID) if err != nil && err != ErrNoRows { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if !isJs { @@ -64,317 +59,323 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { } else { _, _ = w.Write(successJSONBytes) } + return nil } -// TODO: Add support for soft-deletion and add a permission just for hard delete +// TODO: Add support for soft-deletion and add a permission for hard delete in addition to the usual // TODO: Disable stat updates in posts handled by plugin_socialgroups -func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { - tid, err := strconv.Atoi(r.URL.Path[len("/topic/delete/submit/"):]) - if err != nil { - PreError("The provided TopicID is not a valid number.", w, r) - return +func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { + // TODO: Move this to some sort of middleware + var tids []int + var isJs = false + if r.Header.Get("Content-type") == "application/json" { + if r.Body == nil { + return PreErrorJS("No request body", w, r) + } + //log.Print("r.Body: ", r.Body) + err := json.NewDecoder(r.Body).Decode(&tids) + if err != nil { + //log.Print("parse err: ", err) + return PreErrorJS("We weren't able to parse your data", w, r) + } + isJs = true + } else { + tid, err := strconv.Atoi(r.URL.Path[len("/topic/delete/submit/"):]) + if err != nil { + return PreError("The provided TopicID is not a valid number.", w, r) + } + tids = append(tids, tid) + } + if len(tids) == 0 { + return LocalErrorJSQ("You haven't provided any IDs", w, r, user, isJs) } - topic, err := topics.Get(tid) - if err == ErrNoRows { - PreError("The topic you tried to delete doesn't exist.", w, r) - return - } else if err != nil { - InternalError(err, w) - return - } + for _, tid := range tids { + topic, err := topics.Get(tid) + if err == ErrNoRows { + return PreErrorJSQ("The topic you tried to delete doesn't exist.", w, r, isJs) + } else if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } - // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return - } - if !user.Perms.ViewTopic || !user.Perms.DeleteTopic { - NoPermissions(w, r, user) - return - } + // TODO: Add hooks to make use of headerLite + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.Perms.DeleteTopic { + return NoPermissionsJSQ(w, r, user, isJs) + } - // We might be able to handle this err better - err = topics.Delete(topic.CreatedBy) - if err != nil { - InternalError(err, w) - return - } + // We might be able to handle this err better + err = topic.Delete() + if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } - ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - LocalError("Bad IP", w, r, user) - return - } - err = addModLog("delete", tid, "topic", ipaddress, user.ID) - if err != nil { - InternalError(err, w) - return - } + ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return LocalErrorJSQ("Bad IP", w, r, user, isJs) + } + err = addModLog("delete", tid, "topic", ipaddress, user.ID) + if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } - // ? - We might need to add soft-delete before we can do an action reply for this - /*_, err = createActionReplyStmt.Exec(tid,"delete",ipaddress,user.ID) - if err != nil { - InternalError(err,w) - return - }*/ + // ? - We might need to add soft-delete before we can do an action reply for this + /*_, err = createActionReplyStmt.Exec(tid,"delete",ipaddress,user.ID) + if err != nil { + return InternalErrorJSQ(err,w,r,isJs) + }*/ - //log.Print("Topic #" + strconv.Itoa(tid) + " was deleted by User #" + strconv.Itoa(user.ID)) + log.Printf("Topic #%d was deleted by User #%d", tid, user.ID) + } http.Redirect(w, r, "/", http.StatusSeeOther) + return nil } -func routeStickTopic(w http.ResponseWriter, r *http.Request, user User) { +func routeStickTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { tid, err := strconv.Atoi(r.URL.Path[len("/topic/stick/submit/"):]) if err != nil { - PreError("The provided TopicID is not a valid number.", w, r) - return + return PreError("The provided TopicID is not a valid number.", w, r) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreError("The topic you tried to pin doesn't exist.", w, r) - return + return PreError("The topic you tried to pin doesn't exist.", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.PinTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err = topic.Stick() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // ! - Can we use user.LastIP here? It might be racey, if another thread mutates it... We need to fix this. ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("stick", tid, "topic", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = topic.CreateActionReply("stick", ipaddress, user) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } -func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user User) { +func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { tid, err := strconv.Atoi(r.URL.Path[len("/topic/unstick/submit/"):]) if err != nil { - PreError("The provided TopicID is not a valid number.", w, r) - return + return PreError("The provided TopicID is not a valid number.", w, r) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreError("The topic you tried to unpin doesn't exist.", w, r) - return + return PreError("The topic you tried to unpin doesn't exist.", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.PinTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err = topic.Unstick() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("unstick", tid, "topic", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = topic.CreateActionReply("unstick", ipaddress, user) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } + http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } -func routeLockTopic(w http.ResponseWriter, r *http.Request, user User) { - tid, err := strconv.Atoi(r.URL.Path[len("/topic/lock/submit/"):]) - if err != nil { - PreError("The provided TopicID is not a valid number.", w, r) - return +func routeLockTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { + // TODO: Move this to some sort of middleware + var tids []int + var isJs = false + if r.Header.Get("Content-type") == "application/json" { + if r.Body == nil { + return PreErrorJS("No request body", w, r) + } + err := json.NewDecoder(r.Body).Decode(&tids) + if err != nil { + return PreErrorJS("We weren't able to parse your data", w, r) + } + isJs = true + } else { + tid, err := strconv.Atoi(r.URL.Path[len("/topic/lock/submit/"):]) + if err != nil { + return PreError("The provided TopicID is not a valid number.", w, r) + } + tids = append(tids, tid) + } + if len(tids) == 0 { + return LocalErrorJSQ("You haven't provided any IDs", w, r, user, isJs) } - topic, err := topics.Get(tid) - if err == ErrNoRows { - PreError("The topic you tried to pin doesn't exist.", w, r) - return - } else if err != nil { - InternalError(err, w) - return + for _, tid := range tids { + topic, err := topics.Get(tid) + if err == ErrNoRows { + return PreErrorJSQ("The topic you tried to lock doesn't exist.", w, r, isJs) + } else if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } + + // TODO: Add hooks to make use of headerLite + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.Perms.CloseTopic { + return NoPermissionsJSQ(w, r, user, isJs) + } + + err = topic.Lock() + if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } + + // ! - Can we use user.LastIP here? It might be racey, if another thread mutates it... We need to fix this. + ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return LocalErrorJSQ("Bad IP", w, r, user, isJs) + } + err = addModLog("lock", tid, "topic", ipaddress, user.ID) + if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } + err = topic.CreateActionReply("lock", ipaddress, user) + if err != nil { + return InternalErrorJSQ(err, w, r, isJs) + } } - // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + if len(tids) == 1 { + http.Redirect(w, r, "/topic/"+strconv.Itoa(tids[0]), http.StatusSeeOther) } - if !user.Perms.ViewTopic || !user.Perms.CloseTopic { - NoPermissions(w, r, user) - return - } - - err = topic.Lock() - if err != nil { - InternalError(err, w) - return - } - - // ! - Can we use user.LastIP here? It might be racey, if another thread mutates it... We need to fix this. - ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - LocalError("Bad IP", w, r, user) - return - } - err = addModLog("lock", tid, "topic", ipaddress, user.ID) - if err != nil { - InternalError(err, w) - return - } - err = topic.CreateActionReply("lock", ipaddress, user) - if err != nil { - InternalError(err, w) - return - } - http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } -func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user User) { +func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user User) RouteError { tid, err := strconv.Atoi(r.URL.Path[len("/topic/unlock/submit/"):]) if err != nil { - PreError("The provided TopicID is not a valid number.", w, r) - return + return PreError("The provided TopicID is not a valid number.", w, r) } topic, err := topics.Get(tid) if err == ErrNoRows { - PreError("The topic you tried to pin doesn't exist.", w, r) - return + return PreError("The topic you tried to unlock doesn't exist.", w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.CloseTopic { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err = topic.Unlock() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // ! - Can we use user.LastIP here? It might be racey, if another thread mutates it... We need to fix this. ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("unlock", tid, "topic", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } err = topic.CreateActionReply("unlock", ipaddress, user) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } + http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + return nil } // TODO: Disable stat updates in posts handled by plugin_socialgroups // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes -func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } isJs := (r.PostFormValue("js") == "1") rid, err := strconv.Atoi(r.URL.Path[len("/reply/edit/submit/"):]) if err != nil { - PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) - return + return PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) } // Get the Reply ID.. var tid int err = getReplyTIDStmt.QueryRow(rid).Scan(&tid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } var fid int err = getTopicFIDStmt.QueryRow(tid).Scan(&fid) if err == ErrNoRows { - PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) - return + return PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, fid) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.EditReply { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) _, err = editReplyStmt.Exec(content, parseMessage(content, fid, "forums"), rid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if !isJs { @@ -382,60 +383,53 @@ func routeReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { } else { w.Write(successJSONBytes) } + return nil } // TODO: Refactor this // TODO: Disable stat updates in posts handled by plugin_socialgroups -func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } isJs := (r.PostFormValue("isJs") == "1") rid, err := strconv.Atoi(r.URL.Path[len("/reply/delete/submit/"):]) if err != nil { - PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) - return + return PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) } reply, err := rstore.Get(rid) if err == ErrNoRows { - PreErrorJSQ("The reply you tried to delete doesn't exist.", w, r, isJs) - return + return PreErrorJSQ("The reply you tried to delete doesn't exist.", w, r, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } var fid int err = getTopicFIDStmt.QueryRow(reply.ParentID).Scan(&fid) if err == ErrNoRows { - PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) - return + return PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } // TODO: Add hooks to make use of headerLite - _, ok := SimpleForumUserCheck(w, r, &user, fid) - if !ok { - return + _, ferr := SimpleForumUserCheck(w, r, &user, fid) + if ferr != nil { + return ferr } if !user.Perms.ViewTopic || !user.Perms.DeleteReply { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } err = reply.Delete() if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } - //log.Print("Reply #" + strconv.Itoa(rid) + " was deleted by User #" + strconv.Itoa(user.ID)) + //log.Printf("Reply #%d was deleted by User #%d", rid, user.ID) if !isJs { //http.Redirect(w,r, "/topic/" + strconv.Itoa(tid), http.StatusSeeOther) } else { @@ -447,57 +441,50 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { wcount := wordCount(reply.Content) err = replyCreator.decreasePostStats(wcount, false) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } } else if err != ErrNoRows { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalErrorJSQ("Bad IP", w, r, user, isJs) - return + return LocalErrorJSQ("Bad IP", w, r, user, isJs) } err = addModLog("delete", reply.ParentID, "reply", ipaddress, user.ID) if err != nil { - InternalErrorJSQ(err, w, r, isJs) + return InternalErrorJSQ(err, w, r, isJs) } + return nil } -func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } isJs := (r.PostFormValue("js") == "1") rid, err := strconv.Atoi(r.URL.Path[len("/profile/reply/edit/submit/"):]) if err != nil { - LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs) - return + return LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs) } // Get the Reply ID.. var uid int err = getUserReplyUIDStmt.QueryRow(rid).Scan(&uid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if user.ID != uid && !user.Perms.EditReply { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } content := html.EscapeString(preparseMessage(r.PostFormValue("edit_item"))) _, err = editProfileReplyStmt.Exec(content, parseMessage(content, 0, ""), rid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if !isJs { @@ -505,59 +492,54 @@ func routeProfileReplyEditSubmit(w http.ResponseWriter, r *http.Request, user Us } else { w.Write(successJSONBytes) } + return nil } -func routeProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeProfileReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } isJs := (r.PostFormValue("isJs") == "1") rid, err := strconv.Atoi(r.URL.Path[len("/profile/reply/delete/submit/"):]) if err != nil { - LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs) - return + return LocalErrorJSQ("The provided Reply ID is not a valid number.", w, r, user, isJs) } var uid int err = getUserReplyUIDStmt.QueryRow(rid).Scan(&uid) if err == ErrNoRows { - LocalErrorJSQ("The reply you tried to delete doesn't exist.", w, r, user, isJs) - return + return LocalErrorJSQ("The reply you tried to delete doesn't exist.", w, r, user, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if user.ID != uid && !user.Perms.DeleteReply { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } _, err = deleteProfileReplyStmt.Exec(rid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } - //log.Print("The profile post '" + strconv.Itoa(rid) + "' was deleted by User #" + strconv.Itoa(user.ID)) + //log.Printf("The profile post '%d' was deleted by User #%d", rid, user.ID) if !isJs { //http.Redirect(w,r, "/user/" + strconv.Itoa(uid), http.StatusSeeOther) } else { w.Write(successJSONBytes) } + return nil } -func routeIps(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func routeIps(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ViewIPs { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } var ip = r.FormValue("ip") @@ -566,65 +548,56 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) { rows, err := findUsersByIPUsersStmt.Query(ip) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows.Close() for rows.Next() { err := rows.Scan(&uid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } reqUserList[uid] = true } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } rows2, err := findUsersByIPTopicsStmt.Query(ip) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows2.Close() for rows2.Next() { err := rows2.Scan(&uid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } reqUserList[uid] = true } err = rows2.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } rows3, err := findUsersByIPRepliesStmt.Query(ip) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows3.Close() for rows3.Next() { err := rows3.Scan(&uid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } reqUserList[uid] = true } err = rows3.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Convert the user ID map to a slice, then bulk load the users @@ -638,47 +611,43 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) { // TODO: What if a user is deleted via the Control Panel? userList, err := users.BulkGetMap(idSlice) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } pi := IPSearchPage{"IP Search", user, headerVars, userList, ip} if preRenderHooks["pre_render_ips"] != nil { if runPreRenderHook("pre_render_ips", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "ip-search.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } // TODO: This is being replaced with the new ban route system -/*func routeBan(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w,r,&user) - if !ok { +/*func routeBan(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w,r,&user) + if ferr != nil { return } if !user.Perms.BanUsers { - NoPermissions(w,r,user) - return + return NoPermissions(w,r,user) } uid, err := strconv.Atoi(r.URL.Path[len("/users/ban/"):]) if err != nil { - LocalError("The provided User ID is not a valid number.",w,r,user) - return + return LocalError("The provided User ID is not a valid number.",w,r,user) } var uname string err = get_user_name_stmt.QueryRow(uid).Scan(&uname) if err == ErrNoRows { - LocalError("The user you're trying to ban no longer exists.",w,r,user) - return + return LocalError("The user you're trying to ban no longer exists.",w,r,user) } else if err != nil { - InternalError(err,w) - return + return InternalError(err,w,r) } confirm_msg := "Are you sure you want to ban '" + uname + "'?" @@ -687,71 +656,63 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) { pi := Page{"Ban User",user,headerVars,tList,yousure} if preRenderHooks["pre_render_ban"] != nil { if runPreRenderHook("pre_render_ban", w, r, &user, &pi) { - return + return nil } } - templates.ExecuteTemplate(w,"areyousure.html",pi) + err = templates.ExecuteTemplate(w,"areyousure.html",pi) + if err != nil { + return InternalError(err,w,r) + } + return nil }*/ -func routeBanSubmit(w http.ResponseWriter, r *http.Request, user User) { +func routeBanSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { if !user.Perms.BanUsers { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } uid, err := strconv.Atoi(r.URL.Path[len("/users/ban/submit/"):]) if err != nil { - LocalError("The provided User ID is not a valid number.", w, r, user) - return + return LocalError("The provided User ID is not a valid number.", w, r, user) } /*if uid == -2 { - LocalError("Stop trying to ban Merlin! Ban admin! Bad! No!",w,r,user) - return + return LocalError("Stop trying to ban Merlin! Ban admin! Bad! No!",w,r,user) }*/ targetUser, err := users.Get(uid) if err == ErrNoRows { - LocalError("The user you're trying to ban no longer exists.", w, r, user) - return + return LocalError("The user you're trying to ban no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if targetUser.IsSuperAdmin || targetUser.IsAdmin || targetUser.IsMod { - LocalError("You may not ban another staff member.", w, r, user) - return + return LocalError("You may not ban another staff member.", w, r, user) } if uid == user.ID { - LocalError("Why are you trying to ban yourself? Stop that.", w, r, user) - return + return LocalError("Why are you trying to ban yourself? Stop that.", w, r, user) } if targetUser.IsBanned { - LocalError("The user you're trying to unban is already banned.", w, r, user) - return + return LocalError("The user you're trying to unban is already banned.", w, r, user) } durationDays, err := strconv.Atoi(r.FormValue("ban-duration-days")) if err != nil { - LocalError("You can only use whole numbers for the number of days", w, r, user) - return + return LocalError("You can only use whole numbers for the number of days", w, r, user) } durationWeeks, err := strconv.Atoi(r.FormValue("ban-duration-weeks")) if err != nil { - LocalError("You can only use whole numbers for the number of weeks", w, r, user) - return + return LocalError("You can only use whole numbers for the number of weeks", w, r, user) } durationMonths, err := strconv.Atoi(r.FormValue("ban-duration-months")) if err != nil { - LocalError("You can only use whole numbers for the number of months", w, r, user) - return + return LocalError("You can only use whole numbers for the number of months", w, r, user) } var duration time.Duration @@ -767,127 +728,106 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user User) { err = targetUser.Ban(duration, user.ID) if err == ErrNoRows { - LocalError("The user you're trying to ban no longer exists.", w, r, user) - return + return LocalError("The user you're trying to ban no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("ban", uid, "user", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther) + return nil } -func routeUnban(w http.ResponseWriter, r *http.Request, user User) { +func routeUnban(w http.ResponseWriter, r *http.Request, user User) RouteError { if !user.Perms.BanUsers { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } uid, err := strconv.Atoi(r.URL.Path[len("/users/unban/"):]) if err != nil { - LocalError("The provided User ID is not a valid number.", w, r, user) - return + return LocalError("The provided User ID is not a valid number.", w, r, user) } targetUser, err := users.Get(uid) if err == ErrNoRows { - LocalError("The user you're trying to unban no longer exists.", w, r, user) - return + return LocalError("The user you're trying to unban no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if !targetUser.IsBanned { - LocalError("The user you're trying to unban isn't banned.", w, r, user) - return + return LocalError("The user you're trying to unban isn't banned.", w, r, user) } err = targetUser.Unban() if err == ErrNoTempGroup { - LocalError("The user you're trying to unban is not banned", w, r, user) - return + return LocalError("The user you're trying to unban is not banned", w, r, user) } else if err == ErrNoRows { - LocalError("The user you're trying to unban no longer exists.", w, r, user) - return + return LocalError("The user you're trying to unban no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("unban", uid, "user", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/user/"+strconv.Itoa(uid), http.StatusSeeOther) + return nil } -func routeActivate(w http.ResponseWriter, r *http.Request, user User) { +func routeActivate(w http.ResponseWriter, r *http.Request, user User) RouteError { if !user.Perms.ActivateUsers { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } uid, err := strconv.Atoi(r.URL.Path[len("/users/activate/"):]) if err != nil { - LocalError("The provided User ID is not a valid number.", w, r, user) - return + return LocalError("The provided User ID is not a valid number.", w, r, user) } targetUser, err := users.Get(uid) if err == ErrNoRows { - LocalError("The account you're trying to activate no longer exists.", w, r, user) - return + return LocalError("The account you're trying to activate no longer exists.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if targetUser.Active { - LocalError("The account you're trying to activate has already been activated.", w, r, user) - return + return LocalError("The account you're trying to activate has already been activated.", w, r, user) } err = targetUser.Activate() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } ipaddress, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { - LocalError("Bad IP", w, r, user) - return + return LocalError("Bad IP", w, r, user) } err = addModLog("activate", targetUser.ID, "user", ipaddress, user.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/user/"+strconv.Itoa(targetUser.ID), http.StatusSeeOther) + return nil } diff --git a/pages.go b/pages.go index 91c19adb..0a7ee4a7 100644 --- a/pages.go +++ b/pages.go @@ -234,7 +234,7 @@ type PanelEditGroupPermsPage struct { type backupItem struct { SQLURL string - // TODO: Add an easier to parse format here for Gosora to be able to more easily reimport portions of the dump and to strip unneccesary data (e.g. table defs and parsed post data) + // TODO: Add an easier to parse format here for Gosora to be able to more easily reimport portions of the dump and to strip unneccessary data (e.g. table defs and parsed post data) Timestamp time.Time } diff --git a/panel_routes.go b/panel_routes.go index ae89d5d7..e788a078 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -17,10 +17,10 @@ import ( "github.com/Azareal/gopsutil/mem" ) -func routePanel(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanel(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } // We won't calculate this on the spot anymore, as the system doesn't seem to like it if we do multiple fetches simultaneously. Should we constantly calculate this on a background thread? Perhaps, the watchdog to scale back heavy features under load? One plus side is that we'd get immediate CPU percentages here instead of waiting it to kick in with WebSockets @@ -65,8 +65,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user User) { var postCount int err = todaysPostCountStmt.QueryRow().Scan(&postCount) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var postInterval = "day" @@ -82,8 +81,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user User) { var topicCount int err = todaysTopicCountStmt.QueryRow().Scan(&topicCount) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var topicInterval = "day" @@ -99,16 +97,14 @@ func routePanel(w http.ResponseWriter, r *http.Request, user User) { var reportCount int err = todaysReportCountStmt.QueryRow().Scan(&reportCount) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var reportInterval = "week" var newUserCount int err = todaysNewUserCountStmt.QueryRow().Scan(&newUserCount) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var newUserInterval = "week" @@ -174,31 +170,30 @@ func routePanel(w http.ResponseWriter, r *http.Request, user User) { pi := PanelDashboardPage{"Control Panel Dashboard", user, headerVars, stats, gridElements} if preRenderHooks["pre_render_panel_dashboard"] != nil { if runPreRenderHook("pre_render_panel_dashboard", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-dashboard.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelForums(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForums(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } // TODO: Paginate this? var forumList []interface{} forums, err := fstore.GetAll() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // ? - Should we generate something similar to the forumView? It might be a little overkill for a page which is rarely loaded in comparison to /forums/ @@ -214,33 +209,31 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user User) { pi := PanelPage{"Forum Manager", user, headerVars, stats, forumList, nil} if preRenderHooks["pre_render_panel_forums"] != nil { if runPreRenderHook("pre_render_panel_forums", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-forums.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } fname := r.PostFormValue("forum-name") @@ -251,41 +244,36 @@ func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user U _, err = fstore.Create(fname, fdesc, active, fpreset) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/panel/forums/", http.StatusSeeOther) + return nil } // TODO: Revamp this -func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user User, sfid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } fid, err := strconv.Atoi(sfid) if err != nil { - LocalError("The provided Forum ID is not a valid number.", w, r, user) - return + return LocalError("The provided Forum ID is not a valid number.", w, r, user) } forum, err := fstore.Get(fid) if err == ErrNoRows { - LocalError("The forum you're trying to delete doesn't exist.", w, r, user) - return + return LocalError("The forum you're trying to delete doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?" @@ -294,70 +282,64 @@ func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user User, s pi := PanelPage{"Delete Forum", user, headerVars, stats, tList, yousure} if preRenderHooks["pre_render_panel_delete_forum"] != nil { if runPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "areyousure.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } fid, err := strconv.Atoi(sfid) if err != nil { - LocalError("The provided Forum ID is not a valid number.", w, r, user) - return + return LocalError("The provided Forum ID is not a valid number.", w, r, user) } err = fstore.Delete(fid) if err == ErrNoRows { - LocalError("The forum you're trying to delete doesn't exist.", w, r, user) - return + return LocalError("The forum you're trying to delete doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/panel/forums/", http.StatusSeeOther) + return nil } -func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } fid, err := strconv.Atoi(sfid) if err != nil { - LocalError("The provided Forum ID is not a valid number.", w, r, user) - return + return LocalError("The provided Forum ID is not a valid number.", w, r, user) } forum, err := fstore.Get(fid) if err == ErrNoRows { - LocalError("The forum you're trying to edit doesn't exist.", w, r, user) - return + return LocalError("The forum you're trying to edit doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + + return InternalError(err, w, r) } if forum.Preset == "" { @@ -366,8 +348,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfi glist, err := gstore.GetAll() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } var gplist []GroupForumPermPreset @@ -381,49 +362,44 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfi pi := PanelEditForumPage{"Forum Editor", user, headerVars, stats, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist} if preRenderHooks["pre_render_panel_edit_forum"] != nil { if runPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-forum-edit.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } isJs := (r.PostFormValue("js") == "1") fid, err := strconv.Atoi(sfid) if err != nil { - LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) - return + return LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) } forum, err := fstore.Get(fid) if err == ErrNoRows { - LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) - return + return LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } forumName := r.PostFormValue("forum_name") @@ -440,8 +416,7 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user Use err = forum.Update(forumName, forumDesc, active, forumPreset) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } if !isJs { @@ -449,39 +424,35 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user Use } else { w.Write(successJSONBytes) } + return nil } -func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user User, sfid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageForums { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } isJs := (r.PostFormValue("js") == "1") fid, err := strconv.Atoi(sfid) if err != nil { - LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) - return + return LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs) } gid, err := strconv.Atoi(r.PostFormValue("gid")) if err != nil { - LocalErrorJSQ("Invalid Group ID", w, r, user, isJs) - return + return LocalErrorJSQ("Invalid Group ID", w, r, user, isJs) } permPreset := stripInvalidGroupForumPreset(r.PostFormValue("perm_preset")) @@ -489,11 +460,9 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use forum, err := fstore.Get(fid) if err == ErrNoRows { - LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) - return + return LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) } else if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } forumUpdateMutex.Lock() @@ -503,28 +472,24 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use defer permUpdateMutex.Unlock() group, err := gstore.Get(gid) if err != nil { - LocalError("The group whose permissions you're updating doesn't exist.", w, r, user) - return + return LocalError("The group whose permissions you're updating doesn't exist.", w, r, user) } group.Forums[fid] = fperms err = replaceForumPermsForGroup(gid, map[int]string{fid: permPreset}, map[int]ForumPerms{fid: fperms}) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } // TODO: Add this and replaceForumPermsForGroup into a transaction? _, err = updateForumStmt.Exec(forum.Name, forum.Desc, forum.Active, "", fid) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } err = fstore.Reload(fid) if err != nil { - // Log this? -- Another admin might have deleted it - LocalErrorJSQ("Unable to reload forum", w, r, user, isJs) - return + // TODO: Log this? -- Another admin might have deleted it + return LocalErrorJSQ("Unable to reload forum", w, r, user, isJs) } } @@ -533,24 +498,23 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use } else { w.Write(successJSONBytes) } + return nil } -func routePanelSettings(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelSettings(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } //log.Print("headerVars.Settings",headerVars.Settings) var settingList = make(map[string]interface{}) rows, err := getSettingsStmt.Query() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows.Close() @@ -560,8 +524,7 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user User) { for rows.Next() { err := rows.Scan(&sname, &scontent, &stype) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if stype == "list" { @@ -569,8 +532,7 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user User) { labels := strings.Split(llist, ",") conv, err := strconv.Atoi(scontent) if err != nil { - LocalError("The setting '"+sname+"' can't be converted to an integer", w, r, user) - return + return LocalError("The setting '"+sname+"' can't be converted to an integer", w, r, user) } scontent = labels[conv-1] } else if stype == "bool" { @@ -584,40 +546,37 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user User) { } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } pi := PanelPage{"Setting Manager", user, headerVars, stats, tList, settingList} if preRenderHooks["pre_render_panel_settings"] != nil { if runPreRenderHook("pre_render_panel_settings", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-settings.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelSetting(w http.ResponseWriter, r *http.Request, user User, sname string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelSetting(w http.ResponseWriter, r *http.Request, user User, sname string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } setting := Setting{sname, "", "", ""} err := getSettingStmt.QueryRow(setting.Name).Scan(&setting.Content, &setting.Type) if err == ErrNoRows { - LocalError("The setting you want to edit doesn't exist.", w, r, user) - return + return LocalError("The setting you want to edit doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } var itemList []interface{} @@ -625,8 +584,7 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user User, sname llist := GetSettingLabel(setting.Name) conv, err := strconv.Atoi(setting.Content) if err != nil { - LocalError("The value of this setting couldn't be converted to an integer", w, r, user) - return + return LocalError("The value of this setting couldn't be converted to an integer", w, r, user) } labels := strings.Split(llist, ",") @@ -642,33 +600,31 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user User, sname pi := PanelPage{"Edit Setting", user, headerVars, stats, itemList, setting} if preRenderHooks["pre_render_panel_setting"] != nil { if runPreRenderHook("pre_render_panel_setting", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-setting.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user User, sname string) { - headerLite, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user User, sname string) RouteError { + headerLite, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - LocalError("Bad Form", w, r, user) - return + return LocalError("Bad Form", w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } var stype, sconstraints string @@ -676,11 +632,9 @@ func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user User, sn err = getFullSettingStmt.QueryRow(sname).Scan(&sname, &stype, &sconstraints) if err == ErrNoRows { - LocalError("The setting you want to edit doesn't exist.", w, r, user) - return + return LocalError("The setting you want to edit doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if stype == "bool" { @@ -694,64 +648,60 @@ func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user User, sn // TODO: Make this a method or function? _, err = updateSettingStmt.Exec(scontent, sname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } errmsg := headerLite.Settings.ParseSetting(sname, scontent, stype, sconstraints) if errmsg != "" { - LocalError(errmsg, w, r, user) - return + return LocalError(errmsg, w, r, user) } settingBox.Store(headerLite.Settings) http.Redirect(w, r, "/panel/settings/", http.StatusSeeOther) + return nil } -func routePanelWordFilters(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelWordFilters(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return nil } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } var filterList = wordFilterBox.Load().(WordFilterBox) pi := PanelPage{"Word Filter Manager", user, headerVars, stats, tList, filterList} if preRenderHooks["pre_render_panel_word_filters"] != nil { if runPreRenderHook("pre_render_panel_word_filters", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "panel-word-filters.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelWordFiltersCreate(w http.ResponseWriter, r *http.Request, user User) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelWordFiltersCreate(w http.ResponseWriter, r *http.Request, user User) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } isJs := (r.PostFormValue("js") == "1") find := strings.TrimSpace(r.PostFormValue("find")) if find == "" { - LocalErrorJSQ("You need to specify what word you want to match", w, r, user, isJs) - return + return LocalErrorJSQ("You need to specify what word you want to match", w, r, user, isJs) } // Unlike with find, it's okay if we leave this blank, as this means that the admin wants to remove the word entirely with no replacement @@ -759,27 +709,29 @@ func routePanelWordFiltersCreate(w http.ResponseWriter, r *http.Request, user Us res, err := createWordFilterStmt.Exec(find, replacement) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } lastID, err := res.LastInsertId() if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } addWordFilter(int(lastID), find, replacement) - http.Redirect(w, r, "/panel/settings/word-filters/", http.StatusSeeOther) // TODO: Return json for JS? + if !isJs { + http.Redirect(w, r, "/panel/settings/word-filters/", http.StatusSeeOther) + } else { + w.Write(successJSONBytes) + } + return nil } -func routePanelWordFiltersEdit(w http.ResponseWriter, r *http.Request, user User, wfid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelWordFiltersEdit(w http.ResponseWriter, r *http.Request, user User, wfid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditSettings { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } _ = wfid @@ -787,43 +739,40 @@ func routePanelWordFiltersEdit(w http.ResponseWriter, r *http.Request, user User pi := PanelPage{"Edit Word Filter", user, headerVars, stats, tList, nil} if preRenderHooks["pre_render_panel_word_filters_edit"] != nil { if runPreRenderHook("pre_render_panel_word_filters_edit", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "panel-word-filters-edit.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelWordFiltersEditSubmit(w http.ResponseWriter, r *http.Request, user User, wfid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelWordFiltersEditSubmit(w http.ResponseWriter, r *http.Request, user User, wfid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } // TODO: Either call it isJs or js rather than flip-flopping back and forth across the routes x.x isJs := (r.PostFormValue("isJs") == "1") if !user.Perms.EditSettings { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } id, err := strconv.Atoi(wfid) if err != nil { - LocalErrorJSQ("The word filter ID must be an integer.", w, r, user, isJs) - return + return LocalErrorJSQ("The word filter ID must be an integer.", w, r, user, isJs) } find := strings.TrimSpace(r.PostFormValue("find")) if find == "" { - LocalErrorJSQ("You need to specify what word you want to match", w, r, user, isJs) - return + return LocalErrorJSQ("You need to specify what word you want to match", w, r, user, isJs) } // Unlike with find, it's okay if we leave this blank, as this means that the admin wants to remove the word entirely with no replacement @@ -831,8 +780,7 @@ func routePanelWordFiltersEditSubmit(w http.ResponseWriter, r *http.Request, use _, err = updateWordFilterStmt.Exec(find, replacement, id) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } wordFilters := wordFilterBox.Load().(WordFilterBox) @@ -840,35 +788,32 @@ func routePanelWordFiltersEditSubmit(w http.ResponseWriter, r *http.Request, use wordFilterBox.Store(wordFilters) http.Redirect(w, r, "/panel/settings/word-filters/", http.StatusSeeOther) + return nil } -func routePanelWordFiltersDeleteSubmit(w http.ResponseWriter, r *http.Request, user User, wfid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelWordFiltersDeleteSubmit(w http.ResponseWriter, r *http.Request, user User, wfid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } err := r.ParseForm() if err != nil { - PreError("Bad Form", w, r) - return + return PreError("Bad Form", w, r) } isJs := (r.PostFormValue("isJs") == "1") if !user.Perms.EditSettings { - NoPermissionsJSQ(w, r, user, isJs) - return + return NoPermissionsJSQ(w, r, user, isJs) } id, err := strconv.Atoi(wfid) if err != nil { - LocalErrorJSQ("The word filter ID must be an integer.", w, r, user, isJs) - return + return LocalErrorJSQ("The word filter ID must be an integer.", w, r, user, isJs) } _, err = deleteWordFilterStmt.Exec(id) if err != nil { - InternalErrorJSQ(err, w, r, isJs) - return + return InternalErrorJSQ(err, w, r, isJs) } wordFilters := wordFilterBox.Load().(WordFilterBox) @@ -876,98 +821,90 @@ func routePanelWordFiltersDeleteSubmit(w http.ResponseWriter, r *http.Request, u wordFilterBox.Store(wordFilters) http.Redirect(w, r, "/panel/settings/word-filters/", http.StatusSeeOther) + return nil } -func routePanelPlugins(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelPlugins(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManagePlugins { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } var pluginList []interface{} for _, plugin := range plugins { - //log.Print("plugin.Name",plugin.Name) - //log.Print("plugin.Installed",plugin.Installed) + //log.Print("plugin.Name ", plugin.Name) + //log.Print("plugin.Installed ", plugin.Installed) pluginList = append(pluginList, plugin) } pi := PanelPage{"Plugin Manager", user, headerVars, stats, pluginList, nil} if preRenderHooks["pre_render_panel_plugins"] != nil { if runPreRenderHook("pre_render_panel_plugins", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "panel-plugins.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelPluginsActivate(w http.ResponseWriter, r *http.Request, user User, uname string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelPluginsActivate(w http.ResponseWriter, r *http.Request, user User, uname string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManagePlugins { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } //log.Print("uname","'"+uname+"'") plugin, ok := plugins[uname] if !ok { - LocalError("The plugin isn't registered in the system", w, r, user) - return + return LocalError("The plugin isn't registered in the system", w, r, user) } if plugin.Installable && !plugin.Installed { - LocalError("You can't activate this plugin without installing it first", w, r, user) - return + return LocalError("You can't activate this plugin without installing it first", w, r, user) } var active bool err := isPluginActiveStmt.QueryRow(uname).Scan(&active) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var hasPlugin = (err == nil) if plugins[uname].Activate != nil { err = plugins[uname].Activate() if err != nil { - LocalError(err.Error(), w, r, user) - return + return LocalError(err.Error(), w, r, user) } } - //log.Print("err",err) - //log.Print("active",active) + //log.Print("err", err) + //log.Print("active", active) if hasPlugin { if active { - LocalError("The plugin is already active", w, r, user) - return + return LocalError("The plugin is already active", w, r, user) } //log.Print("updatePlugin") _, err = updatePluginStmt.Exec(1, uname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } else { //log.Print("addPlugin") _, err := addPluginStmt.Exec(uname, 1, 0) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } @@ -976,52 +913,44 @@ func routePanelPluginsActivate(w http.ResponseWriter, r *http.Request, user User plugins[uname] = plugin err = plugins[uname].Init() if err != nil { - LocalError(err.Error(), w, r, user) - return + return LocalError(err.Error(), w, r, user) } http.Redirect(w, r, "/panel/plugins/", http.StatusSeeOther) + return nil } -func routePanelPluginsDeactivate(w http.ResponseWriter, r *http.Request, user User, uname string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelPluginsDeactivate(w http.ResponseWriter, r *http.Request, user User, uname string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManagePlugins { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } - if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } plugin, ok := plugins[uname] if !ok { - LocalError("The plugin isn't registered in the system", w, r, user) - return + return LocalError("The plugin isn't registered in the system", w, r, user) } var active bool err := isPluginActiveStmt.QueryRow(uname).Scan(&active) if err == ErrNoRows { - LocalError("The plugin you're trying to deactivate isn't active", w, r, user) - return + return LocalError("The plugin you're trying to deactivate isn't active", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if !active { - LocalError("The plugin you're trying to deactivate isn't active", w, r, user) - return + return LocalError("The plugin you're trying to deactivate isn't active", w, r, user) } _, err = updatePluginStmt.Exec(0, uname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } plugin.Active = false @@ -1029,78 +958,68 @@ func routePanelPluginsDeactivate(w http.ResponseWriter, r *http.Request, user Us plugins[uname].Deactivate() http.Redirect(w, r, "/panel/plugins/", http.StatusSeeOther) + return nil } -func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user User, uname string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user User, uname string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManagePlugins { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } plugin, ok := plugins[uname] if !ok { - LocalError("The plugin isn't registered in the system", w, r, user) - return + return LocalError("The plugin isn't registered in the system", w, r, user) } if !plugin.Installable { - LocalError("This plugin is not installable", w, r, user) - return + return LocalError("This plugin is not installable", w, r, user) } if plugin.Installed { - LocalError("This plugin has already been installed", w, r, user) - return + return LocalError("This plugin has already been installed", w, r, user) } var active bool err := isPluginActiveStmt.QueryRow(uname).Scan(&active) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var hasPlugin = (err == nil) if plugins[uname].Install != nil { err = plugins[uname].Install() if err != nil { - LocalError(err.Error(), w, r, user) - return + return LocalError(err.Error(), w, r, user) } } if plugins[uname].Activate != nil { err = plugins[uname].Activate() if err != nil { - LocalError(err.Error(), w, r, user) - return + return LocalError(err.Error(), w, r, user) } } if hasPlugin { _, err = updatePluginInstallStmt.Exec(1, uname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = updatePluginStmt.Exec(1, uname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } else { _, err := addPluginStmt.Exec(uname, 1, 1) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } @@ -1110,17 +1029,17 @@ func routePanelPluginsInstall(w http.ResponseWriter, r *http.Request, user User, plugins[uname] = plugin err = plugins[uname].Init() if err != nil { - LocalError(err.Error(), w, r, user) - return + return LocalError(err.Error(), w, r, user) } http.Redirect(w, r, "/panel/plugins/", http.StatusSeeOther) + return nil } -func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } page, _ := strconv.Atoi(r.FormValue("page")) @@ -1130,8 +1049,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) { var userList []User rows, err := getUsersOffsetStmt.Query(offset, perPage) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows.Close() @@ -1140,8 +1058,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) { puser := &User{ID: 0} err := rows.Scan(&puser.ID, &puser.Name, &puser.Group, &puser.Active, &puser.IsSuperAdmin, &puser.Avatar) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } puser.initPerms() @@ -1162,58 +1079,53 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) { } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } pageList := paginate(stats.Users, perPage, 5) pi := PanelUserPage{"User Manager", user, headerVars, stats, userList, pageList, page, lastPage} if preRenderHooks["pre_render_panel_users"] != nil { if runPreRenderHook("pre_render_panel_users", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-users.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user User, suid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user User, suid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditUser { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } uid, err := strconv.Atoi(suid) if err != nil { - LocalError("The provided User ID is not a valid number.", w, r, user) - return + return LocalError("The provided User ID is not a valid number.", w, r, user) } targetUser, err := users.Get(uid) if err == ErrNoRows { - LocalError("The user you're trying to edit doesn't exist.", w, r, user) - return + return LocalError("The user you're trying to edit doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if targetUser.IsAdmin && !user.IsAdmin { - LocalError("Only administrators can edit the account of an administrator.", w, r, user) - return + return LocalError("Only administrators can edit the account of an administrator.", w, r, user) } + // ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using? groups, err := gstore.GetRange(1, 0) // ? - 0 = Go to the end if err != nil { - InternalError(err, w) - return // ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using? + return InternalError(err, w, r) } var groupList []interface{} @@ -1230,99 +1142,84 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user User, suid pi := PanelPage{"User Editor", user, headerVars, stats, groupList, targetUser} if preRenderHooks["pre_render_panel_edit_user"] != nil { if runPreRenderHook("pre_render_panel_edit_user", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-user-edit.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User, suid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User, suid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditUser { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } uid, err := strconv.Atoi(suid) if err != nil { - LocalError("The provided User ID is not a valid number.", w, r, user) - return + return LocalError("The provided User ID is not a valid number.", w, r, user) } targetUser, err := users.Get(uid) if err == ErrNoRows { - LocalError("The user you're trying to edit doesn't exist.", w, r, user) - return + return LocalError("The user you're trying to edit doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if targetUser.IsAdmin && !user.IsAdmin { - LocalError("Only administrators can edit the account of an administrator.", w, r, user) - return + return LocalError("Only administrators can edit the account of other administrators.", w, r, user) } newname := html.EscapeString(r.PostFormValue("user-name")) if newname == "" { - LocalError("You didn't put in a username.", w, r, user) - return + return LocalError("You didn't put in a username.", w, r, user) } newemail := html.EscapeString(r.PostFormValue("user-email")) if newemail == "" { - LocalError("You didn't put in an email address.", w, r, user) - return + return LocalError("You didn't put in an email address.", w, r, user) } if (newemail != targetUser.Email) && !user.Perms.EditUserEmail { - LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user) - return + return LocalError("You need the EditUserEmail permission to edit the email address of a user.", w, r, user) } newpassword := r.PostFormValue("user-password") if newpassword != "" && !user.Perms.EditUserPassword { - LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user) - return + return LocalError("You need the EditUserPassword permission to edit the password of a user.", w, r, user) } newgroup, err := strconv.Atoi(r.PostFormValue("user-group")) if err != nil { - LocalError("The provided GroupID is not a valid number.", w, r, user) - return + return LocalError("You need to provide a whole number for the group ID", w, r, user) } group, err := gstore.Get(newgroup) if err == ErrNoRows { - LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) - return + return LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if !user.Perms.EditUserGroupAdmin && group.IsAdmin { - LocalError("You need the EditUserGroupAdmin permission to assign someone to an administrator group.", w, r, user) - return + return LocalError("You need the EditUserGroupAdmin permission to assign someone to an administrator group.", w, r, user) } if !user.Perms.EditUserGroupSuperMod && group.IsMod { - LocalError("You need the EditUserGroupAdmin permission to assign someone to a super mod group.", w, r, user) - return + return LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user) } _, err = updateUserStmt.Exec(newname, newemail, newgroup, targetUser.ID) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if newpassword != "" { @@ -1334,12 +1231,13 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User ucache.CacheRemove(targetUser.ID) } http.Redirect(w, r, "/panel/users/edit/"+strconv.Itoa(targetUser.ID), http.StatusSeeOther) + return nil } -func routePanelGroups(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroups(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } page, _ := strconv.Atoi(r.FormValue("page")) @@ -1389,49 +1287,44 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user User) { pi := PanelGroupPage{"Group Manager", user, headerVars, stats, groupList, pageList, page, lastPage} if preRenderHooks["pre_render_panel_groups"] != nil { if runPreRenderHook("pre_render_panel_groups", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "panel-groups.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user User, sgid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user User, sgid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditGroup { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } gid, err := strconv.Atoi(sgid) if err != nil { - LocalError("The Group ID is not a valid integer.", w, r, user) - return + return LocalError("You need to provide a whole number for the group ID", w, r, user) } group, err := gstore.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if group.IsAdmin && !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) } if group.IsMod && !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) } var rank string @@ -1453,48 +1346,43 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user User, sgi pi := PanelEditGroupPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, group.Tag, rank, disableRank} if preRenderHooks["pre_render_panel_edit_group"] != nil { if runPreRenderHook("pre_render_panel_edit_group", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-group-edit.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User, sgid string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User, sgid string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditGroup { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } gid, err := strconv.Atoi(sgid) if err != nil { - LocalError("The Group ID is not a valid integer.", w, r, user) - return + return LocalError("The Group ID is not a valid integer.", w, r, user) } group, err := gstore.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if group.IsAdmin && !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) } if group.IsMod && !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) } // TODO: Load the phrases in bulk for efficiency? @@ -1536,58 +1424,51 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User pi := PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms} if preRenderHooks["pre_render_panel_edit_group_perms"] != nil { if runPreRenderHook("pre_render_panel_edit_group_perms", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-group-edit-perms.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user User, sgid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user User, sgid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditGroup { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } gid, err := strconv.Atoi(sgid) if err != nil { - LocalError("The Group ID is not a valid integer.", w, r, user) - return + return LocalError("You need to provide a whole number for the group ID", w, r, user) } group, err := gstore.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters") - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if group.IsAdmin && !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) } if group.IsMod && !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) } gname := r.FormValue("group-name") if gname == "" { - LocalError("The group name can't be left blank.", w, r, user) - return + return LocalError("The group name can't be left blank.", w, r, user) } gtag := r.FormValue("group-tag") rank := r.FormValue("group-type") @@ -1609,88 +1490,75 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use defer groupUpdateMutex.Unlock() if rank != originalRank { if !user.Perms.EditGroupGlobalPerms { - LocalError("You need the EditGroupGlobalPerms permission to change the group type.", w, r, user) - return + return LocalError("You need the EditGroupGlobalPerms permission to change the group type.", w, r, user) } switch rank { case "Admin": if !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to designate this group as an admin group.", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to designate this group as an admin group.", w, r, user) } err = group.ChangeRank(true, true, false) case "Mod": if !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to designate this group as a super-mod group.", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to designate this group as a super-mod group.", w, r, user) } err = group.ChangeRank(false, true, false) case "Banned": err = group.ChangeRank(false, false, true) case "Guest": - LocalError("You can't designate a group as a guest group.", w, r, user) - return + return LocalError("You can't designate a group as a guest group.", w, r, user) case "Member": err = group.ChangeRank(false, false, false) default: - LocalError("Invalid group type.", w, r, user) - return + return LocalError("Invalid group type.", w, r, user) } if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } _, err = updateGroupStmt.Exec(gname, gtag, gid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } group.Name = gname group.Tag = gtag http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther) + return nil } -func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user User, sgid string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, user User, sgid string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditGroup { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } gid, err := strconv.Atoi(sgid) if err != nil { - LocalError("The Group ID is not a valid integer.", w, r, user) - return + return LocalError("The Group ID is not a valid integer.", w, r, user) } group, err := gstore.Get(gid) if err == ErrNoRows { //log.Print("aaaaa monsters o.o") - NotFound(w, r) - return + return NotFound(w, r) } else if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } if group.IsAdmin && !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) } if group.IsMod && !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to edit a super-mod group.", w, r, user) } ////var lpmap map[string]bool = make(map[string]bool) @@ -1714,43 +1582,36 @@ func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use pjson, err := json.Marshal(pmap) if err != nil { - LocalError("Unable to marshal the data", w, r, user) - return + return LocalError("Unable to marshal the data", w, r, user) } - _, err = updateGroupPermsStmt.Exec(pjson, gid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } - err = rebuildGroupPermissions(gid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/panel/groups/edit/perms/"+strconv.Itoa(gid), http.StatusSeeOther) + return nil } -func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.EditGroup { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } groupName := r.PostFormValue("group-name") if groupName == "" { - LocalError("You need a name for this group!", w, r, user) - return + return LocalError("You need a name for this group!", w, r, user) } groupTag := r.PostFormValue("group-tag") @@ -1759,15 +1620,13 @@ func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user U groupType := r.PostFormValue("group-type") if groupType == "Admin" { if !user.Perms.EditGroupAdmin { - LocalError("You need the EditGroupAdmin permission to create admin groups", w, r, user) - return + return LocalError("You need the EditGroupAdmin permission to create admin groups", w, r, user) } isAdmin = true isMod = true } else if groupType == "Mod" { if !user.Perms.EditGroupSuperMod { - LocalError("You need the EditGroupSuperMod permission to create admin groups", w, r, user) - return + return LocalError("You need the EditGroupSuperMod permission to create admin groups", w, r, user) } isMod = true } else if groupType == "Banned" { @@ -1777,20 +1636,19 @@ func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user U gid, err := gstore.Create(groupName, groupTag, isAdmin, isMod, isBanned) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther) + return nil } -func routePanelThemes(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelThemes(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageThemes { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } var pThemeList, vThemeList []Theme @@ -1809,64 +1667,57 @@ func routePanelThemes(w http.ResponseWriter, r *http.Request, user User) { pi := PanelThemesPage{"Theme Manager", user, headerVars, stats, pThemeList, vThemeList} if preRenderHooks["pre_render_panel_themes"] != nil { if runPreRenderHook("pre_render_panel_themes", w, r, &user, &pi) { - return + return nil } } err := templates.ExecuteTemplate(w, "panel-themes.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user User, uname string) { - _, ok := SimplePanelUserCheck(w, r, &user) - if !ok { - return +func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user User, uname string) RouteError { + _, ferr := SimplePanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.Perms.ManageThemes { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if r.FormValue("session") != user.Session { - SecurityError(w, r, user) - return + return SecurityError(w, r, user) } theme, ok := themes[uname] if !ok { - LocalError("The theme isn't registered in the system", w, r, user) - return + return LocalError("The theme isn't registered in the system", w, r, user) } if theme.Disabled { - LocalError("You must not enable this theme", w, r, user) - return + return LocalError("You must not enable this theme", w, r, user) } var isDefault bool - log.Print("uname", uname) + log.Print("uname", uname) // TODO: Do we need to log this? err := isThemeDefaultStmt.QueryRow(uname).Scan(&isDefault) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } hasTheme := err != ErrNoRows if hasTheme { - log.Print("isDefault", isDefault) + log.Print("isDefault", isDefault) // TODO: Do we need to log this? if isDefault { - LocalError("The theme is already active", w, r, user) - return + return LocalError("The theme is already active", w, r, user) } _, err = updateThemeStmt.Exec(1, uname) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } else { _, err := addThemeStmt.Exec(uname, 1) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } } @@ -1875,8 +1726,7 @@ func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user Use defaultTheme := defaultThemeBox.Load().(string) _, err = updateThemeStmt.Exec(0, defaultTheme) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } log.Print("Setting theme '" + theme.Name + "' as the default theme") @@ -1885,8 +1735,7 @@ func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user Use dTheme, ok := themes[defaultTheme] if !ok { - InternalError(errors.New("The default theme is missing"), w) - return + return InternalError(errors.New("The default theme is missing"), w, r) } dTheme.Active = false themes[defaultTheme] = dTheme @@ -1897,16 +1746,16 @@ func routePanelThemesSetDefault(w http.ResponseWriter, r *http.Request, user Use changeDefaultThemeMutex.Unlock() http.Redirect(w, r, "/panel/themes/", http.StatusSeeOther) + return nil } -func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backupURL string) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backupURL string) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.IsSuperAdmin { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } if backupURL != "" { @@ -1917,25 +1766,22 @@ func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backup if ext == ".sql" { info, err := os.Stat("./backups/" + backupURL) if err != nil { - NotFound(w, r) - return + return NotFound(w, r) } // TODO: Change the served filename to gosora_backup_%timestamp%.sql, the time the file was generated, not when it was modified aka what the name of it should be w.Header().Set("Content-Disposition", "attachment; filename=gosora_backup.sql") w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) // TODO: Fix the problem where non-existent files aren't greeted with custom 404s on ServeFile()'s side http.ServeFile(w, r, "./backups/"+backupURL) - return + return nil } - NotFound(w, r) - return + return NotFound(w, r) } var backupList []backupItem backupFiles, err := ioutil.ReadDir("./backups") if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } for _, backupFile := range backupFiles { var ext = filepath.Ext(backupFile.Name()) @@ -1948,21 +1794,21 @@ func routePanelBackups(w http.ResponseWriter, r *http.Request, user User, backup pi := PanelBackupPage{"Backups", user, headerVars, stats, backupList} err = templates.ExecuteTemplate(w, "panel-backups.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } var logCount int err := modlogCountStmt.QueryRow().Scan(&logCount) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } page, _ := strconv.Atoi(r.FormValue("page")) @@ -1971,8 +1817,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { rows, err := getModlogsOffsetStmt.Query(offset, perPage) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } defer rows.Close() @@ -1982,8 +1827,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { for rows.Next() { err := rows.Scan(&action, &elementID, &elementType, &ipaddress, &actorID, &doneAt) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } actor, err := users.Get(actorID) @@ -2051,31 +1895,30 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } pageList := paginate(logCount, perPage, 5) pi := PanelLogsPage{"Moderation Logs", user, headerVars, stats, logs, pageList, page, lastPage} if preRenderHooks["pre_render_panel_mod_log"] != nil { if runPreRenderHook("pre_render_panel_mod_log", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "panel-modlogs.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func routePanelDebug(w http.ResponseWriter, r *http.Request, user User) { - headerVars, stats, ok := PanelUserCheck(w, r, &user) - if !ok { - return +func routePanelDebug(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, stats, ferr := PanelUserCheck(w, r, &user) + if ferr != nil { + return ferr } if !user.IsAdmin { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } uptime := "..." @@ -2086,6 +1929,7 @@ func routePanelDebug(w http.ResponseWriter, r *http.Request, user User) { pi := PanelDebugPage{"Debug", user, headerVars, stats, uptime, openConnCount, dbAdapter} err := templates.ExecuteTemplate(w, "panel-debug.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } diff --git a/plugin_skeleton.go b/plugin_skeleton.go index fc7c8dc6..d1094628 100644 --- a/plugin_skeleton.go +++ b/plugin_skeleton.go @@ -22,7 +22,7 @@ func init() { The Deactivate field is for the handler which is called by the software when the admin hits the Deactivate button in the control panel. You should clean-up any resources you have allocated, remove any hooks, close any statements, etc. within this handler. - The Installation field is for one-off installation logic such as creating tables. You will need to run the seperate uninstallation function for that. + The Installation field is for one-off installation logic such as creating tables. You will need to run the separate uninstallation function for that. That Uninstallation field which is currently unused is for not only deactivating this plugin, but for purging any data associated with it such a new tables or data produced by the end-user. */ diff --git a/plugin_socialgroups.go b/plugin_socialgroups.go index c0315ddb..d2f0b31d 100644 --- a/plugin_socialgroups.go +++ b/plugin_socialgroups.go @@ -281,17 +281,16 @@ func socialgroupsGroupWidgets(headerVars *HeaderVars, sgItem *SocialGroup) (succ Custom Pages */ -func socialgroupsGroupList(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func socialgroupsGroupList(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } socialgroupsCommonAreaWidgets(headerVars) rows, err := socialgroupsListStmt.Query() if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var sgList []*SocialGroup @@ -299,24 +298,23 @@ func socialgroupsGroupList(w http.ResponseWriter, r *http.Request, user User) { sgItem := &SocialGroup{ID: 0} err := rows.Scan(&sgItem.ID, &sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Joinable, &sgItem.Owner, &sgItem.MemberCount, &sgItem.CreatedAt, &sgItem.LastUpdateTime) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } sgItem.Link = socialgroupsBuildGroupURL(nameToSlug(sgItem.Name), sgItem.ID) sgList = append(sgList, sgItem) } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } rows.Close() pi := SocialGroupListPage{"Group List", user, headerVars, sgList} err = templates.ExecuteTemplate(w, "socialgroups_group_list.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } func socialgroupsGetGroup(sgid int) (sgItem *SocialGroup, err error) { @@ -325,7 +323,7 @@ func socialgroupsGetGroup(sgid int) (sgItem *SocialGroup, err error) { return sgItem, err } -func socialgroupsViewGroup(w http.ResponseWriter, r *http.Request, user User) { +func socialgroupsViewGroup(w http.ResponseWriter, r *http.Request, user User) RouteError { // SEO URLs... halves := strings.Split(r.URL.Path[len("/group/"):], ".") if len(halves) < 2 { @@ -333,48 +331,45 @@ func socialgroupsViewGroup(w http.ResponseWriter, r *http.Request, user User) { } sgid, err := strconv.Atoi(halves[1]) if err != nil { - PreError("Not a valid group ID", w, r) - return + return PreError("Not a valid group ID", w, r) } sgItem, err := socialgroupsGetGroup(sgid) if err != nil { - LocalError("Bad group", w, r, user) - return + return LocalError("Bad group", w, r, user) } if !sgItem.Active { - NotFound(w, r) + return NotFound(w, r) } // Re-route the request to routeForums var ctx = context.WithValue(r.Context(), "socialgroups_current_group", sgItem) - routeForum(w, r.WithContext(ctx), user, strconv.Itoa(sgItem.MainForumID)) + return routeForum(w, r.WithContext(ctx), user, strconv.Itoa(sgItem.MainForumID)) } -func socialgroupsCreateGroup(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func socialgroupsCreateGroup(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } // TODO: Add an approval queue mode for group creation if !user.Loggedin || !user.PluginPerms["CreateSocialGroup"] { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } socialgroupsCommonAreaWidgets(headerVars) pi := Page{"Create Group", user, headerVars, tList, nil} err := templates.ExecuteTemplate(w, "socialgroups_create_group.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } -func socialgroupsCreateGroupSubmit(w http.ResponseWriter, r *http.Request, user User) { +func socialgroupsCreateGroupSubmit(w http.ResponseWriter, r *http.Request, user User) RouteError { // TODO: Add an approval queue mode for group creation if !user.Loggedin || !user.PluginPerms["CreateSocialGroup"] { - NoPermissions(w, r, user) - return + return NoPermissions(w, r, user) } var groupActive = true @@ -397,41 +392,37 @@ func socialgroupsCreateGroupSubmit(w http.ResponseWriter, r *http.Request, user // Create the backing forum fid, err := fstore.Create(groupName, "", true, "") if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } res, err := socialgroupsCreateGroupStmt.Exec(groupName, groupDesc, groupActive, groupPrivacy, user.ID, fid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } lastID, err := res.LastInsertId() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } // Add the main backing forum to the forum list err = socialgroupsAttachForum(int(lastID), fid) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } _, err = socialgroupsAddMemberStmt.Exec(lastID, user.ID, 2) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } http.Redirect(w, r, socialgroupsBuildGroupURL(nameToSlug(groupName), int(lastID)), http.StatusSeeOther) + return nil } -func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { - headerVars, ok := UserCheck(w, r, &user) - if !ok { - return +func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) RouteError { + headerVars, ferr := UserCheck(w, r, &user) + if ferr != nil { + return ferr } // SEO URLs... @@ -441,16 +432,14 @@ func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { } sgid, err := strconv.Atoi(halves[1]) if err != nil { - PreError("Not a valid group ID", w, r) - return + return PreError("Not a valid group ID", w, r) } var sgItem = &SocialGroup{ID: sgid} var mainForum int // Unused err = socialgroupsGetGroupStmt.QueryRow(sgid).Scan(&sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Joinable, &sgItem.Owner, &sgItem.MemberCount, &mainForum, &sgItem.Backdrop, &sgItem.CreatedAt, &sgItem.LastUpdateTime) if err != nil { - LocalError("Bad group", w, r, user) - return + return LocalError("Bad group", w, r, user) } sgItem.Link = socialgroupsBuildGroupURL(nameToSlug(sgItem.Name), sgItem.ID) @@ -458,8 +447,7 @@ func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { rows, err := socialgroupsMemberListJoinStmt.Query(sgid) if err != nil && err != ErrNoRows { - InternalError(err, w) - return + return InternalError(err, w, r) } var sgMembers []SocialGroupMember @@ -467,8 +455,7 @@ func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { sgMember := SocialGroupMember{PostCount: 0} err := rows.Scan(&sgMember.User.ID, &sgMember.Rank, &sgMember.PostCount, &sgMember.JoinedAt, &sgMember.User.Name, &sgMember.User.Avatar) if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } sgMember.Link = buildProfileURL(nameToSlug(sgMember.User.Name), sgMember.User.ID) if sgMember.User.Avatar != "" { @@ -495,8 +482,7 @@ func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { } err = rows.Err() if err != nil { - InternalError(err, w) - return + return InternalError(err, w, r) } rows.Close() @@ -504,13 +490,14 @@ func socialgroupsMemberList(w http.ResponseWriter, r *http.Request, user User) { // A plugin with plugins. Pluginception! if preRenderHooks["pre_render_socialgroups_member_list"] != nil { if runPreRenderHook("pre_render_socialgroups_member_list", w, r, &user, &pi) { - return + return nil } } err = templates.ExecuteTemplate(w, "socialgroups_member_list.html", pi) if err != nil { - InternalError(err, w) + return InternalError(err, w, r) } + return nil } func socialgroupsAttachForum(sgid int, fid int) error { @@ -587,7 +574,7 @@ func socialgroupsForumCheck(args ...interface{}) (skip interface{}) { if !ok { sgItem, err = socialgroupsGetGroup(forum.ParentID) if err != nil { - InternalError(errors.New("Unable to find the parent group for a forum"), w) + InternalError(errors.New("Unable to find the parent group for a forum"), w, r) *success = false return false } @@ -614,7 +601,7 @@ func socialgroupsForumCheck(args ...interface{}) (skip interface{}) { err = socialgroupsGetMemberStmt.QueryRow(sgItem.ID, user.ID).Scan(&rank, &posts, &joinedAt) if err != nil && err != ErrNoRows { *success = false - InternalError(err, w) + InternalError(err, w, r) return false } else if err != nil { return true diff --git a/public/EQCSS.js b/public/EQCSS.js new file mode 100644 index 00000000..4430df84 --- /dev/null +++ b/public/EQCSS.js @@ -0,0 +1,651 @@ +/* + +# EQCSS +## version 1.7.0 + +A JavaScript plugin to read EQCSS syntax to provide: +scoped styles, element queries, container queries, +meta-selectors, eval(), and element-based units. + +- github.com/eqcss/eqcss +- elementqueries.com + +Authors: Tommy Hodgins, Maxime Euzière, Azareal + +License: MIT + +*/ + +// Uses Node, AMD or browser globals to create a module +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD: Register as an anonymous module + define([], factory); + } else if (typeof module === 'object' && module.exports) { + // Node: Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node + module.exports = factory(); + } else { + // Browser globals (root is window) + root.EQCSS = factory(); + } +}(this, function() { + var EQCSS = { + data: [] + } + + /* + * EQCSS.load() + * Called automatically on page load. + * Call it manually after adding EQCSS code in the page. + * Loads and parses all the EQCSS code. + */ + EQCSS.load = function() { + // Retrieve all style blocks + var styles = document.getElementsByTagName('style'); + + for (var i = 0; i < styles.length; i++) { + // Test if the style is not read yet + if (styles[i].getAttribute('data-eqcss-read') === null) { + + // Mark the style block as read + styles[i].setAttribute('data-eqcss-read', 'true'); + + EQCSS.process(styles[i].innerHTML); + } + } + + // Retrieve all link tags + var link = document.getElementsByTagName('link'); + + for (i = 0; i < link.length; i++) { + // Test if the link is not read yet, and has rel=stylesheet + if (link[i].getAttribute('data-eqcss-read') === null && link[i].rel === 'stylesheet') { + // retrieve the file content with AJAX and process it + if (link[i].href) { + (function() { + var xhr = new XMLHttpRequest; + xhr.open('GET', link[i].href, true); + xhr.send(null); + xhr.onreadystatechange = function() { + EQCSS.process(xhr.responseText); + } + })(); + } + // Mark the link as read + link[i].setAttribute('data-eqcss-read', 'true'); + } + } + } + + /* + * EQCSS.parse() + * Called by load for each script / style / link resource. + * Generates data for each Element Query found + */ + EQCSS.parse = function(code) { + var parsed_queries = new Array(); + + // Cleanup + code = code.replace(/\s+/g, ' '); // reduce spaces and line breaks + code = code.replace(/\/\*[\w\W]*?\*\//g, ''); // remove comments + code = code.replace(/@element/g, '\n@element'); // one element query per line + code = code.replace(/(@element.*?\{([^}]*?\{[^}]*?\}[^}]*?)*\}).*/g, '$1'); // Keep the queries only (discard regular css written around them) + + // Parse + + // For each query + code.replace(/(@element.*(?!@element))/g, function(string, query) { + // Create a data entry + var dataEntry = {}; + + // Extract the selector + query.replace(/(@element)\s*(".*?"|'.*?'|.*?)\s*(and\s*\(|{)/g, function(string, atrule, selector, extra) { + // Strip outer quotes if present + selector = selector.replace(/^\s?['](.*)[']/, '$1'); + selector = selector.replace(/^\s?["](.*)["]/, '$1'); + + dataEntry.selector = selector; + }) + + // Extract the conditions (measure, value, unit) + dataEntry.conditions = []; + query.replace(/and ?\( ?([^:]*) ?: ?([^)]*) ?\)/g, function(string, measure, value) { + // Separate value and unit if it's possible + var unit = null; + unit = value.replace(/^(\d*\.?\d+)(\D+)$/, '$2'); + + if (unit === value) { + unit = null; + } + value = value.replace(/^(\d*\.?\d+)\D+$/, '$1'); + dataEntry.conditions.push({measure: measure, value: value, unit: unit}); + }); + + // Extract the styles + query.replace(/{(.*)}/g, function(string, style) { + dataEntry.style = style; + }); + + parsed_queries.push(dataEntry); + }); + + return parsed_queries; + } + + /* + * EQCSS.register() + * Add a single object, or an array of objects to EQCSS.data + * + */ + EQCSS.register = function(queries) { + if (Object.prototype.toString.call(queries) === '[object Object]') { + EQCSS.data.push(queries); + EQCSS.apply(); + } + + if (Object.prototype.toString.call(queries) === '[object Array]') { + for (var i=0; i if it doesn't exist) + css_block = document.querySelector('#' + element_guid); + + if (!css_block) { + css_block = document.createElement('style'); + css_block.id = element_guid; + css_block.setAttribute('data-eqcss-read', 'true'); + document.querySelector('head').appendChild(css_block); + } + css_block = document.querySelector('#' + element_guid); + + // Reset the query test's result (first, we assume that the selector is matched) + test = true; + + // Loop on the conditions + test_conditions: for (var k = 0; k < EQCSS.data[i].conditions.length; k++) { + // Reuse element and parent's computed style instead of computing it everywhere + computed_style = window.getComputedStyle(elements[j], null); + + parent_computed_style = null; + + if (elements[j] != document.documentElement) { + parent_computed_style = window.getComputedStyle(elements[j].parentNode, null); + } + + // Do we have to reconvert the size in px at each call? + // This is true only for vw/vh/vmin/vmax + var recomputed = false; + + // If the condition's unit is vw, convert current value in vw, in px + if (EQCSS.data[i].conditions[k].unit === 'vw') { + recomputed = true; + + var value = parseInt(EQCSS.data[i].conditions[k].value); + EQCSS.data[i].conditions[k].recomputed_value = value * window.innerWidth / 100; + } + + // If the condition's unit is vh, convert current value in vh, in px + else if (EQCSS.data[i].conditions[k].unit === 'vh') { + recomputed = true; + + var value = parseInt(EQCSS.data[i].conditions[k].value); + EQCSS.data[i].conditions[k].recomputed_value = value * window.innerHeight / 100; + } + + // If the condition's unit is vmin, convert current value in vmin, in px + else if (EQCSS.data[i].conditions[k].unit === 'vmin') { + recomputed = true; + + var value = parseInt(EQCSS.data[i].conditions[k].value); + EQCSS.data[i].conditions[k].recomputed_value = value * Math.min(window.innerWidth, window.innerHeight) / 100; + } + + // If the condition's unit is vmax, convert current value in vmax, in px + else if (EQCSS.data[i].conditions[k].unit === 'vmax') { + recomputed = true; + + var value = parseInt(EQCSS.data[i].conditions[k].value); + EQCSS.data[i].conditions[k].recomputed_value = value * Math.max(window.innerWidth, window.innerHeight) / 100; + } + + // If the condition's unit is set and is not px or %, convert it into pixels + else if (EQCSS.data[i].conditions[k].unit != null && EQCSS.data[i].conditions[k].unit != 'px' && EQCSS.data[i].conditions[k].unit != '%') { + // Create a hidden DIV, sibling of the current element (or its child, if the element is ) + // Set the given measure and unit to the DIV's width + // Measure the DIV's width in px + // Remove the DIV + var div = document.createElement('div'); + + div.style.visibility = 'hidden'; + div.style.border = '1px solid red'; + div.style.width = EQCSS.data[i].conditions[k].value + EQCSS.data[i].conditions[k].unit; + + var position = elements[j]; + if (elements[j] != document.documentElement) { + position = elements[j].parentNode; + } + + position.appendChild(div); + EQCSS.data[i].conditions[k].value = parseInt(window.getComputedStyle(div, null).getPropertyValue('width')); + EQCSS.data[i].conditions[k].unit = 'px'; + position.removeChild(div); + } + + // Store the good value in final_value depending if the size is recomputed or not + var final_value = recomputed ? EQCSS.data[i].conditions[k].recomputed_value : parseInt(EQCSS.data[i].conditions[k].value); + + // Check each condition for this query and this element + // If at least one condition is false, the element selector is not matched + switch (EQCSS.data[i].conditions[k].measure) { + case 'min-width': + // Min-width in px + if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { + element_width = parseInt(computed_style.getPropertyValue('width')); + if (!(element_width >= final_value)) { + test = false; + break test_conditions; + } + } + + // Min-width in % + if (EQCSS.data[i].conditions[k].unit === '%') { + element_width = parseInt(computed_style.getPropertyValue('width')); + parent_width = parseInt(parent_computed_style.getPropertyValue('width')); + if (!(parent_width / element_width <= 100 / final_value)) { + test = false; + break test_conditions; + } + } + + break; + + case 'max-width': + // Max-width in px + if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { + element_width = parseInt(computed_style.getPropertyValue('width')); + if (!(element_width <= final_value)) { + test = false; + break test_conditions; + } + } + + // Max-width in % + if (EQCSS.data[i].conditions[k].unit === '%') { + element_width = parseInt(computed_style.getPropertyValue('width')); + parent_width = parseInt(parent_computed_style.getPropertyValue('width')); + if (!(parent_width / element_width >= 100 / final_value)) { + test = false; + break test_conditions; + } + } + break; + + case 'min-height': + // Min-height in px + if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { + element_height = parseInt(computed_style.getPropertyValue('height')); + if (!(element_height >= final_value)) { + test = false; + break test_conditions; + } + } + + // Min-height in % + if (EQCSS.data[i].conditions[k].unit === '%') { + element_height = parseInt(computed_style.getPropertyValue('height')); + parent_height = parseInt(parent_computed_style.getPropertyValue('height')); + if (!(parent_height / element_height <= 100 / final_value)) { + test = false; + break test_conditions; + } + } + break; + + case 'max-height': + // Max-height in px + if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { + element_height = parseInt(computed_style.getPropertyValue('height')); + if (!(element_height <= final_value)) { + test = false; + break test_conditions; + } + } + + // Max-height in % + if (EQCSS.data[i].conditions[k].unit === '%') { + element_height = parseInt(computed_style.getPropertyValue('height')); + parent_height = parseInt(parent_computed_style.getPropertyValue('height')); + if (!(parent_height / element_height >= 100 / final_value)) { + test = false; + break test_conditions; + } + } + break; + + // Min-characters + case 'min-characters': + // form inputs + if (elements[j].value) { + if (!(elements[j].value.length >= final_value)) { + test = false; + break test_conditions; + } + } + // blocks + else { + if (!(elements[j].textContent.length >= final_value)) { + test = false; + break test_conditions; + } + } + break; + + // Max-characters + case 'max-characters': + // form inputs + if (elements[j].value) { + if (!(elements[j].value.length <= final_value)) { + test = false; + break test_conditions; + } + } + // blocks + else { + if (!(elements[j].textContent.length <= final_value)) { + test = false; + break test_conditions; + } + } + break; + + // Min-children + case 'min-children': + if (!(elements[j].children.length >= final_value)) { + test = false; + break test_conditions; + } + break; + + // Max-children + case 'max-children': + if (!(elements[j].children.length <= final_value)) { + test = false; + break test_conditions; + } + break; + + // Min-lines + case 'min-lines': + element_height = + parseInt(computed_style.getPropertyValue('height')) + - parseInt(computed_style.getPropertyValue('border-top-width')) + - parseInt(computed_style.getPropertyValue('border-bottom-width')) + - parseInt(computed_style.getPropertyValue('padding-top')) + - parseInt(computed_style.getPropertyValue('padding-bottom')); + + element_line_height = computed_style.getPropertyValue('line-height'); + if (element_line_height === 'normal') { + var element_font_size = parseInt(computed_style.getPropertyValue('font-size')); + element_line_height = element_font_size * 1.125; + } else { + element_line_height = parseInt(element_line_height); + } + + if (!(element_height / element_line_height >= final_value)) { + test = false; + break test_conditions; + } + break; + + // Max-lines + case 'max-lines': + element_height = + parseInt(computed_style.getPropertyValue('height')) + - parseInt(computed_style.getPropertyValue('border-top-width')) + - parseInt(computed_style.getPropertyValue('border-bottom-width')) + - parseInt(computed_style.getPropertyValue('padding-top')) + - parseInt(computed_style.getPropertyValue('padding-bottom')); + + element_line_height = computed_style.getPropertyValue('line-height'); + if (element_line_height === 'normal') { + var element_font_size = parseInt(computed_style.getPropertyValue('font-size')); + element_line_height = element_font_size * 1.125; + } else { + element_line_height = parseInt(element_line_height); + } + + if (!(element_height / element_line_height + 1 <= final_value)) { + test = false; + break test_conditions; + } + break; + } + } + + // Update CSS block: + // If all conditions are met: copy the CSS code from the query to the corresponding CSS block + if (test === true) { + // Get the CSS code to apply to the element + css_code = EQCSS.data[i].style; + + // Replace eval('xyz') with the result of try{with(element){eval(xyz)}} in JS + css_code = css_code.replace( + /eval\( *((".*?")|('.*?')) *\)/g, + function(string, match) { + return EQCSS.tryWithEval(elements[j], match); + } + ); + + // Replace '$this' or 'eq_this' with '[element_guid]' + css_code = css_code.replace(/(\$|eq_)this/gi, '[' + element_guid + ']'); + + // Replace '$parent' or 'eq_parent' with '[element_guid_parent]' + css_code = css_code.replace(/(\$|eq_)parent/gi, '[' + element_guid_parent + ']'); + + if(css_block.innerHTML != css_code){ + css_block.innerHTML = css_code; + } + } + + // If condition is not met: empty the CSS block + else if(css_block.innerHTML != '') { + css_block.innerHTML = ''; + } + } + } + } + + /* + * Eval('') and $it + * (…yes with() was necessary, and eval() too!) + */ + EQCSS.tryWithEval = function(element, string) { + var $it = element; + var ret = ''; + + try { + with ($it) { ret = eval(string.slice(1, -1)) } + } + catch(e) { + ret = ''; + } + return ret; + } + + /* + * EQCSS.reset + * Deletes parsed queries removes EQCSS-generated tags and attributes + * To reload EQCSS again after running EQCSS.reset() use EQCSS.load() + */ + EQCSS.reset = function() { + // Reset EQCSS.data, removing previously parsed queries + EQCSS.data = []; + + // Remove EQCSS-generated style tags from head + var style_tag = document.querySelectorAll('head style[id^="data-eqcss-"]'); + for (var i = 0; i < style_tag.length; i++) { + style_tag[i].parentNode.removeChild(style_tag[i]); + } + + // Remove EQCSS-generated attributes from all tags + var tag = document.querySelectorAll('*'); + + // For each tag in the document + for (var j = 0; j < tag.length; j++) { + // Loop through all attributes + for (var k = 0; k < tag[j].attributes.length; k++) { + // If an attribute begins with 'data-eqcss-' + if (tag[j].attributes[k].name.indexOf('data-eqcss-') === 0) { + // Remove the attribute from the tag + tag[j].removeAttribute(tag[j].attributes[k].name) + } + } + } + } + + /* + * 'DOM Ready' cross-browser polyfill / Diego Perini / MIT license + * Forked from: https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js + */ + EQCSS.domReady = function(fn) { + var done = false; + var top = true; + var doc = window.document; + var root = doc.documentElement; + var modern = !~navigator.userAgent.indexOf('MSIE 8'); + var add = modern ? 'addEventListener' : 'attachEvent'; + var rem = modern ? 'removeEventListener' : 'detachEvent'; + var pre = modern ? '' : 'on'; + var init = function(e) { + if (e.type === 'readystatechange' && doc.readyState !== 'complete') return; + (e.type === 'load' ? window : doc)[rem](pre + e.type, init, false); + if (!done && (done = true)) fn.call(window, e.type || e); + }, + poll = function() { + try { + root.doScroll('left'); + } + catch(e) { + setTimeout(poll, 50); + return; + } + init('poll'); + }; + + if (doc.readyState === 'complete') { + fn.call(window, 'lazy'); + return; + } + + if (!modern && root.doScroll) { + try { + top = !window.frameElement; + } + catch(e) {} + if (top) poll(); + } + doc[add](pre + 'DOMContentLoaded', init, false); + doc[add](pre + 'readystatechange', init, false); + window[add](pre + 'load', init, false); + } + + /* + * EQCSS.throttle + * Ensures EQCSS.apply() is not called more than once every (EQCSS_timeout)ms + */ + var EQCSS_throttle_available = true; + var EQCSS_throttle_queued = false; + var EQCSS_mouse_down = false; + var EQCSS_timeout = 200; + + EQCSS.throttle = function() { + /* if (EQCSS_throttle_available) {*/ + EQCSS.apply(); + /*EQCSS_throttle_available = false; + + setTimeout(function() { + EQCSS_throttle_available = true; + if (EQCSS_throttle_queued) { + EQCSS_throttle_queued = false; + EQCSS.apply(); + } + }, EQCSS_timeout); + } else { + EQCSS_throttle_queued = true; + }*/ + } + + // Call load (and apply, indirectly) on page load + EQCSS.domReady(function() { + EQCSS.load(); + EQCSS.throttle(); + }); + + // On resize, click, call EQCSS.throttle. + window.addEventListener('resize', EQCSS.throttle); + window.addEventListener('click', EQCSS.throttle); + + // Debug: here's a shortcut for console.log + function l(a) { console.log(a) } + + return EQCSS; +})); \ No newline at end of file diff --git a/public/global.js b/public/global.js index 81dfc38b..289759f9 100644 --- a/public/global.js +++ b/public/global.js @@ -3,24 +3,38 @@ var form_vars = {}; var alertList = []; var alertCount = 0; var conn; +var selectedTopics = []; +var attachItemCallback = function(){} -function post_link(event) -{ - event.preventDefault(); - var form_action = $(event.target).closest('a').attr("href"); - //console.log("Form Action: " + form_action); - $.ajax({ url: form_action, type: "POST", dataType: "json", data: {js: "1"} }); +// TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts +function ajaxError(xhr,status,errstr) { + console.log("The AJAX request failed"); + console.log("xhr", xhr); + console.log("status", status); + console.log("errstr", errstr); + if(status=="parsererror") { + console.log("The server didn't respond with a valid JSON response"); + } + console.trace(); } -function bind_to_alerts() { +function postLink(event) +{ + event.preventDefault(); + let formAction = $(event.target).closest('a').attr("href"); + //console.log("Form Action: " + formAction); + $.ajax({ url: formAction, type: "POST", dataType: "json", error: ajaxError, data: {js: "1"} }); +} + +function bindToAlerts() { $(".alertItem.withAvatar a").click(function(event) { event.stopPropagation(); - $.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", data: { asid: $(this).attr("data-asid") } }); + $.ajax({ url: "/api/?action=set&module=dismiss-alert", type: "POST", dataType: "json", error: ajaxError, data: { asid: $(this).attr("data-asid") } }); }); } // TODO: Add the ability for users to dismiss alerts -function load_alerts(menu_alerts) +function loadAlerts(menu_alerts) { var alertListNode = menu_alerts.getElementsByClassName("alertList")[0]; var alertCounterNode = menu_alerts.getElementsByClassName("alert_counter")[0]; @@ -69,7 +83,7 @@ function load_alerts(menu_alerts) } alertCount = data.msgCount; - bind_to_alerts(); + bindToAlerts(); }, error: function(magic,theStatus,error) { var errtxt @@ -124,7 +138,7 @@ $(document).ready(function(){ console.log("The WebSockets connection was closed"); } conn.onmessage = function(event) { - //console.log("WS_Message: ",event.data); + //console.log("WS_Message:", event.data); if(event.data[0] == "{") { try { var data = JSON.parse(event.data); @@ -165,7 +179,7 @@ $(document).ready(function(){ setTimeout(n.close.bind(n), 8000); } - bind_to_alerts(); + bindToAlerts(); } } @@ -216,6 +230,7 @@ $(document).ready(function(){ url: formAction, type: "POST", dataType: "json", + error: ajaxError, data: { topic_name: topicNameInput, topic_status: topicStatusInput, @@ -227,7 +242,7 @@ $(document).ready(function(){ $(".delete_item").click(function(event) { - post_link(event); + postLink(event); $(this).closest('.deletable_block').remove(); }); @@ -248,7 +263,7 @@ $(document).ready(function(){ var formAction = $(this).closest('a').attr("href"); //console.log("Form Action:",formAction); - $.ajax({ url: formAction, type: "POST", dataType: "json", data: { isJs: "1", edit_item: newContent } + $.ajax({ url: formAction, type: "POST", error: ajaxError, dataType: "json", data: { isJs: "1", edit_item: newContent } }); }); }); @@ -273,6 +288,7 @@ $(document).ready(function(){ url: formAction + "?session=" + session, type: "POST", dataType: "json", + error: ajaxError, data: { isJs: "1", edit_item: newContent } }); }); @@ -283,37 +299,37 @@ $(document).ready(function(){ event.preventDefault(); if($(this).find("input").length !== 0) return; //console.log("clicked .edit_fields"); - var block_parent = $(this).closest('.editable_parent'); - //console.log(block_parent); - block_parent.find('.hide_on_edit').hide(); - block_parent.find('.show_on_edit').show(); - block_parent.find('.editable_block').show(); - block_parent.find('.editable_block').each(function(){ - var field_name = this.getAttribute("data-field"); - var field_type = this.getAttribute("data-type"); - if(field_type=="list") + var blockParent = $(this).closest('.editable_parent'); + //console.log(blockParent); + blockParent.find('.hide_on_edit').hide(); + blockParent.find('.show_on_edit').show(); + blockParent.find('.editable_block').show(); + blockParent.find('.editable_block').each(function(){ + var fieldName = this.getAttribute("data-field"); + var fieldType = this.getAttribute("data-type"); + if(fieldType=="list") { - var field_value = this.getAttribute("data-value"); - if(field_name in form_vars) var it = form_vars[field_name]; + var fieldValue = this.getAttribute("data-value"); + if(fieldName in form_vars) var it = form_vars[fieldName]; else var it = ['No','Yes']; var itLen = it.length; var out = ""; - //console.log("Field Name:",field_name); - //console.log("Field Type:",field_type); - //console.log("Field Value:",field_value); + //console.log("Field Name:",fieldName); + //console.log("Field Type:",fieldType); + //console.log("Field Value:",fieldValue); for (var i = 0; i < itLen; i++) { var sel = ""; - if(field_value == i || field_value == it[i]) { + if(fieldValue == i || fieldValue == it[i]) { sel = "selected "; - this.classList.remove(field_name + '_' + it[i]); + this.classList.remove(fieldName + '_' + it[i]); this.innerHTML = ""; } out += ""; } - this.innerHTML = ""; + this.innerHTML = ""; } - else if(field_type=="hidden") {} - else this.innerHTML = ""; + else if(fieldType=="hidden") {} + else this.innerHTML = ""; }); // Remove any handlers already attached to the submitter @@ -323,31 +339,31 @@ $(document).ready(function(){ { event.preventDefault(); //console.log("running .submit_edit event"); - var out_data = {isJs: "1"} - var block_parent = $(this).closest('.editable_parent'); - block_parent.find('.editable_block').each(function() { - var field_name = this.getAttribute("data-field"); - var field_type = this.getAttribute("data-type"); - if(field_type=="list") { + var outData = {isJs: "1"} + var blockParent = $(this).closest('.editable_parent'); + blockParent.find('.editable_block').each(function() { + var fieldName = this.getAttribute("data-field"); + var fieldType = this.getAttribute("data-type"); + if(fieldType=="list") { var newContent = $(this).find('select :selected').text(); - this.classList.add(field_name + '_' + newContent); + this.classList.add(fieldName + '_' + newContent); this.innerHTML = ""; - } else if(field_type=="hidden") { + } else if(fieldType=="hidden") { var newContent = $(this).val(); } else { var newContent = $(this).find('input').eq(0).val(); this.innerHTML = newContent; } this.setAttribute("data-value",newContent); - out_data[field_name] = newContent; + outData[fieldName] = newContent; }); - var form_action = $(this).closest('a').attr("href"); - //console.log("Form Action:", form_action); - //console.log(out_data); - $.ajax({ url: form_action + "?session=" + session, type:"POST", dataType:"json", data: out_data }); - block_parent.find('.hide_on_edit').show(); - block_parent.find('.show_on_edit').hide(); + var formAction = $(this).closest('a').attr("href"); + //console.log("Form Action:", formAction); + //console.log(outData); + $.ajax({ url: formAction + "?session=" + session, type:"POST", dataType:"json", data: outData, error: ajaxError }); + blockParent.find('.hide_on_edit').show(); + blockParent.find('.show_on_edit').hide(); }); }); @@ -357,7 +373,7 @@ $(document).ready(function(){ var ip = this.textContent; if(ip.length > 10){ this.innerHTML = "Show IP"; - this.onclick = function(event){ + this.onclick = function(event) { event.preventDefault(); this.textContent = ip; }; @@ -369,23 +385,23 @@ $(document).ready(function(){ $("#back").removeClass("alertActive"); }); $(".alert_bell").click(function(){ - var menu_alerts = $(this).parent(); - if(menu_alerts.hasClass("selectedAlert")) { + var menuAlerts = $(this).parent(); + if(menuAlerts.hasClass("selectedAlert")) { event.stopPropagation(); - menu_alerts.removeClass("selectedAlert"); + menuAlerts.removeClass("selectedAlert"); $("#back").removeClass("alertActive"); } }); - var alert_menu_list = document.getElementsByClassName("menu_alerts"); - for(var i = 0; i < alert_menu_list.length; i++) { - load_alerts(alert_menu_list[i]); + var alertMenuList = document.getElementsByClassName("menu_alerts"); + for(var i = 0; i < alertMenuList.length; i++) { + loadAlerts(alertMenuList[i]); } $(".menu_alerts").click(function(event) { event.stopPropagation(); if($(this).hasClass("selectedAlert")) return; - if(!conn) load_alerts(this); + if(!conn) loadAlerts(this); this.className += " selectedAlert"; document.getElementById("back").className += " alertActive" }); @@ -405,7 +421,6 @@ $(document).ready(function(){ function uploadFileHandler() { var fileList = this.files; - // Truncate the number of files to 5 let files = []; for(var i = 0; i < fileList.length && i < 5; i++) @@ -441,11 +456,16 @@ $(document).ready(function(){ }).then(function(hash) { console.log("hash",hash); let content = document.getElementById("input_content") - console.log("content.value",content.value); + console.log("content.value", content.value); - if(content.value == "") content.value = content.value + "//" + siteURL + "/attachs/" + hash + "." + ext; - else content.value = content.value + "\r\n//" + siteURL + "/attachs/" + hash + "." + ext; - console.log("content.value",content.value); + let attachItem; + if(content.value == "") attachItem = "//" + siteURL + "/attachs/" + hash + "." + ext; + else attachItem = "\r\n//" + siteURL + "/attachs/" + hash + "." + ext; + content.value = content.value + attachItem; + console.log("content.value", content.value); + + // For custom / third party text editors + attachItemCallback(attachItem); }); } reader.readAsArrayBuffer(files[i]); @@ -458,6 +478,43 @@ $(document).ready(function(){ if(uploadFiles != null) { uploadFiles.addEventListener("change", uploadFileHandler, false); } + + $(".moderate_link").click(function(event) { + event.preventDefault(); + $(".pre_opt").removeClass("auto_hide"); + $(".topic_row").each(function(){ + $(this).click(function(){ + selectedTopics.push(parseInt($(this).attr("data-tid"),10)); + if(selectedTopics.length==1) { + $(".mod_floater_head span").html("What do you want to do with this topic?"); + } else { + $(".mod_floater_head span").html("What do you want to do with these "+selectedTopics.length+" topics?"); + } + $(this).addClass("topic_selected"); + $(".mod_floater").removeClass("auto_hide"); + }); + }); + $(".mod_floater_submit").click(function(event){ + event.preventDefault(); + let selectNode = this.form.querySelector(".mod_floater_options"); + let optionNode = selectNode.options[selectNode.selectedIndex]; + let action = optionNode.getAttribute("val"); + //console.log("action",action); + + let url = "/topic/"+action+"/submit/"; + //console.log("JSON.stringify(selectedTopics) ", JSON.stringify(selectedTopics)); + $.ajax({ + url: url, + type: "POST", + data: JSON.stringify(selectedTopics), + contentType: "application/json", + error: ajaxError, + success: function() { + window.location.reload(); + } + }); + }); + }); $("#themeSelectorSelect").change(function(){ console.log("Changing the theme to " + this.options[this.selectedIndex].getAttribute("val")); @@ -466,22 +523,13 @@ $(document).ready(function(){ type: "POST", dataType: "json", data: { "newTheme": this.options[this.selectedIndex].getAttribute("val"), isJs: "1" }, + error: ajaxError, success: function (data, status, xhr) { console.log("Theme successfully switched"); - console.log("data",data); - console.log("status",status); - console.log("xhr",xhr); + console.log("data", data); + console.log("status", status); + console.log("xhr", xhr); window.location.reload(); - }, - // TODO: Use a standard error handler for the AJAX calls in here which throws up the response (if JSON) in a .notice? Might be difficult to trace errors in the console, if we reuse the same function every-time - error: function(xhr,status,errstr) { - console.log("The AJAX request failed"); - console.log("xhr",xhr); - console.log("status",status); - console.log("errstr",errstr); - if(status=="parsererror") { - console.log("The server didn't respond with a valid JSON response"); - } } }); }); diff --git a/public/jquery-1.12.3.min.js b/public/jquery-1.12.3.min.js deleted file mode 100644 index dad4f0af..00000000 --- a/public/jquery-1.12.3.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.12.3 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; -}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("