diff --git a/modules/web/nodes/admin_panel/SCsub b/modules/web/nodes/admin_panel/SCsub new file mode 100644 index 000000000..ec2551b2a --- /dev/null +++ b/modules/web/nodes/admin_panel/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("admin_panel", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/admin_panel/admin_node.cpp b/modules/web/nodes/admin_panel/admin_node.cpp new file mode 100644 index 000000000..9961fa9c3 --- /dev/null +++ b/modules/web/nodes/admin_panel/admin_node.cpp @@ -0,0 +1,32 @@ +#include "admin_node.h" + +#include "web/html/form_validator.h" +#include "web/html/html_builder.h" +#include "web/http/cookie.h" +#include "web/http/http_session.h" +#include "web/http/request.h" +#include "web/http/session_manager.h" + +void AdminNode::admin_handle_request_main(Request *request) { + +} + +String AdminNode::admin_get_section_name() { + return ""; +} + +void AdminNode::admin_add_section_links(Vector *links) { + +} + +bool AdminNode::admin_full_render() { + return false; +} + +AdminNode::AdminNode() : + WebNode() { + +} + +AdminNode::~AdminNode() { +} diff --git a/modules/web/nodes/admin_panel/admin_node.h b/modules/web/nodes/admin_panel/admin_node.h new file mode 100644 index 000000000..5d0903fab --- /dev/null +++ b/modules/web/nodes/admin_panel/admin_node.h @@ -0,0 +1,40 @@ +#ifndef ADMIN_NODE_H +#define ADMIN_NODE_H + +#include "web/http/web_node.h" + +#include "core/containers/vector.h" +#include "core/string.h" + +class Request; +class FormValidator; + +struct AdminSectionLinkInfo { + String name; + String link; + + AdminSectionLinkInfo() { + } + + AdminSectionLinkInfo(const String &p_name, const String &p_link) { + name = p_name; + link = p_link; + } +}; + +class AdminNode : public WebNode { + RCPP_OBJECT(AdminNode, WebNode); + +public: + virtual void admin_handle_request_main(Request *request); + virtual String admin_get_section_name(); + virtual void admin_add_section_links(Vector *links); + virtual bool admin_full_render(); + + AdminNode(); + ~AdminNode(); + +protected: +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/admin_panel/admin_panel.cpp b/modules/web/nodes/admin_panel/admin_panel.cpp new file mode 100644 index 000000000..621bd2208 --- /dev/null +++ b/modules/web/nodes/admin_panel/admin_panel.cpp @@ -0,0 +1,171 @@ +#include "admin_panel.h" + +#include "web/html/form_validator.h" +#include "web/html/html_builder.h" +#include "web/http/cookie.h" +#include "web/http/http_session.h" +#include "web/http/request.h" +#include "web/http/session_manager.h" +#include "web/http/web_permission.h" + +#include "admin_node.h" + +void AdminPanel::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + String seg = request->get_current_path_segment(); + + if (seg == "") { + render_admin_panel_list(request); + return; + } + + for (int i = 0; i < _controllers.size(); ++i) { + AdminPanelSection &s = _controllers[i]; + + if (s.section_url == seg) { + AdminNode *c = s.controller; + + request->push_path(); + + if (c->admin_full_render()) { + c->admin_handle_request_main(request); + return; + } + + // render_menu(request); + render_headers(request); + render_segment_body_top(request); + render_controller_panel(request, c); + render_footer(request); + + // request->pop_path(); + + return; + } + } + + request->send_error(404); +} + +void AdminPanel::render_admin_panel_list(Request *request) { + render_headers(request); + render_main_body_top(request); + + String rootlink = request->get_url_root(); + + HTMLBuilder b; + + Vector links; + + b.div()->cls("content"); + + for (int i = 0; i < _controllers.size(); ++i) { + b.div()->cls("section"); + + AdminPanelSection &s = _controllers[i]; + + b.div()->cls("section_header"); + b.w(s.controller->admin_get_section_name()); + b.cdiv(); + + s.controller->admin_add_section_links(&links); + + for (int j = 0; j < links.size(); ++j) { + AdminSectionLinkInfo &li = links[j]; + + b.div()->cls("section_entry"); + b.a()->href(rootlink + s.section_url + "/" + li.link); + b.w(li.name); + b.ca(); + b.cdiv(); + } + + links.clear(); + + b.cdiv(); + } + + b.cdiv(); + + render_footer(request); + + request->body += b.result; + request->compile_and_send_body(); +} + +void AdminPanel::render_controller_panel(Request *request, AdminNode *controller) { + // set up headers + controller->admin_handle_request_main(request); + // set up footers + request->compile_and_send_body(); +} + +void AdminPanel::register_admin_controller(const String §ion, AdminNode *controller) { + AdminPanelSection sec; + + sec.section_url = section; + sec.controller = controller; + + _controllers.push_back(sec); +} + +void AdminPanel::clear() { + _controllers.clear(); +} + +void AdminPanel::render_headers(Request *request) { + request->head += _default_headers; +} + +void AdminPanel::render_main_body_top(Request *request) { + request->body += _default_main_body_top; +} + +void AdminPanel::render_segment_body_top(Request *request) { + request->body += _default_segment_body_top; +} + +void AdminPanel::render_footer(Request *request) { + request->body += _default_footer; +} + +void AdminPanel::set_default_header(const String &val) { + _default_headers = val; +} +void AdminPanel::set_default_main_body_top(const String &val) { + _default_main_body_top = val; +} +void AdminPanel::set_default_segment_body_top(const String &val) { + _default_segment_body_top = val; +} + +void AdminPanel::set_default_footer(const String &val) { + _default_footer = val; +} + +AdminPanel *AdminPanel::get_singleton() { + return _self; +} + +AdminPanel::AdminPanel() : + WebNode() { + + if (_self) { + printf("AdminPanel::AdminPanel(): Error! self is not null!/n"); + } + + _self = this; +} + +AdminPanel::~AdminPanel() { + if (_self == this) { + _self = nullptr; + } +} + +AdminPanel *AdminPanel::_self = nullptr; diff --git a/modules/web/nodes/admin_panel/admin_panel.h b/modules/web/nodes/admin_panel/admin_panel.h new file mode 100644 index 000000000..eabe18eda --- /dev/null +++ b/modules/web/nodes/admin_panel/admin_panel.h @@ -0,0 +1,57 @@ +#ifndef ADMIN_PANEL_H +#define ADMIN_PANEL_H + +#include "web/http/web_node.h" + +#include "core/string.h" +#include "core/containers/vector.h" + +class Request; +class FormValidator; +class AdminNode; + +class AdminPanel : public WebNode { + RCPP_OBJECT(AdminPanel, WebNode); + +public: + void handle_request_main(Request *request); + + virtual void render_admin_panel_list(Request *request); + virtual void render_controller_panel(Request *request, AdminNode *controller); + + void register_admin_controller(const String §ion, AdminNode *controller); + void clear(); + + virtual void render_headers(Request *request); + virtual void render_main_body_top(Request *request); + virtual void render_segment_body_top(Request *request); + virtual void render_footer(Request *request); + + void set_default_header(const String &val); + void set_default_main_body_top(const String &val); + void set_default_segment_body_top(const String &val); + void set_default_footer(const String &val); + + static AdminPanel *get_singleton(); + + AdminPanel(); + ~AdminPanel(); + +protected: + struct AdminPanelSection { + String section_url; + String name; + AdminNode *controller; + }; + + static AdminPanel *_self; + + Vector _controllers; + + String _default_headers; + String _default_main_body_top; + String _default_segment_body_top; + String _default_footer; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/admin_panel/detect.py b/modules/web/nodes/admin_panel/detect.py new file mode 100644 index 000000000..148d2c69a --- /dev/null +++ b/modules/web/nodes/admin_panel/detect.py @@ -0,0 +1,26 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "users" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + +def configure(env): + pass diff --git a/modules/web/nodes/folder_serve_nodes/SCsub b/modules/web/nodes/folder_serve_nodes/SCsub new file mode 100644 index 000000000..e184b7a39 --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("paged_list", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.cpp b/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.cpp new file mode 100644 index 000000000..24e7d4752 --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.cpp @@ -0,0 +1,139 @@ +#include "browsable_folder_serve_node.h" + +#include "core/os/directory.h" +#include "web/file_cache.h" +#include "web/html/html_builder.h" +#include "web/http/request.h" + +void BrowsableFolderServeNode::_handle_request_main(Request *request) { + String file_name = request->get_path(true, false); + + String *s = _folder_indexes[file_name]; + + if (!s) { + request->send_error(HTTP_STATUS_CODE_404_NOT_FOUND); + return; + } + + if (should_render_menu) { + render_menu(request); + } + + request->body += (*s); + request->compile_and_send_body(); +} + +void BrowsableFolderServeNode::render_index(Request *request) { + String *s = _folder_indexes["/"]; + + if (!s) { + return; + } + + request->body += (*s); +} +void BrowsableFolderServeNode::render_preview(Request *request) { +} + +void BrowsableFolderServeNode::load() { + if (serve_folder == "") { + return; + } + + FolderServeNode::load(); + + evaluate_dir(serve_folder, true); +} + +void BrowsableFolderServeNode::evaluate_dir(const String &path, const bool top_level) { + Ref dir; + dir.instance(); + + ERR_FAIL_COND_MSG(dir->open_dir(path) != OK, "Error opening folde!r: " + String(path)); + + String dir_uri; + + if (!top_level) { + dir_uri = path.substr(serve_folder.size(), path.size() - serve_folder.size()); + } else { + dir_uri = "/"; + } + + Vector folders; + Vector files; + + while (dir->next()) { + String np = dir->current_get_path(); + np = np.substr(serve_folder.size(), np.size() - serve_folder.size()); + + if (dir->current_is_file()) { + files.push_back(np); + + } else { + folders.push_back(np); + evaluate_dir(dir->current_get_path()); + } + } + + dir->close_dir(); + + folders.sort_inc(); + files.sort_inc(); + + render_dir_page(dir_uri, folders, files, top_level); +} + +void BrowsableFolderServeNode::render_dir_page(const String &dir_uri, const Vector &folders, const Vector &files, const bool top_level) { + HTMLBuilder b; + + String uri = get_full_uri(false); + + b.div("file_list"); + { + if (!top_level) { + b.div("file_list_entry"); + { + b.a(uri + dir_uri.path_get_prev_dir())->w("..")->ca(); + } + b.cdiv(); + } + + for (int i = 0; i < folders.size(); ++i) { + b.div("file_list_entry"); + { + b.a(uri + folders[i])->w("(Folder) ")->w(folders[i].path_get_basename())->ca(); + } + b.cdiv(); + } + + for (int i = 0; i < files.size(); ++i) { + b.div("file_list_entry"); + { + b.a(uri + files[i])->w("(File) ")->w(files[i].path_get_basename())->ca(); + } + b.cdiv(); + } + } + b.cdiv(); + + String *s = new String(); + s->append_str(b.result); + + _folder_indexes[dir_uri] = s; +} + +BrowsableFolderServeNode::BrowsableFolderServeNode() : + FolderServeNode() { + + should_render_menu = true; +} + +BrowsableFolderServeNode::~BrowsableFolderServeNode() { + for (std::map::iterator E = _folder_indexes.begin(); E != _folder_indexes.end(); E++) { + if (E->second) { + delete E->second; + } + } + + _folder_indexes.clear(); +} diff --git a/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.h b/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.h new file mode 100644 index 000000000..f7115664d --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/browsable_folder_serve_node.h @@ -0,0 +1,44 @@ +#ifndef BROWSABLE_FOLDER_SERVE_NODE_H +#define BROWSABLE_FOLDER_SERVE_NODE_H + +#include "core/string.h" +#include + +#include "folder_serve_node.h" + +// On top of serving the files from the folder set to it's serve_folder property, +// this class also generates HTML directory lists. (Similar to apache's directory listing) +// It caches folder contents on ENTER_TREE, it does not watch for folder changes yet. + +// if (should_render_menu) -> render_menu(request) +//
+//
..
+//
(Folder) test_folder
+//
(File) test_file.md
+// ... +//
+ +class BrowsableFolderServeNode : public FolderServeNode { + RCPP_OBJECT(BrowsableFolderServeNode, FolderServeNode); + +public: + void _handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + virtual void load(); + + void evaluate_dir(const String &path, const bool top_level = false); + virtual void render_dir_page(const String &dir_uri, const Vector &folders, const Vector &files, const bool top_level); + + bool should_render_menu; + + BrowsableFolderServeNode(); + ~BrowsableFolderServeNode(); + +protected: + std::map _folder_indexes; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/folder_serve_nodes/detect.py b/modules/web/nodes/folder_serve_nodes/detect.py new file mode 100644 index 000000000..313219245 --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "paged_list" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/folder_serve_nodes/folder_serve_node.cpp b/modules/web/nodes/folder_serve_nodes/folder_serve_node.cpp new file mode 100644 index 000000000..7286425ac --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/folder_serve_node.cpp @@ -0,0 +1,67 @@ +#include "folder_serve_node.h" + +#include "web/http/request.h" +#include "web/http/web_permission.h" + +void FolderServeNode::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + const String &rp = request->get_current_path_segment(); + + if (rp == "") { + if (!try_route_request_to_children(request)) { + _handle_request_main(request); + } + return; + } + + String file_name = request->get_path(true, false); + + if (file_cache->wwwroot_has_file(file_name)) { + String fp = file_cache->wwwroot; + fp.append_path(file_name); + + request->send_file(fp); + return; + } + + if (!try_route_request_to_children(request)) { + _handle_request_main(request); + } +} + +void FolderServeNode::load() { + file_cache->clear(); + + if (serve_folder == "") { + return; + } + + serve_folder.path_clean_end_slash(); + + file_cache->wwwroot = serve_folder; + file_cache->wwwroot_refresh_cache(); +} + +void FolderServeNode::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + default: + break; + } +} + +FolderServeNode::FolderServeNode() : + WebNode() { + + file_cache = new FileCache(); +} + +FolderServeNode::~FolderServeNode() { +} diff --git a/modules/web/nodes/folder_serve_nodes/folder_serve_node.h b/modules/web/nodes/folder_serve_nodes/folder_serve_node.h new file mode 100644 index 000000000..16d1e753d --- /dev/null +++ b/modules/web/nodes/folder_serve_nodes/folder_serve_node.h @@ -0,0 +1,33 @@ +#ifndef FOLDER_SERVE_NODE_H +#define FOLDER_SERVE_NODE_H + +#include "core/string.h" + +#include "web/file_cache.h" +#include "web/http/web_node.h" + +// This class will serve the files from the folder set to it's serve_folder property. +// It will cache the folder's contents on ENTER_TREE, and will match against the cached list, +// this means directory walking (for example sending http://webapp.com/files/../../../etc/passwd), +// and other techniques like it should not be possible. + +class FolderServeNode : public WebNode { + RCPP_OBJECT(FolderServeNode, WebNode); + +public: + void handle_request_main(Request *request); + + virtual void load(); + + void _notification(const int what); + + String serve_folder; + + FolderServeNode(); + ~FolderServeNode(); + +protected: + FileCache *file_cache; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/list_page/SCsub b/modules/web/nodes/list_page/SCsub new file mode 100644 index 000000000..510fa70b9 --- /dev/null +++ b/modules/web/nodes/list_page/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("list_page", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/list_page/detect.py b/modules/web/nodes/list_page/detect.py new file mode 100644 index 000000000..89d647ebc --- /dev/null +++ b/modules/web/nodes/list_page/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "list_page" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/list_page/list_page.cpp b/modules/web/nodes/list_page/list_page.cpp new file mode 100644 index 000000000..ec6415a0e --- /dev/null +++ b/modules/web/nodes/list_page/list_page.cpp @@ -0,0 +1,197 @@ +#include "list_page.h" + +#include "core/math/math.h" +#include "web/html/html_builder.h" +#include "web/html/utils.h" +#include "web/http/web_permission.h" + +#include +#include + +void ListPage::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + if (_pages.size() == 0) { + render_menu(request); + request->body += _no_entries_response; + request->compile_and_send_body(); + return; + } + + const String &cs = request->get_current_path_segment(); + + if (cs == "") { + render_menu(request); + render_index(request); + request->compile_and_send_body(); + return; + } + + if (!cs.is_uint()) { + request->send_error(HTTP_STATUS_CODE_404_NOT_FOUND); + return; + } + + int p = cs.to_int(); + + p = ((p == 0) ? (0) : (p - 1)); + + if (p < 0 || p >= _pages.size()) { + request->send_error(HTTP_STATUS_CODE_404_NOT_FOUND); + return; + } + + render_menu(request); + request->body += _pages[p]; + request->compile_and_send_body(); +} + +void ListPage::render_index(Request *request) { + request->body += _pages[0]; +} +void ListPage::render_preview(Request *request) { +} + +void ListPage::load() { + if (folder == "") { + RLOG_ERR("Error: ListPage::load called, but a folder is not set!"); + return; + } + + Vector files; + + tinydir_dir dir; + if (tinydir_open(&dir, folder.c_str()) == -1) { + RLOG_ERR("Error opening ListPage::folder! folder: \n" + folder); + return; + } + + while (dir.has_next) { + tinydir_file file; + if (tinydir_readfile(&dir, &file) == -1) { + tinydir_next(&dir); + continue; + } + + if (!file.is_dir) { + String np = file.path; + + files.push_back(np); + } + + tinydir_next(&dir); + } + + tinydir_close(&dir); + + files.sort_inc(); + + Vector list_entries; + + for (uint32_t i = 0; i < files.size(); ++i) { + FILE *f = fopen(files[i].c_str(), "r"); + + if (!f) { + RLOG_ERR("Settings::parse_file: Error opening file!\n"); + return; + } + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); /* same as rewind(f); */ + + String fd; + fd.resize(fsize); + + fread(&fd[0], 1, fsize, f); + fclose(f); + + Utils::markdown_to_html(&fd); + + list_entries.push_back(fd); + } + + render_entries(list_entries); + render_no_entries_response(); +} + +void ListPage::render_entries(const Vector &list_entries) { + if (list_entries.size() == 0) { + return; + } + + int pages = Math::floorf_int(Math::divf(list_entries.size(), entry_per_page)); + for (int i = 0; i < pages; ++i) { + String r = ""; + + int efrom = i * entry_per_page; + int eto = MIN((i + 1) * entry_per_page, list_entries.size()); + + r = render_page(i, pages, list_entries, efrom, eto); + _pages.push_back(r); + } +} + +String ListPage::render_page(const int page_index, const int page_count, const Vector &list_entries, const int efrom, const int eto) { + HTMLBuilder b; + + b.div(main_div_class); + + for (int i = efrom; i < eto; ++i) { + b.w(render_entry(list_entries[i])); + } + + b.w(Utils::get_pagination(get_full_uri(), page_count, page_index, max_visible_navigation_links)); + b.cdiv(); + + return b.result; +} + +String ListPage::render_entry(const String &list_entry) { + HTMLBuilder b; + + b.div(main_div_class); + b.div(empty_div_class)->w(list_entry)->cdiv(); + b.cdiv(); + + return b.result; +} + +void ListPage::render_no_entries_response() { + HTMLBuilder b; + + b.div(empty_div_class)->w(placeholder_text)->cdiv(); + + _no_entries_response = b.result; +} + +void ListPage::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + case NOTIFICATION_EXIT_TREE: + _pages.clear(); + break; + default: + break; + } +} + +ListPage::ListPage() : + WebNode() { + + max_visible_navigation_links = 6; + entry_per_page = 4; + main_div_class = "list_page"; + entry_div_class = "list_entry"; + empty_div_class = "list_entry_empty"; + placeholder_text = "No content yet!"; +} + +ListPage::~ListPage() { +} \ No newline at end of file diff --git a/modules/web/nodes/list_page/list_page.h b/modules/web/nodes/list_page/list_page.h new file mode 100644 index 000000000..1a5e7110b --- /dev/null +++ b/modules/web/nodes/list_page/list_page.h @@ -0,0 +1,66 @@ +#ifndef LIST_PAGE_H +#define LIST_PAGE_H + +#include "core/containers/vector.h" +#include "core/string.h" + +#include "web/http/web_node.h" + +#include "web/http/request.h" + +// This class will load and generate pages from a folder of md files. It supports pagination, +// it will put entry_per_page md files per page. It generates html on enter tree, and caches everything. +// Each md file gets rendered into a div with a class of "list_entry" + +// HTML (If there are entries): +// render_menu() +//
// Set the class via the main_div_class property +//
md file 1
// Set the class via the entry_div_class property +//
md file 2
+// ... +// +//
+ +// HTML (If there are no entries): +// render_menu() +//
// Set the class via the main_div_class property +//
No content yet!
// Set the class via the empty_div_class property, text via placeholder_text property +//
+ +class ListPage : public WebNode { + RCPP_OBJECT(ListPage, WebNode); + +public: + void handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + void load(); + + virtual void render_entries(const Vector &list_entries); + virtual String render_page(const int page_index, const int page_count, const Vector &list_entries, const int efrom, const int eto); + virtual String render_entry(const String &list_entry); + virtual void render_no_entries_response(); + + void _notification(const int what); + + ListPage(); + ~ListPage(); + + bool paginate; + int max_visible_navigation_links; + int entry_per_page; + String folder; + + String main_div_class; + String entry_div_class; + String empty_div_class; + String placeholder_text; + +protected: + Vector _pages; + String _no_entries_response; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/message_page/SCsub b/modules/web/nodes/message_page/SCsub new file mode 100644 index 000000000..70f622325 --- /dev/null +++ b/modules/web/nodes/message_page/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("message_page", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/message_page/detect.py b/modules/web/nodes/message_page/detect.py new file mode 100644 index 000000000..37b584bbe --- /dev/null +++ b/modules/web/nodes/message_page/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "message_page" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/message_page/message_page.cpp b/modules/web/nodes/message_page/message_page.cpp new file mode 100644 index 000000000..0c7ff825f --- /dev/null +++ b/modules/web/nodes/message_page/message_page.cpp @@ -0,0 +1,85 @@ +#include "message_page.h" + +#include "database/database.h" + +#include "database/query_builder.h" +#include "database/table_builder.h" +#include "database/query_result.h" +#include "web/http/web_permission.h" + +void MessagePage::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + Ref b = db->get_query_builder(); + + b->select("text")->from("message_page")->end_command(); + + Ref res = db->query(b->query_result); + + Vector msgs; + + while (res->next_row()) { + msgs.push_back(res->get_cell(0)); + } + + String r = ""; + + for (uint32_t i = 0; i < messages.size(); ++i) { + r += "

" + messages[i] + "


"; + } + + for (uint32_t i = 0; i < msgs.size(); ++i) { + r += "

" + msgs[i] + "


"; + } + + r += ""; + + request->body += r; + + request->compile_and_send_body(); +} + +void MessagePage::_migrate(const bool clear, const bool seed_db) { + Ref t = db->get_table_builder(); + + t->drop_table("message_page"); + db->query_run(t->result); + + printf("%s\n", t->result.c_str()); + + t->result.clear(); + + t->create_table("message_page")->integer("id")->auto_increment()->primary_key()->next_row()->varchar("text", 30)->ccreate_table(); + + printf("%s\n", t->result.c_str()); + + db->query(t->result); + + Ref b = db->get_query_builder(); + + b->insert("message_page")->values("default, 'aaewdwd'"); + + printf("%s\n", b->query_result.c_str()); + + db->query_run(b->query_result); + + b->query_result.clear(); + b->insert("message_page")->values("default, 'qqqqq'"); + + printf("%s\n", b->query_result.c_str()); + + db->query_run(b->query_result); +} + +MessagePage::MessagePage() : + WebNode() { + messages.push_back("T message 1"); + messages.push_back("T message 2"); +} + +MessagePage::~MessagePage() { +} \ No newline at end of file diff --git a/modules/web/nodes/message_page/message_page.h b/modules/web/nodes/message_page/message_page.h new file mode 100644 index 000000000..2cf52713d --- /dev/null +++ b/modules/web/nodes/message_page/message_page.h @@ -0,0 +1,26 @@ +#ifndef MESSAGE_PAGE_H +#define MESSAGE_PAGE_H + +#include "core/containers/vector.h" +#include "core/string.h" + +#include "web/http/web_node.h" + +#include "web/http/request.h" + + +class MessagePage : public WebNode { + RCPP_OBJECT(MessagePage, WebNode); + +public: + void handle_request_main(Request *request); + + void _migrate(const bool clear, const bool seed_db); + + MessagePage(); + ~MessagePage(); + + Vector messages; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/paged_article/SCsub b/modules/web/nodes/paged_article/SCsub new file mode 100644 index 000000000..510fa70b9 --- /dev/null +++ b/modules/web/nodes/paged_article/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("list_page", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/paged_article/detect.py b/modules/web/nodes/paged_article/detect.py new file mode 100644 index 000000000..89d647ebc --- /dev/null +++ b/modules/web/nodes/paged_article/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "list_page" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/paged_article/paged_article.cpp b/modules/web/nodes/paged_article/paged_article.cpp new file mode 100644 index 000000000..22d7cc743 --- /dev/null +++ b/modules/web/nodes/paged_article/paged_article.cpp @@ -0,0 +1,173 @@ +#include "paged_article.h" + +#include "core/os/directory.h" +#include "web/html/utils.h" +#include "web/http/web_permission.h" + +#include + +void PagedArticle::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + const String &rp = request->get_current_path_segment(); + + if (request->get_remaining_segment_count() > 1 && rp == "files") { + String file_name = "/" + request->get_path_segment(request->get_current_segment_index() + 1); + + if (file_cache->wwwroot_has_file(file_name)) { + String fp = file_cache->wwwroot + file_name; + + request->send_file(fp); + return; + } + } + + if (rp == "") { + render_menu(request); + + render_index(request); + + request->compile_and_send_body(); + return; + } + + const String *page = pages[rp]; + + if (page == nullptr) { + // bad url + request->send_error(404); + return; + } + + render_menu(request); + request->body += (*page); + request->compile_and_send_body(); +} + +void PagedArticle::render_index(Request *request) { + // summary page + request->body += index_page; +} + +void PagedArticle::render_preview(Request *request) { +} + +void PagedArticle::load() { + ERR_FAIL_COND_MSG(articles_folder == "", "Error: PagedArticle::load called, but a articles_folder is not set!"); + + Ref dir; + dir.instance(); + + ERR_FAIL_COND_MSG(dir->open_dir(articles_folder.c_str()) != OK, "Error opening PagedArticle::folder! folder: " + articles_folder); + + Vector files; + + while (dir->next()) { + if (dir->current_is_file()) { + files.push_back(dir->current_get_name()); + } + } + + dir->close_dir(); + + if (files.size() == 0) { + return; + } + + files.sort_inc(); + + for (uint32_t i = 0; i < files.size(); ++i) { + String file_path = articles_folder; + file_path.append_path(files[i]); + + String fd; + + ERR_CONTINUE_MSG(dir->read_file_into(file_path, &fd) != OK, "PagedArticle::load_folder: Error opening file! " + file_path); + + Utils::markdown_to_html(&fd); + + if (files[i] == "summary.md") { + summary = fd; + + continue; + } + + String pagination; + + pagination = Utils::get_pagination_links(get_full_uri(), files, i); + + String *finals = new String(); + + (*finals) += pagination; + (*finals) += fd; + (*finals) += pagination; + + pages[files[i]] = finals; + + if (i == 0) { + index_page = (*finals); + } + } + + file_cache->clear(); + + if (serve_folder != "") { + if (serve_folder_relative) { + file_cache->wwwroot = articles_folder; + file_cache->wwwroot.append_path(serve_folder); + } else { + file_cache->wwwroot = serve_folder; + } + + file_cache->wwwroot_refresh_cache(); + } + + if (summary == "") { + generate_summary(); + } +} + +String PagedArticle::get_index_page() { + return index_page; +} + +String PagedArticle::get_summary() { + return summary; +} + +void PagedArticle::generate_summary() { + summary = get_uri_segment(); +} + +void PagedArticle::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + default: + break; + } +} + +PagedArticle::PagedArticle() : + WebNode() { + + file_cache = new FileCache(); + + serve_folder_relative = true; + serve_folder = "files"; +} + +PagedArticle::~PagedArticle() { + for (std::map::iterator it = pages.begin(); it != pages.end(); ++it) { + delete ((*it).second); + } + + pages.clear(); + + delete file_cache; +} \ No newline at end of file diff --git a/modules/web/nodes/paged_article/paged_article.h b/modules/web/nodes/paged_article/paged_article.h new file mode 100644 index 000000000..c9b53581c --- /dev/null +++ b/modules/web/nodes/paged_article/paged_article.h @@ -0,0 +1,52 @@ +#ifndef PAGED_ARTICLE_H +#define PAGED_ARTICLE_H + +#include "core/containers/vector.h" +#include "core/string.h" +#include + +#include "web/file_cache.h" +#include "web/http/web_node.h" + +#include "web/http/request.h" + +// This class will load and process all md files from the folder set to it's articles_folder property, +// and serve every file from the directory set to it's serve_folder property. +// if it finds a summary.md, it will serve it as the root. +// It uses pagination. +// THe links is generates currently look like: /01_test.md +// files are served under /files/ + +class PagedArticle : public WebNode { + RCPP_OBJECT(PagedArticle, WebNode); + +public: + void handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + void load(); + void load_folder(const String &folder, const String &path); + String get_index_page(); + String get_summary(); + + virtual void generate_summary(); + + void _notification(const int what); + + PagedArticle(); + ~PagedArticle(); + + String articles_folder; + bool serve_folder_relative; + String serve_folder; + +protected: + String index_page; + String summary; + std::map pages; + FileCache *file_cache; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/paged_article/paged_articles.cpp b/modules/web/nodes/paged_article/paged_articles.cpp new file mode 100644 index 000000000..d0648e04d --- /dev/null +++ b/modules/web/nodes/paged_article/paged_articles.cpp @@ -0,0 +1,94 @@ +#include "paged_articles.h" + +#include "web/html/html_builder.h" + +#include "core/os/directory.h" +#include "paged_article.h" +#include "web/html/utils.h" + +#include + +void PagedArticles::_handle_request_main(Request *request) { + render_menu(request); + + render_index(request); + + request->compile_and_send_body(); +} + +void PagedArticles::render_index(Request *request) { + // summary page + request->body += index_page; +} +void PagedArticles::render_preview(Request *request) { +} + +void PagedArticles::load() { + ERR_FAIL_COND_MSG(folder == "", "Error: PagedArticles::load called, but a folder is not set!"); + + if (folder.size() > 0 && folder[folder.size() - 1] == '/') { + folder.pop_back(); + } + + Ref dir; + dir.instance(); + + ERR_FAIL_COND_MSG(dir->open_dir(folder) != OK, "Error opening PagedArticles::folder! folder: " + folder); + + while (dir->next()) { + if (dir->current_is_dir()) { + String np = dir->current_get_path(); + String fn = dir->current_get_name(); + + String ff = folder; + ff.append_path(fn); + + PagedArticle *p = new PagedArticle(); + String seg = dir->current_get_name(); + p->articles_folder = ff; + p->set_uri_segment(seg); + add_child(p); + } + } + + generate_index_page(); +} + +void PagedArticles::generate_index_page() { + HTMLBuilder b; + + b.div("article_list"); + + for (int i = 0; i < get_child_count(); ++i) { + PagedArticle *a = Object::cast_to(get_child(i)); + + if (a) { + b.a(a->get_full_uri()); + b.div("article_list_entry")->w(a->get_summary())->cdiv(); + b.ca(); + } + } + + b.cdiv(); + + index_page = b.result; +} + +void PagedArticles::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + default: + break; + } + + WebNode::_notification(what); +} + +PagedArticles::PagedArticles() : + WebNode() { +} + +PagedArticles::~PagedArticles() { +} \ No newline at end of file diff --git a/modules/web/nodes/paged_article/paged_articles.h b/modules/web/nodes/paged_article/paged_articles.h new file mode 100644 index 000000000..32d41c8a3 --- /dev/null +++ b/modules/web/nodes/paged_article/paged_articles.h @@ -0,0 +1,50 @@ +#ifndef PAGED_ARTICLES_H +#define PAGED_ARTICLES_H + +#include "core/containers/vector.h" +#include "core/string.h" + +#include "web/file_cache.h" +#include "web/http/web_node.h" + +#include "web/http/request.h" + +// This class will load and process all md files from the folder set to it's folder property, +// and generate one page from them. TThe files are processed in alphabetical order. + +// The generated HTML looks like: + +// render_menu() +//
+// Contents of the first md file. +//
+//
+// Contents of the second md file. +//
+// ... +// + +class PagedArticles : public WebNode { + RCPP_OBJECT(PagedArticles, WebNode); + +public: + void _handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + void load(); + void generate_index_page(); + + void _notification(const int what); + + PagedArticles(); + ~PagedArticles(); + + String folder; + +protected: + String index_page; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/paged_article/paged_articles_md_index.cpp b/modules/web/nodes/paged_article/paged_articles_md_index.cpp new file mode 100644 index 000000000..65c564a7a --- /dev/null +++ b/modules/web/nodes/paged_article/paged_articles_md_index.cpp @@ -0,0 +1,51 @@ +#include "paged_articles_md_index.h" + +#include "web/html/html_builder.h" +#include "web/http/request.h" +#include "web/http/web_permission.h" + +void PagedArticlesMDIndex::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + const String path = request->get_current_path_segment(); + + if (request->get_remaining_segment_count() == 0) { + main_page->handle_request_main(request); + return; + } + + articles->handle_request_main(request); +} + +void PagedArticlesMDIndex::render_index(Request *request) { + main_page->render_index(request); +} + +void PagedArticlesMDIndex::render_preview(Request *request) { + main_page->render_preview(request); +} + +void PagedArticlesMDIndex::load() { + main_page->folder = folder; + main_page->load(); + + articles->articles_folder = folder; + articles->serve_folder = folder + "/files"; + articles->load(); +} + +PagedArticlesMDIndex::PagedArticlesMDIndex() : + WebNode() { + + main_page = new ListPage(); + articles = new PagedArticle(); +} + +PagedArticlesMDIndex::~PagedArticlesMDIndex() { + delete main_page; + delete articles; +} diff --git a/modules/web/nodes/paged_article/paged_articles_md_index.h b/modules/web/nodes/paged_article/paged_articles_md_index.h new file mode 100644 index 000000000..b95f50df1 --- /dev/null +++ b/modules/web/nodes/paged_article/paged_articles_md_index.h @@ -0,0 +1,35 @@ +#ifndef PAGED_ARTICLES_MD_INDEX_H +#define PAGED_ARTICLES_MD_INDEX_H + +#include "core/string.h" + +#include "web/http/web_node.h" + +#include "web_modules/list_page/list_page.h" +#include "web_modules/paged_article/paged_article.h" + +// Inherit from PagedArticles and override generate_index_page -> load and process md files in the set folder +// SHould probably be called something else. PagedArticlesMDIndex ? + +class PagedArticlesMDIndex : public WebNode { + RCPP_OBJECT(PagedArticlesMDIndex, WebNode); + +public: + void handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + void load(); + + PagedArticlesMDIndex(); + ~PagedArticlesMDIndex(); + + String folder; + String base_path; + + ListPage *main_page; + PagedArticle *articles; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac/SCsub b/modules/web/nodes/rbac/SCsub new file mode 100644 index 000000000..dab438f24 --- /dev/null +++ b/modules/web/nodes/rbac/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("rbac", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/rbac/detect.py b/modules/web/nodes/rbac/detect.py new file mode 100644 index 000000000..1c0370784 --- /dev/null +++ b/modules/web/nodes/rbac/detect.py @@ -0,0 +1,31 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "users" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + +def configure(env): + pass + +def get_module_dependencies(): + return [ + "admin_panel", + ] \ No newline at end of file diff --git a/modules/web/nodes/rbac/rbac_controller.cpp b/modules/web/nodes/rbac/rbac_controller.cpp new file mode 100644 index 000000000..428cbd3f0 --- /dev/null +++ b/modules/web/nodes/rbac/rbac_controller.cpp @@ -0,0 +1,872 @@ +#include "rbac_controller.h" + +#include "core/error_macros.h" + +#include "web/html/form_validator.h" +#include "web/html/html_builder.h" +#include "web/http/cookie.h" +#include "web/http/http_session.h" +#include "web/http/request.h" +#include "web/http/session_manager.h" +#include "rbac_default_permissions.h" +#include "web_modules/users/user.h" + +#include "database/database.h" +#include "database/database_manager.h" +#include "database/query_builder.h" +#include "database/query_result.h" +#include "database/table_builder.h" + +void RBACController::handle_request_main(Request *request) { +} + +void RBACController::create_validators() { +} + +void RBACController::admin_handle_request_main(Request *request) { + String seg = request->get_current_path_segment(); + + if (seg == "") { + admin_render_rank_list(request); + return; + } else if (seg == "new_rank") { + request->push_path(); + + admin_handle_new_rank(request); + } else if (seg == "edit_rank") { + request->push_path(); + + admin_handle_edit_rank(request); + } else if (seg == "permission_editor") { + request->push_path(); + + admin_permission_editor(request); + } +} + +void RBACController::admin_handle_new_rank(Request *request) { + + if (request->get_method() == HTTP_METHOD_POST) { + Ref rank; + rank.instance(); + + rank->name = request->get_parameter("name"); + rank->name_internal = request->get_parameter("name_internal"); + rank->settings = request->get_parameter("settings"); + + int base_permissions = 0; + + for (int i = 0; i < _registered_permissions.size(); ++i) { + String param = request->get_parameter("perm_check_" + String::num(i)); + + if (param != "") { + base_permissions |= _registered_permissions[i].value; + } + } + + rank->base_permissions = base_permissions; + + int rank_permissions = 0; + + for (int i = 0; i < _registered_rank_permissions.size(); ++i) { + String param = request->get_parameter("perm_rank_check_" + String::num(i)); + + if (param != "") { + rank_permissions |= _registered_rank_permissions[i].value; + } + } + + rank->rank_permissions = rank_permissions; + + db_save_rank(rank); + + _ranks[rank->id] = rank; + + request->send_redirect(request->get_url_root_parent() + "edit_rank/" + String::num(rank->id)); + return; + } + + RBACAdminRankViewData data; + render_rank_view(request, &data); +} + +void RBACController::admin_handle_edit_rank(Request *request) { + String seg = request->get_current_path_segment(); + + //check whether it's numeric + //if (!seg.is) + + int id = seg.to_int(); + + if (id == 0) { + RLOG_MSG("RBACController::admin_handle_edit_rank: id == 0!\n"); + request->send_redirect(request->get_url_root_parent()); + return; + } + + Ref rank = _ranks[id]; + + if (!rank.is_valid()) { + RLOG_MSG("RBACController::admin_handle_edit_rank: !rank.is_valid()\n"); + request->send_redirect(request->get_url_root_parent()); + return; + } + + RBACAdminRankViewData data; + data.rank = rank; + + if (request->get_method() == HTTP_METHOD_POST) { + rank->name = request->get_parameter("name"); + rank->name_internal = request->get_parameter("name_internal"); + rank->settings = request->get_parameter("settings"); + + int base_permissions = 0; + + for (int i = 0; i < _registered_permissions.size(); ++i) { + String param = request->get_parameter("perm_check_" + String::num(i)); + + if (param != "") { + base_permissions |= _registered_permissions[i].value; + } + } + + rank->base_permissions = base_permissions; + + int rank_permissions = 0; + + for (int i = 0; i < _registered_rank_permissions.size(); ++i) { + String param = request->get_parameter("perm_rank_check_" + String::num(i)); + + if (param != "") { + rank_permissions |= _registered_rank_permissions[i].value; + } + } + + rank->rank_permissions = rank_permissions; + + db_save_rank(rank); + + data.messages.push_back("Save Success!"); + } + + render_rank_view(request, &data); +} + +void RBACController::render_rank_view(Request *request, RBACAdminRankViewData *data) { + int id = 0; + String name = ""; + String name_internal = ""; + String settings = ""; + int base_permissions = 0; + int rank_permissions = 0; + + if (data->rank.is_valid()) { + id = data->rank->id; + name = data->rank->name; + name_internal = data->rank->name_internal; + settings = data->rank->settings; + base_permissions = data->rank->base_permissions; + rank_permissions = data->rank->rank_permissions; + } + + HTMLBuilder b; + + b.h4()->f()->a()->href(request->get_url_root_parent())->f()->w("<- Back")->ca()->ch4(); + b.h4()->f()->w("RBAC Editor")->ch4(); + + b.div()->cls("messages"); + for (int i = 0; i < data->messages.size(); ++i) { + b.w(data->messages[i])->br(); + } + b.cdiv(); + + b.form()->method("POST")->action(request->get_url_root() + String::num(id)); + { + b.csrf_token(request); + + //b.input()->type("hidden")->name("id")->value(String::num(id))->f()->cinput(); + b.w("Name:")->br(); + b.input()->type("text")->name("name")->value(name)->f()->br(); + b.w("Name (Internal):")->br(); + b.input()->type("text")->name("name_internal")->value(name_internal)->f()->cinput()->br(); + b.w("Custom Settings:")->br(); + b.input()->type("text")->name("settings")->value(settings)->f()->cinput()->br(); + + b.w("Base Permissions:")->br(); + + for (int i = 0; i < _registered_permissions.size(); ++i) { + String checkbox_name = "perm_check_" + String::num(i); + + b.input()->type("checkbox")->name(checkbox_name)->value(checkbox_name)->id(checkbox_name)->checked((base_permissions & _registered_permissions[i].value) != 0); + b.label()->fora(checkbox_name)->f()->w(_registered_permissions[i].name)->clabel(); + } + + b.br(); + + b.w("Rank Permissions:")->br(); + + for (int i = 0; i < _registered_rank_permissions.size(); ++i) { + String checkbox_name = "perm_rank_check_" + String::num(i); + + b.input()->type("checkbox")->name(checkbox_name)->value(checkbox_name)->id(checkbox_name)->checked((rank_permissions & _registered_rank_permissions[i].value) != 0); + b.label()->fora(checkbox_name)->f()->w(_registered_rank_permissions[i].name)->clabel(); + } + + b.br(); + + b.input()->type("submit")->value("Save"); + } + b.cform(); + + request->body += b.result; +} + +void RBACController::admin_permission_editor(Request *request) { + + String seg = request->get_current_path_segment(); + + //check whether it's numeric + //if (!seg.is) + + int id = seg.to_int(); + + if (id == 0) { + RLOG_MSG("RBACController::admin_permission_editor: id == 0!\n"); + request->send_redirect(request->get_url_root_parent()); + return; + } + + Ref rank = _ranks[id]; + + if (!rank.is_valid()) { + RLOG_MSG("RBACController::admin_permission_editor: !rank.is_valid()\n"); + request->send_redirect(request->get_url_root_parent()); + return; + } + + RBACAdminEditPermissionView data; + data.rank = rank; + + request->push_path(); + + String segn = request->get_current_path_segment(); + + if (segn == "") { + admin_render_permission_editor_main_view(request, &data); + return; + } + + if (segn == "new") { + request->push_path(); + + if (request->get_method() == HTTP_METHOD_POST) { + if (admin_process_permission_editor_entry_edit_create_post(request, &data)) { + return; + } + } + + admin_render_permission_editor_entry_edit_create_view(request, &data); + return; + } + + if (segn.is_uint()) { + int perm_index = segn.to_int(); + + request->push_path(); + + if (perm_index < 0 || perm_index >= rank->permissions.size()) { + RLOG_ERR("(perm_index < 0 || perm_index >= rank->permissions.size())!\n"); + request->send_redirect(request->get_url_root_parent()); + return; + } + + data.permission = rank->permissions[perm_index]; + + if (!data.permission.is_valid()) { + RLOG_ERR("(!data.permission.is_valid()\n"); + request->send_error(503); + return; + } + + if (request->get_method() == HTTP_METHOD_POST) { + if (admin_process_permission_editor_entry_edit_create_post(request, &data)) { + return; + } + } + + admin_render_permission_editor_entry_edit_create_view(request, &data); + return; + } + + request->send_error(404); +} + +void RBACController::admin_render_permission_editor_main_view(Request *request, RBACAdminEditPermissionView *data) { + HTMLBuilder b; + + Ref rank = data->rank; + + b.h4()->f()->a()->href(request->get_url_root_parent(2))->f()->w("<- Back")->ca()->ch4(); + b.h4()->f()->w("RBAC Editor")->ch4(); + + b.div()->cls("heading"); + { + b.w("[ Id ]: ")->wn(rank->id)->w(", [ Name ]: ")->w(rank->name)->w(", [ Name Internal ]: ")->w(rank->name_internal); + } + b.cdiv(); + + b.br(); + + for (int i = 0; i < rank->permissions.size(); ++i) { + Ref perm = rank->permissions[i]; + + if (!perm.is_valid()) { + RLOG_ERR("RBACController::admin_render_permission_editor_main_view: !perm.is_valid()\n"); + continue; + } + + b.div()->cls("row"); + { + b.a()->href(request->get_url_root() + String::num(i)); + + b.w("-- Rank: [ Id ]: ")->wn(perm->id)->w(", [ Rank Id ]: ")->wn(perm->rank_id)->w(", [ Name ]: ")->w(perm->name); + b.w(" [ URL ]: ")->w(perm->url)->w(", [ Sort Order ]: ")->wn(perm->sort_order); + b.w(" [ Permissions ]: "); + + int pcount = 0; + int perms = perm->permissions; + for (int i = 0; i < _registered_permissions.size(); ++i) { + if ((_registered_permissions[i].value & perms) != 0) { + if (pcount > 0) { + b.w(", "); + } + + b.w(_registered_permissions[i].name); + + ++pcount; + } + } + + if (pcount == 0) { + b.w("- None -"); + } + + b.ca(); + } + b.cdiv(); + } + + b.br(); + + b.a()->href(request->get_url_root("new")); + b.w("New Permission"); + b.ca(); + + request->body += b.result; +} + +void RBACController::admin_render_permission_editor_entry_edit_create_view(Request *request, RBACAdminEditPermissionView *data) { + HTMLBuilder b; + + Ref rank = data->rank; + Ref perm = data->permission; + + String name; + String url; + int sort_order = 0; + int permissions = 0; + + if (perm.is_valid()) { + name = perm->name; + url = perm->url; + sort_order = perm->sort_order; + permissions = perm->permissions; + } + + b.h4()->f()->a()->href(request->get_url_root_parent())->f()->w("<- Back")->ca()->ch4(); + b.h4()->f()->w("RBAC Editor")->ch4(); + b.br(); + + b.div()->cls("messages"); + for (int i = 0; i < data->messages.size(); ++i) { + b.w(data->messages[i])->br(); + } + b.cdiv(); + b.br(); + + b.div()->cls("heading"); + { + b.w("Rank: [ Id ]: ")->wn(rank->id)->w(", [ Name ]: ")->w(rank->name)->w(", [ Name Internal ]: ")->w(rank->name_internal); + } + b.cdiv(); + b.br(); + + b.form()->method("POST")->action(request->get_url_root()); + { + b.csrf_token(request); + + b.w("Name:")->br(); + b.input()->type("text")->name("name")->value(name)->f()->br(); + b.w("URL:")->br(); + b.input()->type("text")->name("url")->value(url)->f()->cinput()->br(); + + b.w("Permissions:")->br(); + + for (int i = 0; i < _registered_permissions.size(); ++i) { + String checkbox_name = "perm_check_" + String::num(i); + + b.input()->type("checkbox")->name(checkbox_name)->value(checkbox_name)->id(checkbox_name)->checked((permissions & _registered_permissions[i].value) != 0); + b.label()->fora(checkbox_name)->f()->w(_registered_permissions[i].name)->clabel(); + } + + b.br(); + + b.input()->type("submit")->value("Save"); + } + b.cform(); + + request->body += b.result; +} + +bool RBACController::admin_process_permission_editor_entry_edit_create_post(Request *request, RBACAdminEditPermissionView *data) { + Ref rank = data->rank; + + Ref perm = data->permission; + + if (!perm.is_valid()) { + perm.instance(); + + perm->rank_id = rank->id; + + if (rank->permissions.size() > 0) { + Ref p = rank->permissions[rank->permissions.size() - 1]; + + perm->sort_order = p->sort_order + 1; + } + + rank->permissions.push_back(perm); + } + + perm->name = request->get_parameter("name"); + perm->url = request->get_parameter("url"); + + int permissions = 0; + + for (int i = 0; i < _registered_permissions.size(); ++i) { + String param = request->get_parameter("perm_check_" + String::num(i)); + + if (param != "") { + permissions |= _registered_permissions[i].value; + } + } + + perm->permissions = permissions; + + //set this up in the form by default + //perm->sort_order = request->get_parameter("sort_order").to_int(); + + db_save_permission(perm); + + if (perm->id == 0) { + RLOG_ERR("RBACController::admin_process_permission_editor_entry_edit_create_post: perm->id == 0!\n"); + } + + request->send_redirect(request->get_url_root_parent()); + + return true; +} + +void RBACController::admin_render_rank_list(Request *request) { + HTMLBuilder b; + + b.h4()->f()->a()->href(request->get_url_root_parent())->f()->w("<- Back")->ca()->ch4(); + b.h4()->f()->w("RBAC Editor")->ch4(); + + for (std::map >::iterator p = _ranks.begin(); p != _ranks.end(); p++) { + Ref r = p->second; + + if (!r.is_valid()) { + continue; + } + + b.div()->cls("row"); + { + b.a()->href(request->get_url_root("permission_editor/") + String::num(r->id)); + b.w("[ Id ]: ")->wn(r->id)->w(", [ Name ]: ")->w(r->name)->w(", [ Name Internal ]: ")->w(r->name_internal); + b.w(", [ Base Permissions ]: "); + + int pcount = 0; + int perms = r->base_permissions; + for (int i = 0; i < _registered_permissions.size(); ++i) { + if ((_registered_permissions[i].value & perms) != 0) { + if (pcount > 0) { + b.w(", "); + } + + b.w(_registered_permissions[i].name); + + ++pcount; + } + } + + if (pcount == 0) { + b.w("- None -"); + } + + b.w(", [ Rank Permissions ]: "); + + pcount = 0; + perms = r->rank_permissions; + for (int i = 0; i < _registered_rank_permissions.size(); ++i) { + if ((_registered_rank_permissions[i].value & perms) != 0) { + if (pcount > 0) { + b.w(", "); + } + + b.w(_registered_rank_permissions[i].name); + + ++pcount; + } + } + + if (pcount == 0) { + b.w("- None -"); + } + + b.ca(); + + b.w(" - "); + + b.a()->href(request->get_url_root("edit_rank/") + String::num(r->id)); + b.w("[ Edit ]"); + b.ca(); + } + b.cdiv(); + } + + b.br(); + + b.a()->href(request->get_url_root("new_rank")); + b.w("New Rank"); + b.ca(); + + request->body += b.result; +} + +void RBACController::admin_render_rank_editor(Request *request) { +} + +String RBACController::admin_get_section_name() { + return "Role Based Access Control"; +} + +void RBACController::admin_add_section_links(Vector *links) { + links->push_back(AdminSectionLinkInfo("Editor", "")); +} + +void RBACController::register_permission(const String &name, const int val) { + _registered_permissions.push_back(PermissionEntry(name, val)); +} +void RBACController::register_rank_permission(const String &name, const int val) { + _registered_rank_permissions.push_back(PermissionEntry(name, val)); +} +void RBACController::clear_registered_permissions() { + _registered_permissions.clear(); + _registered_rank_permissions.clear(); +} + +void RBACController::initialize() { + _ranks = db_load_ranks(); + _default_rank_id = db_get_default_rank(); + _default_user_rank_id = db_get_default_user_rank(); + + register_permissions(); +} + +void RBACController::register_permissions() { + register_permission("Create", User::PERMISSION_CREATE); + register_permission("Read", User::PERMISSION_READ); + register_permission("Update", User::PERMISSION_UPDATE); + register_permission("Delete", User::PERMISSION_DELETE); + + register_rank_permission("Admin Panel", RBAC_RANK_PERMISSION_ADMIN_PANEL); + register_rank_permission("Use Redirect", RBAC_RANK_PERMISSION_USE_REDIRECT); +} + +Ref RBACController::get_rank(int rank_id) { + return _ranks[rank_id]; +} + +int RBACController::get_default_user_rank_id() { + return _default_user_rank_id; +} +Ref RBACController::get_default_user_rank() { + return _ranks[get_default_user_rank_id()]; +} + +int RBACController::get_default_rank_id() { + return _default_rank_id; +} + +Ref RBACController::get_default_rank() { + return _ranks[get_default_rank_id()]; +} + +String &RBACController::get_redirect_url() { + return _redirect_url; +} + +bool RBACController::continue_on_missing_default_rank() { + //todo, add setting + return false; +} + +//DB + +std::map > RBACController::db_load_ranks() { + std::map > ranks; + + Ref qb = get_query_builder(); + + qb->select("id,name,name_internal,settings,base_permissions,rank_permissions")->from(_rbac_ranks_table); + Ref res = qb->run(); + + while (res->next_row()) { + Ref r; + r.instance(); + + r->id = res->get_cell_int(0); + r->name = res->get_cell_str(1); + r->name_internal = res->get_cell_str(2); + r->settings = res->get_cell_str(3); + r->base_permissions = res->get_cell_int(4); + r->rank_permissions = res->get_cell_int(5); + + ranks[r->id] = r; + } + + qb->reset(); + qb->select("id,rank_id,name,url,sort_order,permissions")->from(_rbac_permissions_table); + res = qb->run(); + + while (res->next_row()) { + Ref p; + p.instance(); + + p->id = res->get_cell_int(0); + p->rank_id = res->get_cell_int(1); + p->name = res->get_cell_str(2); + p->url = res->get_cell_str(3); + p->sort_order = res->get_cell_int(4); + p->permissions = res->get_cell_int(5); + + Ref r = ranks[p->rank_id]; + + if (!r.is_valid()) { + RLOG_ERR("RBACModel::load_permissions: !r.is_valid()!"); + continue; + } + + r->permissions.push_back(p); + } + + for (std::map >::iterator i = ranks.begin(); i != ranks.end(); ++i) { + Ref r = i->second; + + if (r.is_valid()) { + r->sort_permissions(); + } + } + + return ranks; +} + +void RBACController::db_save(const Ref &rank) { + db_save_rank(rank); + + for (int i = 0; i < rank->permissions.size(); ++i) { + Ref permission = rank->permissions[i]; + + int rid = rank->id; + + if (permission->rank_id != rid) { + permission->rank_id = rid; + } + + db_save_permission(permission); + } +} + +void RBACController::db_save_rank(const Ref &rank) { + Ref qb = get_query_builder(); + + if (rank->id == 0) { + qb->insert(_rbac_ranks_table, "name,name_internal,settings,base_permissions,rank_permissions")->values(); + qb->val(rank->name)->val(rank->name_internal)->val(rank->settings)->val(rank->base_permissions)->val(rank->rank_permissions); + qb->cvalues(); + qb->select_last_insert_id(); + Ref res = qb->run(); + //qb->print(); + + Ref r = rank; + + r->id = res->get_last_insert_rowid(); + } else { + qb->update(_rbac_ranks_table)->set(); + qb->setp("name", rank->name); + qb->setp("name_internal", rank->name_internal); + qb->setp("settings", rank->settings); + qb->setp("base_permissions", rank->base_permissions); + qb->setp("rank_permissions", rank->rank_permissions); + qb->cset(); + qb->where()->wp("id", rank->id); + qb->end_command(); + qb->run_query(); + //qb->print(); + } +} + +void RBACController::db_save_permission(const Ref &permission) { + Ref qb = get_query_builder(); + + if (permission->id == 0) { + qb->insert(_rbac_permissions_table, "rank_id,name,url,sort_order,permissions")->values(); + qb->val(permission->rank_id)->val(permission->name)->val(permission->url); + qb->val(permission->sort_order)->val(permission->permissions); + qb->cvalues(); + qb->select_last_insert_id(); + Ref res = qb->run(); + //qb->print(); + + Ref r = permission; + + r->id = res->get_last_insert_rowid(); + } else { + qb->update(_rbac_permissions_table)->set(); + qb->setp("rank_id", permission->rank_id); + qb->setp("name", permission->name); + qb->setp("url", permission->url); + qb->setp("sort_order", permission->sort_order); + qb->setp("permissions", permission->permissions); + qb->cset(); + qb->where()->wp("id", permission->id); + qb->end_command(); + qb->run_query(); + //qb->print(); + } +} + +int RBACController::db_get_default_rank() { + //todo, load this, and save it to a table (probably a new settings class) + return 3; +} + +int RBACController::db_get_default_user_rank() { + //todo, load this, and save it to a table (probably a new settings class) + return 2; +} + +String RBACController::db_get_redirect_url() { + //todo, load this, and save it to a table (probably a new settings class) + return String("/user/login"); +} + +void RBACController::create_table() { + Ref tb = get_table_builder(); + + tb->create_table(_rbac_ranks_table); + tb->integer("id")->auto_increment()->next_row(); + tb->varchar("name", 60)->not_null()->next_row(); + tb->varchar("name_internal", 100)->not_null()->next_row(); + tb->varchar("settings", 200)->not_null()->next_row(); + tb->integer("base_permissions")->not_null()->next_row(); + tb->integer("rank_permissions")->not_null()->next_row(); + tb->primary_key("id"); + tb->ccreate_table(); + //tb->run_query(); + //tb->print(); + + //tb->result = ""; + + tb->create_table(_rbac_permissions_table); + tb->integer("id")->auto_increment()->next_row(); + tb->integer("rank_id")->not_null()->next_row(); + tb->varchar("name", 60)->not_null()->next_row(); + tb->varchar("url", 100)->not_null()->next_row(); + tb->integer("sort_order")->not_null()->next_row(); + tb->integer("permissions")->not_null()->next_row(); + + tb->primary_key("id"); + tb->foreign_key("rank_id")->references(_rbac_ranks_table, "id"); + tb->ccreate_table(); + tb->run_query(); + //tb->print(); +} +void RBACController::drop_table() { + Ref tb = get_table_builder(); + + tb->drop_table_if_exists(_rbac_permissions_table)->drop_table_if_exists(_rbac_ranks_table)->run_query(); + //tb->print(); +} + +void RBACController::create_default_entries() { + Ref admin; + admin.instance(); + + admin->name = "Admin"; + admin->base_permissions = User::PERMISSION_ALL; + admin->rank_permissions = RBAC_RANK_PERMISSION_ADMIN_PANEL; + + db_save_rank(admin); + + Ref user; + user.instance(); + + user->name = "User"; + //user->base_permissions = User::PERMISSION_READ; + //user->rank_permissions = 0; + + //temporary! + user->base_permissions = User::PERMISSION_ALL; + user->rank_permissions = RBAC_RANK_PERMISSION_ADMIN_PANEL; + + db_save_rank(user); + + Ref guest; + guest.instance(); + + guest->name = "Guest"; + guest->base_permissions = User::PERMISSION_READ; + guest->rank_permissions = RBAC_RANK_PERMISSION_USE_REDIRECT; + + db_save_rank(guest); +} + + +RBACController *RBACController::get_singleton() { + return _self; +} + +RBACController::RBACController() : + AdminNode() { + + if (_self) { + printf("RBACController::RBACController(): Error! self is not null!/n"); + } + + _default_rank_id = 0; + _default_user_rank_id = 0; + + _rbac_ranks_table = "rbac_ranks"; + _rbac_permissions_table = "rbac_permissions"; + + _self = this; +} + +RBACController::~RBACController() { + if (_self == this) { + _self = nullptr; + } +} + +RBACController *RBACController::_self = nullptr; diff --git a/modules/web/nodes/rbac/rbac_controller.h b/modules/web/nodes/rbac/rbac_controller.h new file mode 100644 index 000000000..b155cfe08 --- /dev/null +++ b/modules/web/nodes/rbac/rbac_controller.h @@ -0,0 +1,127 @@ +#ifndef RBAC_CONTROLLER_H +#define RBAC_CONTROLLER_H + +#include + +#include "web_modules/admin_panel/admin_node.h" + +#include "core/containers/vector.h" +#include "core/string.h" + +#include "rbac_permission.h" +#include "rbac_rank.h" + +class Request; +class FormValidator; + +class RBACController : public AdminNode { + RCPP_OBJECT(RBACController, AdminNode); + +public: + void handle_request_main(Request *request); + void create_validators(); + + void admin_handle_request_main(Request *request); + String admin_get_section_name(); + void admin_add_section_links(Vector *links); + + struct RBACAdminRankViewData { + Ref rank; + Vector messages; + + int id = 0; + String name = ""; + String name_internal = ""; + String settings = ""; + int rank_permissions = 0; + }; + + void admin_handle_new_rank(Request *request); + void admin_handle_edit_rank(Request *request); + void render_rank_view(Request *request, RBACAdminRankViewData *data); + + struct RBACAdminEditPermissionView { + Ref rank; + Ref permission; + Vector messages; + + int rank_id = 0; + int permission_id = 0; + }; + + void admin_permission_editor(Request *request); + void admin_render_permission_editor_main_view(Request *request, RBACAdminEditPermissionView *data); + void admin_render_permission_editor_entry_edit_create_view(Request *request, RBACAdminEditPermissionView *data); + bool admin_process_permission_editor_entry_edit_create_post(Request *request, RBACAdminEditPermissionView *data); + + void admin_render_rank_list(Request *request); + void admin_render_rank_editor(Request *request); + + void register_permission(const String &name, const int val); + void register_rank_permission(const String &name, const int val); + void clear_registered_permissions(); + + void initialize(); + virtual void register_permissions(); + + Ref get_rank(int rank_id); + + int get_default_user_rank_id(); + Ref get_default_user_rank(); + + int get_default_rank_id(); + Ref get_default_rank(); + + String &get_redirect_url(); + + bool continue_on_missing_default_rank(); + + // db + + virtual std::map > db_load_ranks(); + + virtual void db_save(const Ref &rank); + virtual void db_save_rank(const Ref &rank); + virtual void db_save_permission(const Ref &permission); + virtual int db_get_default_rank(); + virtual int db_get_default_user_rank(); + virtual String db_get_redirect_url(); + + void create_table(); + void drop_table(); + void create_default_entries(); + + static RBACController *get_singleton(); + + RBACController(); + ~RBACController(); + +protected: + static RBACController *_self; + + int _default_rank_id; + int _default_user_rank_id; + std::map > _ranks; + String _redirect_url; + + struct PermissionEntry { + String name; + int value; + + PermissionEntry() { + } + + PermissionEntry(const String &p_name, const int p_val) { + name = p_name; + value = p_val; + } + }; + + String _rbac_ranks_table; + String _rbac_permissions_table; + + Vector _registered_permissions; + Vector _registered_rank_permissions; +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac/rbac_default_permissions.h b/modules/web/nodes/rbac/rbac_default_permissions.h new file mode 100644 index 000000000..b4b214931 --- /dev/null +++ b/modules/web/nodes/rbac/rbac_default_permissions.h @@ -0,0 +1,9 @@ +#ifndef RBAC_DEFAULT_PERMISSIONS_H +#define RBAC_DEFAULT_PERMISSIONS_H + +enum RBACDefaultRankPermissions { + RBAC_RANK_PERMISSION_ADMIN_PANEL = 1 << 0, + RBAC_RANK_PERMISSION_USE_REDIRECT = 1 << 1, +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac/rbac_permission.cpp b/modules/web/nodes/rbac/rbac_permission.cpp new file mode 100644 index 000000000..12c394efb --- /dev/null +++ b/modules/web/nodes/rbac/rbac_permission.cpp @@ -0,0 +1,21 @@ +#include "rbac_permission.h" + +bool RBACPermission::is_smaller(const Ref &b) const { + if (!b.is_valid()) { + return true; + } + + return sort_order < b->sort_order; +} + +RBACPermission::RBACPermission() : + Resource() { + + id = 0; + rank_id = 0; + sort_order = 0; + permissions = 0; +} + +RBACPermission::~RBACPermission() { +} diff --git a/modules/web/nodes/rbac/rbac_permission.h b/modules/web/nodes/rbac/rbac_permission.h new file mode 100644 index 000000000..150090dd5 --- /dev/null +++ b/modules/web/nodes/rbac/rbac_permission.h @@ -0,0 +1,25 @@ +#ifndef RBAC_PERMISSION_H +#define RBAC_PERMISSION_H + +#include "core/string.h" + +#include "core/resource.h" + +class RBACPermission : public Resource { + RCPP_OBJECT(RBACPermission, Resource); + +public: + int id; + int rank_id; + String name; + String url; + int sort_order; + int permissions; + + bool is_smaller(const Ref &b) const; + + RBACPermission(); + ~RBACPermission(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac/rbac_rank.cpp b/modules/web/nodes/rbac/rbac_rank.cpp new file mode 100644 index 000000000..5a43f0dfe --- /dev/null +++ b/modules/web/nodes/rbac/rbac_rank.cpp @@ -0,0 +1,77 @@ +#include "rbac_rank.h" + +#include "web/http/request.h" + +Ref RBACRank::match_request(Request *request) { + const String &full_path = request->get_path_full(); + + Ref perm; + int current_max = 0; + + for (int i = 0; i < permissions.size(); ++i) { + Ref p; + + if (!p.is_valid()) { + continue; + } + + int c = full_path.first_difference_index(p->url); + + if (c > current_max) { + perm = p; + current_max = c; + } + } + + return perm; +} + +bool RBACRank::get_permissions(Request *request) { + int perm = base_permissions; + + Ref match = match_request(request); + + if (match.is_valid()) { + perm = match->permissions; + } + + return perm; +} + +bool RBACRank::has_permission(Request *request, const int permission) { + int perm = base_permissions; + + Ref match = match_request(request); + + if (match.is_valid()) { + perm = match->permissions; + } + + return (perm & permission) != 0; +} + +bool RBACRank::has_rank_permission(const int permission) { + return (rank_permissions & permission) != 0; +} + +void RBACRank::sort_permissions() { + for (int i = 0; i < permissions.size(); ++i) { + for (int j = i + 1; j < permissions.size(); ++j) { + if (permissions[j]->is_smaller(permissions[i])) { + permissions.swap(i, j); + } + } + } +} + +RBACRank::RBACRank() : + Resource() { + + id = 0; + base_permissions = 0; + rank_permissions = 0; +} + +RBACRank::~RBACRank() { + permissions.clear(); +} diff --git a/modules/web/nodes/rbac/rbac_rank.h b/modules/web/nodes/rbac/rbac_rank.h new file mode 100644 index 000000000..674cded86 --- /dev/null +++ b/modules/web/nodes/rbac/rbac_rank.h @@ -0,0 +1,40 @@ +#ifndef RBAC_RANK_H +#define RBAC_RANK_H + +#include "core/string.h" +#include "core/containers/vector.h" + +#include "core/resource.h" + +#include "rbac_permission.h" + +class Request; + +class RBACRank : public Resource { + RCPP_OBJECT(RBACRank, Resource); + +public: + int id; + + String name; + String name_internal; + String settings; + + int base_permissions; + int rank_permissions; + + Vector > permissions; + + Ref match_request(Request *request); + + bool get_permissions(Request *request); + bool has_permission(Request *request, const int permission); + bool has_rank_permission(const int permission); + + void sort_permissions(); + + RBACRank(); + ~RBACRank(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac_users/SCsub b/modules/web/nodes/rbac_users/SCsub new file mode 100644 index 000000000..33e297856 --- /dev/null +++ b/modules/web/nodes/rbac_users/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("users", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/rbac_users/detect.py b/modules/web/nodes/rbac_users/detect.py new file mode 100644 index 000000000..cf9500ee4 --- /dev/null +++ b/modules/web/nodes/rbac_users/detect.py @@ -0,0 +1,33 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "users" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass + +def get_module_dependencies(): + return [ + "users", + "rbac" + ] \ No newline at end of file diff --git a/modules/web/nodes/rbac_users/rbac_user.cpp b/modules/web/nodes/rbac_users/rbac_user.cpp new file mode 100644 index 000000000..000ecb092 --- /dev/null +++ b/modules/web/nodes/rbac_users/rbac_user.cpp @@ -0,0 +1,37 @@ +#include "rbac_user.h" + +int RBACUser::get_permissions(Request *request) { + if (!rbac_rank.is_valid()) { + return 0; + } + + return rbac_rank->get_permissions(request); +} +bool RBACUser::has_permission(Request *request, const int permission) { + if (!rbac_rank.is_valid()) { + return false; + } + + return rbac_rank->has_permission(request, permission); +} +int RBACUser::get_additional_permissions(Request *request) { + if (!rbac_rank.is_valid()) { + return 0; + } + + return rbac_rank->rank_permissions; +} +bool RBACUser::has_additional_permission(Request *request, const int permission) { + if (!rbac_rank.is_valid()) { + return false; + } + + return rbac_rank->rank_permissions & permission; +} + +RBACUser::RBACUser() : + User() { +} + +RBACUser::~RBACUser() { +} diff --git a/modules/web/nodes/rbac_users/rbac_user.h b/modules/web/nodes/rbac_users/rbac_user.h new file mode 100644 index 000000000..f86acf1fe --- /dev/null +++ b/modules/web/nodes/rbac_users/rbac_user.h @@ -0,0 +1,27 @@ +#ifndef RBAC_USER_H +#define RBAC_USER_H + +#include "core/string.h" + +#include "web_modules/users/user.h" +#include "web_modules/rbac/rbac_rank.h" + +class Request; +class FormValidator; + +class RBACUser : public User { + RCPP_OBJECT(RBACUser, User); + +public: + Ref rbac_rank; + + int get_permissions(Request *request); + bool has_permission(Request *request, const int permission); + int get_additional_permissions(Request *request); + bool has_additional_permission(Request *request, const int permission); + + RBACUser(); + ~RBACUser(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/rbac_users/rbac_user_controller.cpp b/modules/web/nodes/rbac_users/rbac_user_controller.cpp new file mode 100644 index 000000000..deac3f4dd --- /dev/null +++ b/modules/web/nodes/rbac_users/rbac_user_controller.cpp @@ -0,0 +1,146 @@ +#include "rbac_user_controller.h" + +#include "web/http/http_session.h" +#include "web/http/request.h" + +#include "web_modules/rbac/rbac_controller.h" +#include "web_modules/rbac/rbac_default_permissions.h" +#include "rbac_user.h" + +Ref RBACUserController::db_get_user(const int id) { + Ref u = UserController::db_get_user(id); + + if (u.is_valid()) { + u->rbac_rank = RBACController::get_singleton()->get_rank(u->rank); + } + + return u; +} +Ref RBACUserController::db_get_user(const String &user_name_input) { + Ref u = UserController::db_get_user(user_name_input); + + if (u.is_valid()) { + u->rbac_rank = RBACController::get_singleton()->get_rank(u->rank); + } + + return u; +} + +Vector > RBACUserController::db_get_all() { + Vector > users = UserController::db_get_all(); + + for (int i = 0; i < users.size(); ++i) { + Ref u = users[i]; + + if (u.is_valid()) { + u->rbac_rank = RBACController::get_singleton()->get_rank(u->rank); + } + } + + return users; +} + +Ref RBACUserController::create_user() { + Ref u; + u.instance(); + + u->rank = RBACController::get_singleton()->get_default_user_rank_id(); + u->rbac_rank = RBACController::get_singleton()->get_rank(u->rank); + + return u; +} + +RBACUserController::RBACUserController() : + UserController() { +} + +RBACUserController::~RBACUserController() { +} + +// returnring true means handled, false means continue +bool RBACUserSessionSetupMiddleware::on_before_handle_request_main(Request *request) { + if (request->session.is_valid()) { + int user_id = request->session->get_int("user_id"); + + if (user_id != 0) { + + Ref u = UserController::get_singleton()->db_get_user(user_id); + + if (u.is_valid()) { + request->reference_data["user"] = u; + } else { + // log + request->session->remove("user_id"); + } + } + } + + return false; +} + +RBACUserSessionSetupMiddleware::RBACUserSessionSetupMiddleware() { +} +RBACUserSessionSetupMiddleware::~RBACUserSessionSetupMiddleware() { +} + +// returnring true means handled, false means continue +bool RBACDefaultUserSessionSetupMiddleware::on_before_handle_request_main(Request *request) { + // note: add a new file handler middleware func, so basic file handling is easy to set up before this + + Ref rank; + + if (request->session.is_valid()) { + int user_id = request->session->get_int("user_id"); + + if (user_id != 0) { + + Ref u = UserController::get_singleton()->db_get_user(user_id); + + if (u.is_valid()) { + rank = u->rbac_rank; + + request->reference_data["user"] = u; + } else { + // log + request->session->remove("user_id"); + } + } + } + + if (!rank.is_valid()) { + rank = RBACController::get_singleton()->get_default_rank(); + + if (!rank.is_valid()) { + if (RBACController::get_singleton()->continue_on_missing_default_rank()) { + RLOG_ERR("get_default_rank() has not been set up properly!!! Continuing!"); + return false; + } else { + RLOG_ERR("get_default_rank() has not been set up properly!!! Sending 404!"); + request->send_error(404); + return true; + } + + + } + } + + if (!rank->has_permission(request, User::PERMISSION_READ)) { + if (rank->has_rank_permission(RBAC_RANK_PERMISSION_USE_REDIRECT)) { + // Note this can make the webapp prone to enumerations, if not done correctly + // e.g. redirect from /admin, but sending 404 on a non existing uri, which does not have + // a special rbac entry + request->send_redirect(RBACController::get_singleton()->get_redirect_url()); + return true; + } + + request->send_error(404); + return true; + } + + return false; +} + +RBACDefaultUserSessionSetupMiddleware::RBACDefaultUserSessionSetupMiddleware() { +} +RBACDefaultUserSessionSetupMiddleware::~RBACDefaultUserSessionSetupMiddleware() { +} diff --git a/modules/web/nodes/rbac_users/rbac_user_controller.h b/modules/web/nodes/rbac_users/rbac_user_controller.h new file mode 100644 index 000000000..24ec78038 --- /dev/null +++ b/modules/web/nodes/rbac_users/rbac_user_controller.h @@ -0,0 +1,54 @@ +#ifndef RBAC_USER_CONTROLLER_H +#define RBAC_USER_CONTROLLER_H + +#include "web_modules/users/user_controller.h" + +#include "web/http/middleware.h" + +class Request; + +class RBACUserController : public UserController { + RCPP_OBJECT(RBACUserController, UserController); + +public: + // db + + Ref db_get_user(const int id); + Ref db_get_user(const String &user_name_input); + + Vector > db_get_all(); + + Ref create_user(); + + RBACUserController(); + ~RBACUserController(); + +protected: +}; + +// just session setup +class RBACUserSessionSetupMiddleware : public Middleware { + RCPP_OBJECT(RBACUserSessionSetupMiddleware, Middleware); + +public: + //returnring true means handled, false means continue + bool on_before_handle_request_main(Request *request); + + RBACUserSessionSetupMiddleware(); + ~RBACUserSessionSetupMiddleware(); +}; + +// this one also handles missing read permission / redirect +class RBACDefaultUserSessionSetupMiddleware : public Middleware { + RCPP_OBJECT(RBACDefaultUserSessionSetupMiddleware, Middleware); + +public: + //returnring true means handled, false means continue + bool on_before_handle_request_main(Request *request); + + RBACDefaultUserSessionSetupMiddleware(); + ~RBACDefaultUserSessionSetupMiddleware(); +}; + + +#endif \ No newline at end of file diff --git a/modules/web/nodes/static_pages/SCsub b/modules/web/nodes/static_pages/SCsub new file mode 100644 index 000000000..e184b7a39 --- /dev/null +++ b/modules/web/nodes/static_pages/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("paged_list", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/static_pages/detect.py b/modules/web/nodes/static_pages/detect.py new file mode 100644 index 000000000..313219245 --- /dev/null +++ b/modules/web/nodes/static_pages/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "paged_list" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/static_pages/static_page.cpp b/modules/web/nodes/static_pages/static_page.cpp new file mode 100644 index 000000000..6600c5409 --- /dev/null +++ b/modules/web/nodes/static_pages/static_page.cpp @@ -0,0 +1,73 @@ +#include "static_page.h" + +#include "core/os/directory.h" +#include "web/file_cache.h" +#include "web/html/html_builder.h" +#include "web/html/utils.h" +#include "web/http/request.h" + +void StaticPage::_handle_request_main(Request *request) { + if (should_render_menu) { + render_menu(request); + } + + render_index(request); + request->compile_and_send_body(); +} + +void StaticPage::render_index(Request *request) { + request->body += data; +} + +void StaticPage::render_preview(Request *request) { + request->body += preview_data; +} + +void StaticPage::load_file(const String &path) { + Ref d; + d.instance(); + + d->read_file_into(path, &data); +} + +void StaticPage::load_and_process_file(const String &path) { + Ref d; + d.instance(); + + d->read_file_into(path, &data); + + if (path.file_get_extension() == "md") { + Utils::markdown_to_html(&data); + } +} + +void StaticPage::load_md_file(const String &path) { + Ref d; + d.instance(); + + d->read_file_into(path, &data); + Utils::markdown_to_html(&data); +} + +void StaticPage::set_data_md(const String &d) { + data.clear(); + + Utils::markdown_to_html(&data); +} + +void StaticPage::set_data(const String &d) { + data = d; +} + +void StaticPage::set_preview_data(const String &d) { + preview_data = d; +} + +StaticPage::StaticPage() : + WebNode() { + + should_render_menu = true; +} + +StaticPage::~StaticPage() { +} diff --git a/modules/web/nodes/static_pages/static_page.h b/modules/web/nodes/static_pages/static_page.h new file mode 100644 index 000000000..3783a70e3 --- /dev/null +++ b/modules/web/nodes/static_pages/static_page.h @@ -0,0 +1,33 @@ +#ifndef STATIC_PAGE_H +#define STATIC_PAGE_H + +#include "core/string.h" + +#include "web/http/web_node.h" + +class StaticPage : public WebNode { + RCPP_OBJECT(StaticPage, WebNode); + +public: + void _handle_request_main(Request *request); + + void render_index(Request *request); + void render_preview(Request *request); + + void load_file(const String &path); + void load_and_process_file(const String &path); + void load_md_file(const String &path); + + void set_data_md(const String &d); + void set_data(const String &d); + void set_preview_data(const String &d); + + String data; + String preview_data; + bool should_render_menu; + + StaticPage(); + ~StaticPage(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/static_pages/static_page_file.cpp b/modules/web/nodes/static_pages/static_page_file.cpp new file mode 100644 index 000000000..63fc4b598 --- /dev/null +++ b/modules/web/nodes/static_pages/static_page_file.cpp @@ -0,0 +1,36 @@ +#include "static_page_file.h" + +#include "web/file_cache.h" +#include "web/html/html_builder.h" +#include "web/http/request.h" + +void StaticPageFile::load() { + if (file_path == "") { + return; + } + + if (process_if_can) { + load_and_process_file(file_path); + } else { + load_file(file_path); + } +} + +void StaticPageFile::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + default: + break; + } +} + +StaticPageFile::StaticPageFile() : + StaticPage() { + + process_if_can = true; +} + +StaticPageFile::~StaticPageFile() { +} diff --git a/modules/web/nodes/static_pages/static_page_file.h b/modules/web/nodes/static_pages/static_page_file.h new file mode 100644 index 000000000..18e5473e1 --- /dev/null +++ b/modules/web/nodes/static_pages/static_page_file.h @@ -0,0 +1,23 @@ +#ifndef STATIC_PAGE_FILE_H +#define STATIC_PAGE_FILE_H + +#include "core/string.h" + +#include "static_page.h" + +class StaticPageFile : public StaticPage { + RCPP_OBJECT(StaticPageFile, StaticPage); + +public: + void load(); + + void _notification(const int what); + + String file_path; + bool process_if_can; + + StaticPageFile(); + ~StaticPageFile(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/static_pages/static_page_folder_files.cpp b/modules/web/nodes/static_pages/static_page_folder_files.cpp new file mode 100644 index 000000000..40a421432 --- /dev/null +++ b/modules/web/nodes/static_pages/static_page_folder_files.cpp @@ -0,0 +1,57 @@ +#include "static_page_folder_files.h" + +#include "core/os/directory.h" +#include "web/file_cache.h" +#include "web/html/html_builder.h" +#include "web/html/utils.h" +#include "web/http/request.h" + +void StaticPageFolderFiles::load() { + if (dir_path == "") { + return; + } + + Ref d; + d.instance(); + + ERR_FAIL_COND_MSG(!d->open_dir(dir_path), "Dir Path = " + dir_path); + + String str; + while (d->has_next()) { + if (d->current_is_file()) { + String fn = dir_path; + fn.append_path(d->current_get_name_cstr()); + + d->read_file_into(fn, &str); + + if (process_if_can && d->current_get_extension() == "md") { + Utils::markdown_to_html(&str); + } + + append_data(str); + } + } +} + +void StaticPageFolderFiles::append_data(const String &d) { + data += d; +} + +void StaticPageFolderFiles::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: + load(); + break; + default: + break; + } +} + +StaticPageFolderFiles::StaticPageFolderFiles() : + StaticPage() { + + process_if_can = true; +} + +StaticPageFolderFiles::~StaticPageFolderFiles() { +} diff --git a/modules/web/nodes/static_pages/static_page_folder_files.h b/modules/web/nodes/static_pages/static_page_folder_files.h new file mode 100644 index 000000000..21f6b8ded --- /dev/null +++ b/modules/web/nodes/static_pages/static_page_folder_files.h @@ -0,0 +1,24 @@ +#ifndef STATIC_PAGE_FOLDER_FILES_H +#define STATIC_PAGE_FOLDER_FILES_H + +#include "core/string.h" + +#include "static_page.h" + +class StaticPageFolderFiles : public StaticPage { + RCPP_OBJECT(StaticPageFolderFiles, StaticPage); + +public: + void load(); + virtual void append_data(const String &d); + + void _notification(const int what); + + String dir_path; + bool process_if_can; + + StaticPageFolderFiles(); + ~StaticPageFolderFiles(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/users/SCsub b/modules/web/nodes/users/SCsub new file mode 100644 index 000000000..33e297856 --- /dev/null +++ b/modules/web/nodes/users/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") + +# Build it all as a library +lib = env_mod.add_library("users", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/modules/web/nodes/users/detect.py b/modules/web/nodes/users/detect.py new file mode 100644 index 000000000..d35fcab18 --- /dev/null +++ b/modules/web/nodes/users/detect.py @@ -0,0 +1,27 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "users" + + +def can_build(): + return True + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + pass diff --git a/modules/web/nodes/users/user.cpp b/modules/web/nodes/users/user.cpp new file mode 100644 index 000000000..f6da8b571 --- /dev/null +++ b/modules/web/nodes/users/user.cpp @@ -0,0 +1,107 @@ +#include "user.h" + +#include "rapidjson/document.h" +#include "rapidjson/filewritestream.h" +#include "rapidjson/rapidjson.h" +#include "rapidjson/stringbuffer.h" +#include +#include +#include + +#include "database/database_manager.h" +#include "database/query_builder.h" +#include "database/query_result.h" +#include "database/table_builder.h" + +#include "web/html/form_validator.h" +#include "web/html/html_builder.h" +#include "web/http/cookie.h" +#include "web/http/http_session.h" +#include "web/http/request.h" +#include "web/http/session_manager.h" +#include "web/html/utils.h" + +String User::to_json(rapidjson::Document *into) { + rapidjson::Document *document; + + if (into) { + document = into; + } else { + document = new rapidjson::Document(); + } + + document->SetObject(); + + document->AddMember("id", id, document->GetAllocator()); + + document->AddMember("name", rapidjson::Value(name_user_input.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("email", rapidjson::Value(email_user_input.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("rank", rank, document->GetAllocator()); + document->AddMember("pre_salt", rapidjson::Value(pre_salt.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("post_salt", rapidjson::Value(post_salt.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("password_hash", rapidjson::Value(password_hash.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("banned", banned, document->GetAllocator()); + document->AddMember("password_reset_token", rapidjson::Value(password_reset_token.c_str(), document->GetAllocator()), document->GetAllocator()); + document->AddMember("locked", locked, document->GetAllocator()); + + if (into) { + return ""; + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document->Accept(writer); + + String s = buffer.GetString(); + + delete document; + + return s; +} + +void User::from_json(const String &p_data) { + + rapidjson::Document data; + data.Parse(p_data.c_str()); + + rapidjson::Value uobj = data.GetObject(); + + id = uobj["id"].GetInt(); + name_user_input = uobj["name"].GetString(); + email_user_input = uobj["email"].GetString(); + rank = uobj["rank"].GetInt(); + pre_salt = uobj["pre_salt"].GetString(); + post_salt = uobj["post_salt"].GetString(); + password_hash = uobj["password_hash"].GetString(); + banned = uobj["banned"].GetBool(); + + password_reset_token = uobj["password_reset_token"].GetString(); + locked = uobj["locked"].GetBool(); +} + +int User::get_permissions(Request *request) { + return PERMISSION_ALL; +} + +bool User::has_permission(Request *request, const int permission) { + return true; +} + +int User::get_additional_permissions(Request *request) { + return 0; +} + +bool User::has_additional_permission(Request *request, const int permission) { + return true; +} + +User::User() : + Resource() { + + rank = 0; + banned = false; + locked = false; +} + +User::~User() { +} diff --git a/modules/web/nodes/users/user.h b/modules/web/nodes/users/user.h new file mode 100644 index 000000000..32f77c69e --- /dev/null +++ b/modules/web/nodes/users/user.h @@ -0,0 +1,49 @@ +#ifndef USER_H +#define USER_H + +#include "core/string.h" + +#include "core/resource.h" +#include "rapidjson/document.h" +#include + +class Request; +class FormValidator; + +class User : public Resource { + RCPP_OBJECT(User, Resource); + +public: + enum Permissions { + PERMISSION_CREATE = 1 << 0, + PERMISSION_READ = 1 << 1, + PERMISSION_UPDATE = 1 << 2, + PERMISSION_DELETE = 1 << 3, + + PERMISSION_ALL = PERMISSION_CREATE | PERMISSION_READ | PERMISSION_UPDATE | PERMISSION_DELETE, + PERMISSION_NONE = 0 + }; + + String name_user_input; + String email_user_input; + int rank; + String pre_salt; + String post_salt; + String password_hash; + bool banned; + String password_reset_token; + bool locked; + + String to_json(rapidjson::Document *into = nullptr); + void from_json(const String &data); + + virtual int get_permissions(Request *request); + virtual bool has_permission(Request *request, const int permission); + virtual int get_additional_permissions(Request *request); + virtual bool has_additional_permission(Request *request, const int permission); + + User(); + ~User(); +}; + +#endif \ No newline at end of file diff --git a/modules/web/nodes/users/user_controller.cpp b/modules/web/nodes/users/user_controller.cpp new file mode 100644 index 000000000..c602f135f --- /dev/null +++ b/modules/web/nodes/users/user_controller.cpp @@ -0,0 +1,828 @@ +#include "user_controller.h" + +#include "web/html/form_validator.h" +#include "web/html/html_builder.h" +#include "web/http/cookie.h" +#include "web/http/http_session.h" +#include "web/http/request.h" +#include "web/http/session_manager.h" +#include "web/http/web_permission.h" + +#include "database/database.h" +#include "database/database_manager.h" +#include "database/query_builder.h" +#include "database/query_result.h" +#include "database/table_builder.h" + +#include "crypto/hash/sha256.h" + +void UserController::handle_request_main(Request *request) { + if (_web_permission.is_valid()) { + if (_web_permission->activate(request)) { + return; + } + } + + if (request->session.is_valid()) { + Ref u = request->reference_data["user"]; + + if (u.is_valid()) { + handle_request(u, request); + + return; + } + } + + const String &segment = request->get_current_path_segment(); + + if (segment == "") { + handle_login_request_default(request); + + return; + } else if (segment == "login") { + handle_login_request_default(request); + + return; + } else if (segment == "register") { + handle_register_request_default(request); + + return; + } + + handle_login_request_default(request); +} + +void UserController::handle_login_request_default(Request *request) { + LoginRequestData data; + + if (request->get_method() == HTTP_METHOD_POST) { + + // this is probbaly not needed + // it's ok for now as I need to test the validators more + Vector errors; + _login_validator->validate(request, &errors); + for (int i = 0; i < errors.size(); ++i) { + data.error_str += errors[i] + "
"; + } + // not needed end + + data.uname_val = request->get_parameter("username"); + data.pass_val = request->get_parameter("password"); + + Ref user = db_get_user(data.uname_val); + + if (user.is_valid()) { + if (!check_password(user, data.pass_val)) { + data.error_str += "Invalid username or password!"; + } else { + Ref session = request->get_or_create_session(); + + session->add("user_id", user->id); + SessionManager::get_singleton()->save_session(session); + + ::Cookie c = ::Cookie("session_id", session->session_id); + c.path = "/"; + + request->add_cookie(c); + + render_login_success(request); + + return; + } + } else { + data.error_str += "Invalid username or password!"; + } + } + + render_login_request_default(request, &data); +} + +void UserController::render_login_request_default(Request *request, LoginRequestData *data) { + HTMLBuilder b; + + b.w("Login"); + b.br(); + + { + if (data->error_str.size() != 0) { + b.div()->cls("error"); + + b.w(data->error_str); + + b.cdiv(); + } + } + + b.div()->cls("login"); + { + + // todo href path helper + b.form()->method("POST")->href("/user/login"); + { + b.csrf_token(request); + + b.w("Username"); + b.br(); + b.input()->type("text")->name("username")->value(data->uname_val); + b.cinput(); + b.br(); + + b.w("Password"); + b.br(); + b.input()->type("password")->name("password"); + b.cinput(); + b.br(); + + b.input()->type("submit")->value("Send"); + b.cinput(); + } + b.cform(); + } + b.cdiv(); + + request->body += b.result; + + request->compile_and_send_body(); +} + +void UserController::handle_register_request_default(Request *request) { + RegisterRequestData data; + + if (request->get_method() == HTTP_METHOD_POST) { + + Vector errors; + + _registration_validator->validate(request, &errors); + + for (int i = 0; i < errors.size(); ++i) { + data.error_str += errors[i] + "
"; + } + + data.uname_val = request->get_parameter("username"); + data.email_val = request->get_parameter("email"); + data.pass_val = request->get_parameter("password"); + data.pass_check_val = request->get_parameter("password_check"); + + // todo username length etc check + // todo pw length etc check + + if (is_username_taken(data.uname_val)) { + data.error_str += "Username already taken!
"; + } + + if (is_email_taken(data.email_val)) { + data.error_str += "Email already in use!
"; + } + + if (data.pass_val != data.pass_check_val) { + data.error_str += "The passwords did not match!
"; + } + + if (data.error_str.size() == 0) { + Ref user; + user = create_user(); + + user->name_user_input = data.uname_val; + user->email_user_input = data.email_val; + + create_password(user, data.pass_val); + db_save_user(user); + + render_register_success(request); + return; + } + } + + render_register_request_default(request, &data); +} + +void UserController::render_register_success(Request *request) { + HTMLBuilder b; + + b.div()->cls("success"); + { + b.w("Registration successful! You can now log in!"); + b.br(); + b.a()->href("/user/login"); + b.w(">> Login <<"); + b.ca(); + } + b.cdiv(); + + request->body += b.result; + + request->compile_and_send_body(); +} + +void UserController::render_register_request_default(Request *request, RegisterRequestData *data) { + HTMLBuilder b; + + b.w("Registration"); + b.br(); + + { + if (data->error_str.size() != 0) { + b.div()->cls("error"); + + b.w(data->error_str); + + b.cdiv(); + } + } + + b.div()->cls("register"); + { + // todo href path helper + b.form()->method("POST")->href("/user/register"); + { + b.csrf_token(request); + + b.w("Username"); + b.br(); + b.input()->type("text")->name("username")->value(data->uname_val); + b.cinput(); + b.br(); + + b.w("Email"); + b.br(); + b.input()->type("email")->name("email")->value(data->email_val); + b.cinput(); + b.br(); + + b.w("Password"); + b.br(); + b.input()->type("password")->name("password"); + b.cinput(); + b.br(); + + b.w("Password again"); + b.br(); + b.input()->type("password")->name("password_check"); + b.cinput(); + b.br(); + + b.input()->type("submit")->value("Register"); + b.cinput(); + } + b.cform(); + } + b.cdiv(); + + request->body += b.result; + + request->compile_and_send_body(); +} + +void UserController::render_already_logged_in_error(Request *request) { + request->body += "You are already logged in."; + + request->compile_and_send_body(); +} + +void UserController::render_login_success(Request *request) { + request->body = "Login Success!
"; + + // request->compile_and_send_body(); + request->send_redirect("/user/settings"); +} + +void UserController::handle_request(Ref &user, Request *request) { + const String &segment = request->get_current_path_segment(); + + if (segment == "") { + handle_main_page_request(user, request); + } else if (segment == "settings") { + handle_settings_request(user, request); + } else if (segment == "password_reset") { + handle_password_reset_request(user, request); + } else if (segment == "logout") { + handle_logout_request(user, request); + } else if (segment == "delete") { + handle_delete_request(user, request); + } else if (segment == "login") { + render_already_logged_in_error(request); + } else if (segment == "register") { + render_already_logged_in_error(request); + } else { + request->send_error(404); + } +} + +void UserController::handle_main_page_request(Ref &user, Request *request) { + request->body += "handle_main_page_request"; + + request->compile_and_send_body(); +} + +void UserController::handle_settings_request(Ref &user, Request *request) { + + SettingsRequestData data; + + if (request->get_method() == HTTP_METHOD_POST) { + + data.uname_val = request->get_parameter("username"); + data.email_val = request->get_parameter("email"); + data.pass_val = request->get_parameter("password"); + data.pass_check_val = request->get_parameter("password_check"); + + bool changed = false; + + Vector errors; + + bool valid = _profile_validator->validate(request, &errors); + + for (int i = 0; i < errors.size(); ++i) { + data.error_str += errors[i] + "
"; + } + + if (valid) { + if (data.uname_val == user->name_user_input) { + data.uname_val = ""; + } + + if (data.email_val == user->email_user_input) { + data.email_val = ""; + } + + if (data.uname_val != "") { + if (is_username_taken(data.uname_val)) { + data.error_str += "Username already taken!
"; + } else { + // todo sanitize for html special chars! + user->name_user_input = data.uname_val; + changed = true; + data.uname_val = ""; + } + } + + if (data.email_val != "") { + if (is_email_taken(data.email_val)) { + data.error_str += "Email already in use!
"; + } else { + // todo sanitize for html special chars! + // also send email + user->email_user_input = data.email_val; + changed = true; + data.email_val = ""; + } + } + + if (data.pass_val != "") { + if (data.pass_val != data.pass_check_val) { + data.error_str += "The passwords did not match!
"; + } else { + create_password(user, data.pass_val); + + changed = true; + } + } + + if (changed) { + db_save_user(user); + } + } + } + + render_settings_request(user, request, &data); +} + +void UserController::render_settings_request(Ref &user, Request *request, SettingsRequestData *data) { + HTMLBuilder b; + + b.w("Settings"); + b.br(); + + { + if (data->error_str.size() != 0) { + b.div()->cls("error"); + + b.w(data->error_str); + + b.cdiv(); + } + } + + b.div()->cls("settings"); + { + // todo href path helper + b.form()->method("POST")->href("/user/settings"); + { + b.csrf_token(request); + + b.w("Username"); + b.br(); + b.input()->type("text")->name("username")->placeholder(user->name_user_input)->value(data->uname_val); + b.cinput(); + b.br(); + + b.w("Email"); + b.br(); + b.input()->type("email")->name("email")->placeholder(user->email_user_input)->value(data->email_val); + b.cinput(); + b.br(); + + b.w("Password"); + b.br(); + b.input()->type("password")->placeholder("*******")->name("password"); + b.cinput(); + b.br(); + + b.w("Password again"); + b.br(); + b.input()->type("password")->placeholder("*******")->name("password_check"); + b.cinput(); + b.br(); + + b.input()->type("submit")->value("Save"); + b.cinput(); + } + b.cform(); + } + b.cdiv(); + + request->body += b.result; + + request->compile_and_send_body(); +} + +void UserController::handle_password_reset_request(Ref &user, Request *request) { + request->body += "handle_password_reset_request"; + + request->compile_and_send_body(); +} + +void UserController::handle_logout_request(Ref &user, Request *request) { + request->remove_cookie("session_id"); + + db_save_user(user); + + SessionManager::get_singleton()->delete_session(request->session->session_id); + request->session = nullptr; + + HTMLBuilder b; + b.w("Logout successful!"); + request->body += b.result; + + request->compile_and_send_body(); +} + +void UserController::handle_delete_request(Ref &user, Request *request) { + request->body += "handle_delete_request"; + + request->compile_and_send_body(); +} + +void UserController::create_validators() { + if (!_login_validator) { + // Login + _login_validator = new FormValidator(); + + _login_validator->new_field("username", "Username")->need_to_exist()->need_to_be_alpha_numeric()->need_minimum_length(5)->need_maximum_length(20); + FormField *pw = _login_validator->new_field("password", "Password"); + pw->need_to_exist(); + pw->need_to_have_lowercase_character()->need_to_have_uppercase_character(); + pw->need_minimum_length(5); + } + + if (!_registration_validator) { + // Registration + _registration_validator = new FormValidator(); + + _registration_validator->new_field("username", "Username")->need_to_exist()->need_to_be_alpha_numeric()->need_minimum_length(5)->need_maximum_length(20); + _registration_validator->new_field("email", "Email")->need_to_exist()->need_to_be_email(); + + FormField *pw = _registration_validator->new_field("password", "Password"); + pw->need_to_exist(); + pw->need_to_have_lowercase_character()->need_to_have_uppercase_character(); + pw->need_minimum_length(5); + + _registration_validator->new_field("password_check", "Password check")->need_to_match("password"); + + _registration_validator->new_field("email", "Email")->need_to_exist()->need_to_be_email(); + } + + if (!_profile_validator) { + _profile_validator = new FormValidator(); + + _profile_validator->new_field("username", "Username")->ignore_if_not_exists()->need_to_be_alpha_numeric()->need_minimum_length(5)->need_maximum_length(20); + _profile_validator->new_field("email", "Email")->ignore_if_not_exists()->need_to_be_email(); + + FormField *pw = _profile_validator->new_field("password", "Password"); + pw->ignore_if_not_exists(); + pw->need_to_have_lowercase_character()->need_to_have_uppercase_character(); + pw->need_minimum_length(5); + + _profile_validator->new_field("password_check", "Password check")->ignore_if_other_field_not_exists("password")->need_to_match("password"); + } +} + +Ref UserController::db_get_user(const int id) { + if (id == 0) { + return Ref(); + } + + Ref b = get_query_builder(); + + b->select("username, email, rank, pre_salt, post_salt, password_hash, banned, password_reset_token, locked"); + b->from(_table_name); + + b->where()->wp("id", id); + + b->end_command(); + + Ref r = b->run(); + + if (!r->next_row()) { + return Ref(); + } + + Ref user; + user = create_user(); + + user->id = id; + user->name_user_input = r->get_cell(0); + user->email_user_input = r->get_cell(1); + user->rank = r->get_cell_int(2); + user->pre_salt = r->get_cell(3); + user->post_salt = r->get_cell(4); + user->password_hash = r->get_cell(5); + user->banned = r->get_cell_bool(6); + user->password_reset_token = r->get_cell(7); + user->locked = r->get_cell_bool(8); + + return user; +} + +Ref UserController::db_get_user(const String &user_name_input) { + if (user_name_input == "") { + return Ref(); + } + + Ref b = get_query_builder(); + + b->select("id, email, rank, pre_salt, post_salt, password_hash, banned, password_reset_token, locked"); + b->from(_table_name); + b->where()->wp("username", user_name_input); + b->end_command(); + + Ref r = b->run(); + + if (!r->next_row()) { + return Ref(); + } + + Ref user; + user = create_user(); + + user->id = r->get_cell_int(0); + user->name_user_input = user_name_input; + user->email_user_input = r->get_cell(1); + user->rank = r->get_cell_int(2); + user->pre_salt = r->get_cell(3); + user->post_salt = r->get_cell(4); + user->password_hash = r->get_cell(5); + user->banned = r->get_cell_bool(6); + user->password_reset_token = r->get_cell(7); + user->locked = r->get_cell_bool(8); + + return user; +} + +void UserController::db_save_user(Ref &user) { + Ref b = get_query_builder(); + + if (user->id == 0) { + b->insert(_table_name, "username, email, rank, pre_salt, post_salt, password_hash, banned, password_reset_token, locked"); + + b->values(); + b->val(user->name_user_input); + b->val(user->email_user_input); + b->val(user->rank); + b->val(user->pre_salt); + b->val(user->post_salt); + b->val(user->password_hash); + b->val(user->banned); + b->val(user->password_reset_token); + b->val(user->locked); + b->cvalues(); + + b->end_command(); + b->select_last_insert_id(); + + Ref r = b->run(); + + user->id = r->get_last_insert_rowid(); + } else { + b->update(_table_name); + b->set(); + b->setp("username", user->name_user_input); + b->setp("email", user->email_user_input); + b->setp("rank", user->rank); + b->setp("pre_salt", user->pre_salt); + b->setp("post_salt", user->post_salt); + b->setp("password_hash", user->password_hash); + b->setp("banned", user->banned); + b->setp("password_reset_token", user->password_reset_token); + b->setp("locked", user->locked); + b->cset(); + b->where()->wp("id", user->id); + + // b->print(); + + b->run_query(); + } +} + +Vector > UserController::db_get_all() { + Ref b = get_query_builder(); + + b->select("id, username, email, rank, pre_salt, post_salt, password_hash, banned, password_reset_token, locked"); + b->from(_table_name); + b->end_command(); + // b->print(); + + Vector > users; + + Ref r = b->run(); + + while (r->next_row()) { + Ref user = create_user(); + + user->id = r->get_cell_int(0); + user->name_user_input = r->get_cell(1); + user->email_user_input = r->get_cell(2); + user->rank = r->get_cell_int(3); + user->pre_salt = r->get_cell(4); + user->post_salt = r->get_cell(5); + user->password_hash = r->get_cell(6); + user->banned = r->get_cell_bool(7); + user->password_reset_token = r->get_cell(8); + user->locked = r->get_cell_bool(9); + + users.push_back(user); + } + + return users; +} + +Ref UserController::create_user() { + Ref u; + u.instance(); + + return u; +} + +bool UserController::is_username_taken(const String &user_name_input) { + Ref b = get_query_builder(); + + b->select("id")->from(_table_name)->where("username")->like(user_name_input)->end_command(); + + Ref r = b->run(); + + return r->next_row(); +} +bool UserController::is_email_taken(const String &email_input) { + Ref b = get_query_builder(); + + b->select("id")->from(_table_name)->where("username")->like(email_input)->end_command(); + + Ref r = b->run(); + + return r->next_row(); +} + +bool UserController::check_password(const Ref &user, const String &p_password) { + return hash_password(user, p_password) == user->password_hash; +} + +void UserController::create_password(Ref &user, const String &p_password) { + if (!user.is_valid()) { + printf("Error UserController::create_password !user.is_valid()!\n"); + return; + } + + // todo improve a bit + user->pre_salt = hash_password(user, user->name_user_input + user->email_user_input); + user->post_salt = hash_password(user, user->email_user_input + user->name_user_input); + + user->password_hash = hash_password(user, p_password); +} + +String UserController::hash_password(const Ref &user, const String &p_password) { + if (!user.is_valid()) { + printf("Error UserController::hash_password !user.is_valid()!\n"); + return ""; + } + + Ref s = SHA256::get(); + + String p = user->pre_salt + p_password + user->post_salt; + + String c = s->compute(p); + + return c; +} + +void UserController::create_table() { + Ref tb = get_table_builder(); + + tb->create_table(_table_name); + tb->integer("id")->auto_increment()->next_row(); + tb->varchar("username", 60)->not_null()->next_row(); + tb->varchar("email", 100)->not_null()->next_row(); + tb->integer("rank")->not_null()->next_row(); + tb->varchar("pre_salt", 100)->next_row(); + tb->varchar("post_salt", 100)->next_row(); + tb->varchar("password_hash", 100)->next_row(); + tb->integer("banned")->next_row(); + tb->varchar("password_reset_token", 100)->next_row(); + tb->integer("locked")->next_row(); + tb->primary_key("id"); + tb->ccreate_table(); + tb->run_query(); + // tb->print(); +} +void UserController::drop_table() { + Ref tb = get_table_builder(); + + tb->drop_table_if_exists(_table_name)->run_query(); +} + +void UserController::create_default_entries() { + Ref user; + user = create_user(); + + user->rank = 3; + user->name_user_input = "admin"; + user->email_user_input = "admin@admin.com"; + + create_password(user, "Password"); + db_save_user(user); + + user = create_user(); + + user->rank = 1; + user->name_user_input = "user"; + user->email_user_input = "user@user.com"; + + create_password(user, "Password"); + db_save_user(user); +} + +UserController *UserController::get_singleton() { + return _self; +} + +UserController::UserController() : + WebNode() { + + if (_self) { + printf("UserController::UserController(): Error! self is not null!/n"); + } + + _self = this; + + create_validators(); +} + +UserController::~UserController() { + if (_self == this) { + _self = nullptr; + } +} + +UserController *UserController::_self = nullptr; + +FormValidator *UserController::_login_validator = nullptr; +FormValidator *UserController::_registration_validator = nullptr; +FormValidator *UserController::_profile_validator = nullptr; + +String UserController::_path = "./"; +String UserController::_table_name = "users"; + + +// returnring true means handled, false means continue +bool UserSessionSetupMiddleware::on_before_handle_request_main(Request *request) { + if (request->session.is_valid()) { + int user_id = request->session->get_int("user_id"); + + if (user_id != 0) { + + Ref u = UserController::get_singleton()->db_get_user(user_id); + + if (u.is_valid()) { + request->reference_data["user"] = u; + } else { + // log + request->session->remove("user_id"); + } + } + } + + return false; +} + +UserSessionSetupMiddleware::UserSessionSetupMiddleware() { +} +UserSessionSetupMiddleware::~UserSessionSetupMiddleware() { +} diff --git a/modules/web/nodes/users/user_controller.h b/modules/web/nodes/users/user_controller.h new file mode 100644 index 000000000..cdbf90bcb --- /dev/null +++ b/modules/web/nodes/users/user_controller.h @@ -0,0 +1,118 @@ +#ifndef USER_CONTROLLER_H +#define USER_CONTROLLER_H + +#include "core/containers/vector.h" +#include "core/string.h" + +#include "web/http/web_node.h" + +#include "user.h" +#include "web/http/middleware.h" + +class Request; +class FormValidator; + +class UserController : public WebNode { + RCPP_OBJECT(UserController, WebNode); + +public: + void handle_request_main(Request *request); + + struct LoginRequestData { + String error_str; + String uname_val; + String pass_val; + }; + + virtual void handle_login_request_default(Request *request); + virtual void render_login_request_default(Request *request, LoginRequestData *data); + + struct RegisterRequestData { + String error_str; + String uname_val; + String email_val; + String pass_val; + String pass_check_val; + }; + + virtual void handle_register_request_default(Request *request); + virtual void render_register_request_default(Request *request, RegisterRequestData *data); + virtual void render_register_success(Request *request); + + virtual void render_already_logged_in_error(Request *request); + virtual void render_login_success(Request *request); + + virtual void handle_request(Ref &user, Request *request); + virtual void handle_main_page_request(Ref &user, Request *request); + + struct SettingsRequestData { + String error_str; + + String uname_val; + String email_val; + String pass_val; + String pass_check_val; + }; + + virtual void handle_settings_request(Ref &user, Request *request); + virtual void render_settings_request(Ref &user, Request *request, SettingsRequestData *data); + + virtual void handle_password_reset_request(Ref &user, Request *request); + virtual void handle_logout_request(Ref &user, Request *request); + virtual void handle_delete_request(Ref &user, Request *request); + + virtual void create_validators(); + + // db + + virtual Ref db_get_user(const int id); + virtual Ref db_get_user(const String &user_name_input); + virtual void db_save_user(Ref &user); + + virtual Vector > db_get_all(); + + virtual Ref create_user(); + + bool is_username_taken(const String &user_name_input); + bool is_email_taken(const String &email_input); + + virtual bool check_password(const Ref &user, const String &p_password); + virtual void create_password(Ref &user, const String &p_password); + virtual String hash_password(const Ref &user, const String &p_password); + + virtual void create_table(); + virtual void drop_table(); + virtual void create_default_entries(); + + static UserController *get_singleton(); + + UserController(); + ~UserController(); + +protected: + static UserController *_self; + + static FormValidator *_login_validator; + static FormValidator *_registration_validator; + static FormValidator *_profile_validator; + + String _file_path; + + static String _path; + static String _table_name; +}; + +// just session setup +class UserSessionSetupMiddleware : public Middleware { + RCPP_OBJECT(UserSessionSetupMiddleware, Middleware); + +public: + //returnring true means handled, false means continue + bool on_before_handle_request_main(Request *request); + + UserSessionSetupMiddleware(); + ~UserSessionSetupMiddleware(); +}; + + +#endif \ No newline at end of file