From 076106ecb9d23ee9d6a412244fd0c4aa379539ba Mon Sep 17 00:00:00 2001 From: Albert S Date: Sat, 3 Nov 2018 17:12:20 +0100 Subject: [PATCH] Let's make (git) history! --- .gitignore | 10 + LICENCE | 20 + Makefile | 59 + README.md | 77 + TODO | 8 + cache/fscache.cpp | 62 + cache/fscache.h | 20 + cache/icache.h | 18 + category.cpp | 26 + category.h | 14 + config.cpp | 127 + config.h | 74 + cookie.cpp | 28 + cookie.h | 20 + database/categorydao.cpp | 26 + database/categorydao.h | 21 + database/categorydaosqlite.cpp | 118 + database/categorydaosqlite.h | 18 + database/database.h | 32 + database/databasefactory.cpp | 26 + database/databasefactory.h | 7 + database/exceptions.h | 16 + database/hdr/sqlite_modern_cpp.h | 1047 +++++++ database/hdr/sqlite_modern_cpp/errors.h | 60 + .../hdr/sqlite_modern_cpp/lists/error_codes.h | 93 + database/hdr/sqlite_modern_cpp/log.h | 101 + database/hdr/sqlite_modern_cpp/sqlcipher.h | 44 + .../utility/function_traits.h | 55 + .../utility/uncaught_exceptions.h | 27 + .../sqlite_modern_cpp/utility/utf16_utf8.h | 42 + .../hdr/sqlite_modern_cpp/utility/variant.h | 201 ++ database/pagedao.cpp | 26 + database/pagedao.h | 28 + database/pagedaosqlite.cpp | 201 ++ database/pagedaosqlite.h | 28 + database/permissionsdao.cpp | 26 + database/permissionsdao.h | 12 + database/permissionsdaosqlite.cpp | 32 + database/permissionsdaosqlite.h | 15 + database/queryoption.cpp | 22 + database/queryoption.h | 19 + database/revisiondao.cpp | 22 + database/revisiondao.h | 22 + database/revisiondaosqlite.cpp | 168 ++ database/revisiondaosqlite.h | 23 + database/sessiondao.cpp | 26 + database/sessiondao.h | 16 + database/sessiondaosqlite.cpp | 97 + database/sessiondaosqlite.h | 20 + database/sqlite.cpp | 91 + database/sqlite.h | 31 + database/sqlitedao.cpp | 52 + database/sqlitedao.h | 36 + database/sqlitequeryoption.cpp | 66 + database/sqlitequeryoption.h | 26 + database/userdao.cpp | 26 + database/userdao.h | 20 + database/userdaosqlite.cpp | 93 + database/userdaosqlite.h | 22 + gateway/cgi.cpp | 115 + gateway/cgi.h | 21 + gateway/gatewayfactory.cpp | 29 + gateway/gatewayfactory.h | 8 + gateway/gatewayinterface.cpp | 26 + gateway/gatewayinterface.h | 16 + gateway/httpgateway.cpp | 95 + gateway/httpgateway.h | 22 + gateway/httplib.h | 2395 +++++++++++++++++ handlers/handler.cpp | 79 + handlers/handler.h | 37 + handlers/handlerallcategories.cpp | 44 + handlers/handlerallcategories.h | 15 + handlers/handlerallpages.cpp | 51 + handlers/handlerallpages.h | 13 + handlers/handlercategory.cpp | 51 + handlers/handlercategory.h | 13 + handlers/handlerdefault.cpp | 32 + handlers/handlerdefault.h | 13 + handlers/handlerfactory.cpp | 101 + handlers/handlerfactory.h | 8 + handlers/handlerhistory.cpp | 106 + handlers/handlerhistory.h | 14 + handlers/handlerinvalidaction.cpp | 27 + handlers/handlerinvalidaction.h | 14 + handlers/handlerlogin.cpp | 123 + handlers/handlerlogin.h | 19 + handlers/handlerpage.cpp | 94 + handlers/handlerpage.h | 21 + handlers/handlerpagedelete.cpp | 51 + handlers/handlerpagedelete.h | 26 + handlers/handlerpageedit.cpp | 122 + handlers/handlerpageedit.h | 20 + handlers/handlerpageview.cpp | 170 ++ handlers/handlerpageview.h | 25 + handlers/handlersearch.cpp | 63 + handlers/handlersearch.h | 13 + headline.cpp | 21 + headline.h | 12 + htmllink.cpp | 26 + htmllink.h | 21 + iparser.h | 18 + logger.cpp | 23 + logger.h | 72 + page.cpp | 26 + page.h | 15 + parser.cpp | 143 + parser.h | 19 + permissions.cpp | 38 + permissions.h | 77 + qswiki.cpp | 112 + random.cpp | 54 + random.h | 53 + request.cpp | 129 + request.h | 114 + requestworker.cpp | 93 + requestworker.h | 35 + response.cpp | 47 + response.h | 60 + revision.cpp | 27 + revision.h | 18 + searchresult.h | 10 + session.cpp | 48 + session.h | 19 + setup/config | 35 + setup/sqlite.sql | 48 + template.cpp | 180 ++ template.h | 39 + template/default/_headerlink | 1 + template/default/allcategories | 6 + template/default/allpages | 6 + template/default/error | 6 + template/default/general_footer | 16 + template/default/general_header | 19 + template/default/login | 11 + template/default/login_already | 7 + template/default/page_creation | 15 + template/default/page_creation_preview | 18 + template/default/page_deletion | 11 + template/default/page_footer | 7 + template/default/page_header | 3 + template/default/page_history | 9 + template/default/page_settings | 15 + template/default/page_view | 7 + template/default/page_view_revision | 8 + template/default/recentchanges | 9 + template/default/search | 6 + template/default/show_category | 6 + template/default/show_whatlinkshere | 6 + template/greensimple/_headerlink | 1 + template/greensimple/admin_register | 15 + template/greensimple/allcategories | 6 + template/greensimple/allpages | 6 + template/greensimple/error | 6 + template/greensimple/general_footer | 18 + template/greensimple/general_header | 20 + template/greensimple/login | 11 + template/greensimple/login_already | 7 + template/greensimple/page_creation | 14 + template/greensimple/page_creation_preview | 17 + template/greensimple/page_deletion | 10 + template/greensimple/page_footer | 20 + template/greensimple/page_header | 24 + template/greensimple/page_history | 8 + template/greensimple/page_settings | 16 + template/greensimple/page_view | 5 + template/greensimple/page_view_revision | 6 + template/greensimple/recentchanges | 8 + template/greensimple/search | 7 + template/greensimple/show_category | 6 + template/greensimple/show_whatlinkshere | 6 + template/greensimple/style.css | 172 ++ template/greensimple/user_changepw | 15 + template/quitesimple/_headerlink | 1 + template/quitesimple/admin_register | 15 + template/quitesimple/allcategories | 6 + template/quitesimple/allpages | 6 + template/quitesimple/error | 6 + template/quitesimple/general_footer | 18 + template/quitesimple/general_header | 21 + template/quitesimple/login | 11 + template/quitesimple/login_already | 7 + template/quitesimple/page_creation | 14 + template/quitesimple/page_creation_preview | 17 + template/quitesimple/page_deletion | 10 + template/quitesimple/page_footer | 20 + template/quitesimple/page_header | 25 + template/quitesimple/page_history | 8 + template/quitesimple/page_settings | 16 + template/quitesimple/page_view | 11 + template/quitesimple/page_view_revision | 6 + template/quitesimple/recentchanges | 8 + template/quitesimple/search | 7 + template/quitesimple/show_category | 6 + template/quitesimple/show_whatlinkshere | 6 + template/quitesimple/style.css | 213 ++ template/quitesimple/user_changepw | 15 + templatepage.cpp | 44 + templatepage.h | 20 + tests/parser.h | 11 + tests/request.cpp | 60 + tests/template.cpp | 42 + tests/testconfigprovider.cpp | 33 + tests/testconfigprovider.h | 13 + tests/utils.cpp | 101 + urlprovider.cpp | 116 + urlprovider.h | 51 + user.cpp | 35 + user.h | 24 + utils.cpp | 178 ++ utils.h | 105 + varreplacer.cpp | 106 + varreplacer.h | 26 + 212 files changed, 11541 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO create mode 100644 cache/fscache.cpp create mode 100644 cache/fscache.h create mode 100644 cache/icache.h create mode 100644 category.cpp create mode 100644 category.h create mode 100644 config.cpp create mode 100644 config.h create mode 100644 cookie.cpp create mode 100644 cookie.h create mode 100644 database/categorydao.cpp create mode 100644 database/categorydao.h create mode 100644 database/categorydaosqlite.cpp create mode 100644 database/categorydaosqlite.h create mode 100644 database/database.h create mode 100644 database/databasefactory.cpp create mode 100644 database/databasefactory.h create mode 100644 database/exceptions.h create mode 100644 database/hdr/sqlite_modern_cpp.h create mode 100644 database/hdr/sqlite_modern_cpp/errors.h create mode 100644 database/hdr/sqlite_modern_cpp/lists/error_codes.h create mode 100644 database/hdr/sqlite_modern_cpp/log.h create mode 100644 database/hdr/sqlite_modern_cpp/sqlcipher.h create mode 100644 database/hdr/sqlite_modern_cpp/utility/function_traits.h create mode 100644 database/hdr/sqlite_modern_cpp/utility/uncaught_exceptions.h create mode 100644 database/hdr/sqlite_modern_cpp/utility/utf16_utf8.h create mode 100644 database/hdr/sqlite_modern_cpp/utility/variant.h create mode 100644 database/pagedao.cpp create mode 100644 database/pagedao.h create mode 100644 database/pagedaosqlite.cpp create mode 100644 database/pagedaosqlite.h create mode 100644 database/permissionsdao.cpp create mode 100644 database/permissionsdao.h create mode 100644 database/permissionsdaosqlite.cpp create mode 100644 database/permissionsdaosqlite.h create mode 100644 database/queryoption.cpp create mode 100644 database/queryoption.h create mode 100644 database/revisiondao.cpp create mode 100644 database/revisiondao.h create mode 100644 database/revisiondaosqlite.cpp create mode 100644 database/revisiondaosqlite.h create mode 100644 database/sessiondao.cpp create mode 100644 database/sessiondao.h create mode 100644 database/sessiondaosqlite.cpp create mode 100644 database/sessiondaosqlite.h create mode 100644 database/sqlite.cpp create mode 100644 database/sqlite.h create mode 100644 database/sqlitedao.cpp create mode 100644 database/sqlitedao.h create mode 100644 database/sqlitequeryoption.cpp create mode 100644 database/sqlitequeryoption.h create mode 100644 database/userdao.cpp create mode 100644 database/userdao.h create mode 100644 database/userdaosqlite.cpp create mode 100644 database/userdaosqlite.h create mode 100644 gateway/cgi.cpp create mode 100644 gateway/cgi.h create mode 100644 gateway/gatewayfactory.cpp create mode 100644 gateway/gatewayfactory.h create mode 100644 gateway/gatewayinterface.cpp create mode 100644 gateway/gatewayinterface.h create mode 100644 gateway/httpgateway.cpp create mode 100644 gateway/httpgateway.h create mode 100644 gateway/httplib.h create mode 100644 handlers/handler.cpp create mode 100644 handlers/handler.h create mode 100644 handlers/handlerallcategories.cpp create mode 100644 handlers/handlerallcategories.h create mode 100644 handlers/handlerallpages.cpp create mode 100644 handlers/handlerallpages.h create mode 100644 handlers/handlercategory.cpp create mode 100644 handlers/handlercategory.h create mode 100644 handlers/handlerdefault.cpp create mode 100644 handlers/handlerdefault.h create mode 100644 handlers/handlerfactory.cpp create mode 100644 handlers/handlerfactory.h create mode 100644 handlers/handlerhistory.cpp create mode 100644 handlers/handlerhistory.h create mode 100644 handlers/handlerinvalidaction.cpp create mode 100644 handlers/handlerinvalidaction.h create mode 100644 handlers/handlerlogin.cpp create mode 100644 handlers/handlerlogin.h create mode 100644 handlers/handlerpage.cpp create mode 100644 handlers/handlerpage.h create mode 100644 handlers/handlerpagedelete.cpp create mode 100644 handlers/handlerpagedelete.h create mode 100644 handlers/handlerpageedit.cpp create mode 100644 handlers/handlerpageedit.h create mode 100644 handlers/handlerpageview.cpp create mode 100644 handlers/handlerpageview.h create mode 100644 handlers/handlersearch.cpp create mode 100644 handlers/handlersearch.h create mode 100644 headline.cpp create mode 100644 headline.h create mode 100644 htmllink.cpp create mode 100644 htmllink.h create mode 100644 iparser.h create mode 100644 logger.cpp create mode 100644 logger.h create mode 100644 page.cpp create mode 100644 page.h create mode 100644 parser.cpp create mode 100644 parser.h create mode 100644 permissions.cpp create mode 100644 permissions.h create mode 100644 qswiki.cpp create mode 100644 random.cpp create mode 100644 random.h create mode 100644 request.cpp create mode 100644 request.h create mode 100644 requestworker.cpp create mode 100644 requestworker.h create mode 100644 response.cpp create mode 100644 response.h create mode 100644 revision.cpp create mode 100644 revision.h create mode 100644 searchresult.h create mode 100644 session.cpp create mode 100644 session.h create mode 100644 setup/config create mode 100644 setup/sqlite.sql create mode 100644 template.cpp create mode 100644 template.h create mode 100644 template/default/_headerlink create mode 100644 template/default/allcategories create mode 100644 template/default/allpages create mode 100644 template/default/error create mode 100644 template/default/general_footer create mode 100644 template/default/general_header create mode 100644 template/default/login create mode 100644 template/default/login_already create mode 100644 template/default/page_creation create mode 100644 template/default/page_creation_preview create mode 100644 template/default/page_deletion create mode 100644 template/default/page_footer create mode 100644 template/default/page_header create mode 100644 template/default/page_history create mode 100644 template/default/page_settings create mode 100644 template/default/page_view create mode 100644 template/default/page_view_revision create mode 100644 template/default/recentchanges create mode 100644 template/default/search create mode 100644 template/default/show_category create mode 100644 template/default/show_whatlinkshere create mode 100644 template/greensimple/_headerlink create mode 100644 template/greensimple/admin_register create mode 100644 template/greensimple/allcategories create mode 100644 template/greensimple/allpages create mode 100644 template/greensimple/error create mode 100644 template/greensimple/general_footer create mode 100644 template/greensimple/general_header create mode 100644 template/greensimple/login create mode 100644 template/greensimple/login_already create mode 100644 template/greensimple/page_creation create mode 100644 template/greensimple/page_creation_preview create mode 100644 template/greensimple/page_deletion create mode 100644 template/greensimple/page_footer create mode 100644 template/greensimple/page_header create mode 100644 template/greensimple/page_history create mode 100644 template/greensimple/page_settings create mode 100644 template/greensimple/page_view create mode 100644 template/greensimple/page_view_revision create mode 100644 template/greensimple/recentchanges create mode 100644 template/greensimple/search create mode 100644 template/greensimple/show_category create mode 100644 template/greensimple/show_whatlinkshere create mode 100644 template/greensimple/style.css create mode 100644 template/greensimple/user_changepw create mode 100644 template/quitesimple/_headerlink create mode 100644 template/quitesimple/admin_register create mode 100644 template/quitesimple/allcategories create mode 100644 template/quitesimple/allpages create mode 100644 template/quitesimple/error create mode 100644 template/quitesimple/general_footer create mode 100644 template/quitesimple/general_header create mode 100644 template/quitesimple/login create mode 100644 template/quitesimple/login_already create mode 100644 template/quitesimple/page_creation create mode 100644 template/quitesimple/page_creation_preview create mode 100644 template/quitesimple/page_deletion create mode 100644 template/quitesimple/page_footer create mode 100644 template/quitesimple/page_header create mode 100644 template/quitesimple/page_history create mode 100644 template/quitesimple/page_settings create mode 100644 template/quitesimple/page_view create mode 100644 template/quitesimple/page_view_revision create mode 100644 template/quitesimple/recentchanges create mode 100644 template/quitesimple/search create mode 100644 template/quitesimple/show_category create mode 100644 template/quitesimple/show_whatlinkshere create mode 100644 template/quitesimple/style.css create mode 100644 template/quitesimple/user_changepw create mode 100644 templatepage.cpp create mode 100644 templatepage.h create mode 100644 tests/parser.h create mode 100644 tests/request.cpp create mode 100644 tests/template.cpp create mode 100644 tests/testconfigprovider.cpp create mode 100644 tests/testconfigprovider.h create mode 100644 tests/utils.cpp create mode 100644 urlprovider.cpp create mode 100644 urlprovider.h create mode 100644 user.cpp create mode 100644 user.h create mode 100644 utils.cpp create mode 100644 utils.h create mode 100644 varreplacer.cpp create mode 100644 varreplacer.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7510822 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.o +*.d +*.out +*.gch +*.user +qswiki +wikiqs* +data/* +gtest* +cgi-bin/* diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..3b1b460 --- /dev/null +++ b/LICENCE @@ -0,0 +1,20 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..af3545c --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ + + +CXXFLAGS=-std=c++17 -O0 -g -pg -no-pie -pipe -MMD -Wall -Wextra +RELEASE_CXXFLAGS=-std=c++17 -O3 -pipe -MMD -Wall -Wextra +LDFLAGS=-lsqlite3 -lpthread -lcrypto -lstdc++fs + +#currently default g++ versions in most distros do not usually support c++17 well enough +CXX=g++-8.2.0 + + +SOURCES=$(wildcard *.cpp) +SOURCES+=$(wildcard gateway/*.cpp) +SOURCES+=$(wildcard handlers/*.cpp) +SOURCES+=$(wildcard database/*.cpp) +SOURCES+=$(wildcard cache/*.cpp) + +HEADERS=$(wildcard *.h) +HEADERS+=$(wildcard gateway/*.h) +HEADERS+=$(wildcard handlers/*.h) +HEADERS+=$(wildcard database/*.h) +HEADERS+=$(wildcard cache/*.h) + + +OBJECTS=$(patsubst %.cpp, %.o, $(SOURCES)) +WIKIOBJECTS=$(filter-out test.o, $(OBJECTS)) +TESTOBJECTS=$(filter-out qswiki.o, $(OBJECTS)) +DEPENDS = ${WIKIOBJECTS:.o=.d} +-include ${DEPENDS} + +# Points to the root of Google Test, relative to where this file is. +# Remember to tweak this if you move this file. +GTEST_DIR = /home/data/SOURCES/gtest/googletest + +GTESTS_TESTDIR = ./tests/ + +GTEST_CXXFLAGS=-std=c++17 -isystem $(GTEST_DIR)/include -I$(GTEST_DIR) -g -O0 -pipe -Wall -Wextra +GTEST_LDFLAGS=-lsqlite3 -g -O0 -lpthread -lcrypto -lstdc++fs +GTEST_OBJECTS=$(filter-out qswiki.o, $(WIKIOBJECTS)) + +.DEFAULT_GOAL := qswiki + +release: CXXFLAGS=$(RELEASE_CXXFLAGS) +release: qswiki +qswiki: $(WIKIOBJECTS) + $(CXX) $(WIKIOBJECTS) ${LDFLAGS} -I database/hdr -o qswiki + +test: $(TESTOBJECTS) + $(CXX) $(TESTOBJECTS) ${LDFLAGS} -o test + +gtest: $(GTESTS_TESTDIR)/*.cpp $(GTEST_OBJECTS) + $(CXX) -o gtest $(GTESTS_TESTDIR)/*.cpp $(GTEST_OBJECTS) $(GTEST_CXXFLAGS) $(GTEST_DIR)/src/gtest_main.cc $(GTEST_DIR)/src/gtest-all.cc $(GTEST_LDFLAGS) + +%.o:%.cpp + $(CXX) ${CXXFLAGS} ${LDFLAGS} -I database/hdr -c -o $@ $< + +clean: + rm -f $(OBJECTS) $(DEPENDS) + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a262c85 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# qswiki + +About +==== +qswiki is a wiki software, intended for small wikis. Originally +implemented in C, it's now written in C++. + +History +==== +A couple of years ago, I wanted to setup a personal wiki on my raspberry +pi. However, the distribution I used back then did not have a PHP package +for ARM. So I decided I would write one in C. Yes, that's an odd way +to approach the problem and indeed, I may have had too much time back +then. Also, I wanted to see how it's like to write a "web app" in C +and wanted to sharpen my C a little bit. + +Of course, it's pretty straightforward at first. No really. Just use CGI. +And indeed, that's probably more than enough. Then I decided to play +around and started using FastCGI (with the official library from now +defunct fastcgi.com) and created a multi-threaded version. It initially +used a "pile of files database", but that became too painful, so then +I started using sqlite. + +C++ +--- +Eventually the code became unmaintainable. Initially, I wanted something +quick. I did not care about memory leaks (as it was CGI initially). +After FastCGI, they became an issue. In the end, the task of avoiding +memory leaks became too annoying. And of course, C does not include any +"batteries" and while I could manage, this too was another good reason. + +Overall, I am just continuing the experiment with C++17 now. It's not +nearly as bad as you would expect perhaps. Some things are surprisingly +convenient even. Still, the standard library is lacking and +I would hope for a some better built-in Unicode support in the future. + +Features +======== +To be fair, at this point it doesn't even have a "diff" between revisions +yet and does not have features that make you prefer it over other wikis. + + - CGI + - HTTP server using the header only library cpp-httplib. It's more + portable and more "future-proof" than FastCGI (since the official website + disappeared, the library's future appears to be uncertain). + - Support for user accounts. Passwords are stored using PBKDF2. + sqlite database, but not too much of an effort to add other types of + storage backends. sqlite is using the great header only library + sqlite_modern_cpp + - Relatively fine-grained permission system. + - Categories + - Templates + - FTS search + - Caching + +Security +======== +The most reasonable way would have been to add some sort sandboxing +support right away, but this is lacking so far. As for "web security", +all POST requests are centrally protected against CSRF attacks and all +input is escaped against XSS attacks. + +Building +======== +Dependencies: + - cpp-httplib: https://github.com/yhirose/cpp-httplib + - SqliteModernCpp: https://github.com/SqliteModernCpp + +Given the fact those are header-only libraries, they are already +included here, so you only need to run: + +```make release``` + + +Setup +===== +To be written diff --git a/TODO b/TODO new file mode 100644 index 0000000..743b686 --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ +search: allow all chars (filter sqlite match syntax) +diff +Redirection,Rename +UI for permission system. +user administration +user registration +more caching +not all config values take effect yet. diff --git a/cache/fscache.cpp b/cache/fscache.cpp new file mode 100644 index 0000000..aef4fbc --- /dev/null +++ b/cache/fscache.cpp @@ -0,0 +1,62 @@ +#include +#include +#include "fscache.h" +#include "../logger.h" + +FsCache::FsCache(std::string path) +{ + if(!std::filesystem::exists(path)) + { + throw std::runtime_error { "Directory does not exist" }; + } + this->path = path; +} + +std::string FsCache::getFilePath(std::string_view path) const +{ + std::filesystem::path ps { path }; + std::string name = ps.filename(); + return std::filesystem::path { this->path } / name; +} +std::optional FsCache::get(std::string_view key) const +{ + std::string path = getFilePath(key); + if(std::filesystem::exists(path)) + { + return utils::readCompleteFile(path); + } + return { }; +} + +void FsCache::put(std::string_view key, std::string val) +{ + std::string path = std::filesystem::path { this->path } / key; + std::fstream f1; + f1.open(path, std::ios::out); + f1 << val; +} + +void FsCache::remove(std::string_view key) +{ + std::filesystem::remove_all(std::filesystem::path { this->path} / key); +} + +void FsCache::removePrefix(std::string_view prefix) +{ + //TODO: lock dir + for(auto &entry : std::filesystem::directory_iterator(std::filesystem::path { this->path })) + { + if(static_cast(entry.path().filename()).find(prefix) == 0) + { + std::filesystem::remove_all(entry); + } + } +} + +void FsCache::clear() +{ + for(auto &entry : std::filesystem::directory_iterator(std::filesystem::path { this->path })) + { + std::filesystem::remove_all(entry); + } +} diff --git a/cache/fscache.h b/cache/fscache.h new file mode 100644 index 0000000..b35f079 --- /dev/null +++ b/cache/fscache.h @@ -0,0 +1,20 @@ +#ifndef FSCACHE_H +#define FSCACHE_H +#include "icache.h" +class FsCache : public ICache +{ +private: + std::string path; + std::string getFilePath(std::string_view path) const; +public: + FsCache(std::string directory); + std::optional get(std::string_view key) const; + void put(std::string_view key, std::string val); + void remove(std::string_view key); + void removePrefix(std::string_view prefix); + void clear(); + using ICache::ICache; + ~FsCache() { } +}; + +#endif // FSCACHE_H diff --git a/cache/icache.h b/cache/icache.h new file mode 100644 index 0000000..8bdc326 --- /dev/null +++ b/cache/icache.h @@ -0,0 +1,18 @@ +#ifndef ICACHE_H +#define ICACHE_H +#include +#include +#include +#include "../utils.h" +class ICache +{ +public: + virtual std::optional get(std::string_view key) const = 0; + virtual void put(std::string_view key, std::string val) = 0; + virtual void remove(std::string_view key) = 0; + virtual void removePrefix(std::string_view prefix) = 0; + virtual void clear() = 0; + virtual ~ICache() { } +}; + +#endif // ICACHE_H diff --git a/category.cpp b/category.cpp new file mode 100644 index 0000000..74dea0f --- /dev/null +++ b/category.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "category.h" + +Category::Category() +{ + +} diff --git a/category.h b/category.h new file mode 100644 index 0000000..57c428b --- /dev/null +++ b/category.h @@ -0,0 +1,14 @@ +#ifndef CATEGORY_H +#define CATEGORY_H + +#include +class Category +{ +public: + Category(); + unsigned int id; + std::string name; + +}; + +#endif // CATEGORY_H diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..cc3c815 --- /dev/null +++ b/config.cpp @@ -0,0 +1,127 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "config.h" +#include "permissions.h" +#include +#include +std::string Config::required(const std::string &key) +{ + auto it = this->configmap.find(key); + if(it != this->configmap.end()) + { + return it->second; + } + throw std::runtime_error("Required config key " + key + " not found"); +} + +std::string Config::optional(const std::string &key, std::string defaultvalue) +{ + auto it = this->configmap.find(key); + if(it != this->configmap.end()) + { + return it->second; + } + return defaultvalue; +} + +int Config::optional(const std::string &key, int defaultvalue) +{ + auto it = this->configmap.find(key); + if(it != this->configmap.end()) + { + std::string str = it->second; + return std::stoi(str); + } + return defaultvalue; +} + +Config::Config(const std::map &map) +{ + + this->configmap = map; + this->wikipath = optional("wikipath", "/"); + this->anon_username = optional("anon_username", "anonymouse"); + this->wikiname = required("wikiname"); + this->logfile = required("logfile"); + this->templatepath = required("templatepath"); + this->linkallcats = required("linkallcats"); + this->linkallpages = required("linkallpages"); + this->linkcategory = required("linkcategory"); + this->linkdelete = required("linkdelete"); + this->linkedit = required("linkedit"); + this->linkhistory = required("linkhistory"); + this->linkindex = required("linkindex"); + this->linklogout = required("linklogout"); + this->linkpage = required("linkpage"); + this->linkrecent = required("linkrecent"); + this->linkrevision = required("linkrevision"); + this->linksettings = required("linksettings"); + this->linkshere = required("linkshere"); + this->loginurl = required("loginurl"); + this->linkrecentsort = required("linkrecentsort"); + this->linkhistorysort = required("linkhistorysort"); + this->actionurl = required("actionurl"); + this->settingsurl = required("settingsurl"); + this->deletionurl = required("deletionurl"); + this->adminregisterurl = required("adminregisterurl"); + this->userchangepwurl = required("userchangepwurl"); + this->connectionstring = required("connectionstring"); + + + this->max_pagename_length = optional("max_pagename_length", 256); + this->session_max_lifetime = optional("session_max_lifetime", 3600); + this->query_limit = optional("query_limit", 200); + this->threadscount = optional("threadscount", 1); + + this->anon_permissions = Permissions(required("anon_permissions")); + + this->templateprefix = "{wikiqs:"; + + + +} + +ConfigReader::ConfigReader(const std::string &file) +{ + this->path = file; +} + +Config ConfigReader::readConfig() +{ + std::fstream f1(path, std::fstream::in); + std::string line; + std::map configmap; + while(getline(f1, line)) + { + if(isspace(line[0]) || line[0] == '#') { + continue; +} + std::stringstream s(line); + std::string key; + std::string value; + s >> key >> value; + + configmap.insert(std::make_pair(std::move(key), std::move(value))); + + + } + return Config(configmap); +} diff --git a/config.h b/config.h new file mode 100644 index 0000000..723bc9c --- /dev/null +++ b/config.h @@ -0,0 +1,74 @@ + #ifndef CONFIG_H +#define CONFIG_H +#include +#include +#include +#include +#include "permissions.h" +#include "utils.h" +class Config +{ +private: + std::map configmap; + std::string required(const std::string &key); + + std::string optional(const std::string &key, std::string defaultvalue = ""); + int optional(const std::string &key, int defaulvalue); + +public: + Config(const std::map &map ); + //TODO: these could be references!? + std::string wikiname; + std::string wikipath; + std::string templatepath; + std::string templateprefix; + std::string logfile; + std::string anon_username; + std::string linkindex ; + std::string linkrecent ; + std::string linkallpages ; + std::string linkallcats ; + std::string linkshere ; + std::string linkpage ; + std::string linkrevision ; + std::string linkhistory ; + std::string linkedit ; + std::string linksettings; + std::string linkdelete ; + std::string linklogout ; + std::string linkcategory; + std::string loginurl; + std::string linkrecentsort; + std::string actionurl; + std::string settingsurl; + std::string deletionurl; + std::string linkhistorysort; + std::string adminregisterurl; + std::string userchangepwurl; + std::string connectionstring; + + int query_limit; + int session_max_lifetime; + int max_pagename_length; + int threadscount; + Permissions anon_permissions; + + std::string getConfig(const std::string &key) const + { + return utils::getKeyOrEmpty(configmap, key); + } + +}; + +class ConfigReader +{ +private: + std::string path; + public: + ConfigReader(const std::string &file); + Config readConfig(); + + +}; + +#endif // CONFIG_H diff --git a/cookie.cpp b/cookie.cpp new file mode 100644 index 0000000..18f207d --- /dev/null +++ b/cookie.cpp @@ -0,0 +1,28 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "cookie.h" + +Cookie::Cookie(const std::string &key, const std::string &val, int expireSeconds) +{ + this->key = key; + this->value = val; + this->expires = expireSeconds; +} diff --git a/cookie.h b/cookie.h new file mode 100644 index 0000000..92ac0a3 --- /dev/null +++ b/cookie.h @@ -0,0 +1,20 @@ +#ifndef COOKIE_H +#define COOKIE_H + +#include +class Cookie +{ +public: + std::string key; + std::string value; + int expires; + + Cookie(const std::string &key, const std::string &val, int expireSeconds = 0); + + std::string createHeaderValue() const + { + return key + "=" + value + "; path=/; HttpOnly"; + } +}; + +#endif // COOKIE_H diff --git a/database/categorydao.cpp b/database/categorydao.cpp new file mode 100644 index 0000000..c2bb048 --- /dev/null +++ b/database/categorydao.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "categorydao.h" + +CategoryDao::CategoryDao() +{ + +} diff --git a/database/categorydao.h b/database/categorydao.h new file mode 100644 index 0000000..4eae533 --- /dev/null +++ b/database/categorydao.h @@ -0,0 +1,21 @@ +#ifndef CATEGORYDAO_H +#define CATEGORYDAO_H +#include +#include +#include +#include "queryoption.h" +#include "../category.h" + +class CategoryDao +{ +public: + CategoryDao(); + virtual void save(const Category &c) = 0; + virtual std::vector fetchList(QueryOption o) = 0; + virtual std::optional find(std::string name) = 0; + virtual void deleteCategory(std::string name) = 0; + virtual std::vector fetchMembers(std::string name, QueryOption o) = 0; + +}; + +#endif // CATEGORYDAO_H diff --git a/database/categorydaosqlite.cpp b/database/categorydaosqlite.cpp new file mode 100644 index 0000000..1cfe8e6 --- /dev/null +++ b/database/categorydaosqlite.cpp @@ -0,0 +1,118 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include "categorydaosqlite.h" +#include "sqlitequeryoption.h" +CategoryDaoSqlite::CategoryDaoSqlite() +{ + +} + +std::optional CategoryDaoSqlite::find(std::string name) +{ + try { + Category result; + *db << "SELECT id, name FROM category WHERE name = ?" << name >> std::tie(result.id, result.name); + return result; + } + catch(const sqlite::exceptions::no_rows &e) + { + return {}; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +void CategoryDaoSqlite::save(const Category &c) +{ + + try + { + *db << "INSERT OR IGNORE INTO category (id, name) VALUES (SELECT id FROM category WHERE lower(name) = lower(?), lower(?)" < CategoryDaoSqlite::fetchList(QueryOption o) +{ + std::vector result; + try + { + auto queryoption = SqliteQueryOption(o).setPrependWhere(true).setOrderByColumn("name").build(); + *db << "SELECT name FROM category " + queryoption >> [&](std::string n) { result.push_back(n);}; + + } + catch(const sqlite::exceptions::no_rows &e) + { + return result; + } + catch(const sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; +} +std::vector CategoryDaoSqlite::fetchMembers(std::string name, QueryOption o) +{ + std::vector result; + + SqliteQueryOption queryOption { o }; + std::string queryoptions = queryOption.setOrderByColumn("name").setVisibleColumnName("page.visible").setPrependWhere(false).build(); + + + try + { + auto query = *db << "SELECT page.name AS name FROM categorymember INNER JOIN page ON page.id = categorymember.page WHERE category = (SELECT id FROM category WHERE name = ? ) AND " + queryoptions << name; + query >> [&](std::string p) { result.push_back(p);}; + } + catch(const sqlite::exceptions::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + + return result; +} diff --git a/database/categorydaosqlite.h b/database/categorydaosqlite.h new file mode 100644 index 0000000..b7a477b --- /dev/null +++ b/database/categorydaosqlite.h @@ -0,0 +1,18 @@ +#ifndef CATEGORYDAOSQLITE_H +#define CATEGORYDAOSQLITE_H + +#include "categorydao.h" +#include "sqlitedao.h" +class CategoryDaoSqlite : public CategoryDao, protected SqliteDao +{ +public: + CategoryDaoSqlite(); + std::vector fetchList(QueryOption o) override; + std::vector fetchMembers(std::string name, QueryOption o) override; + void save(const Category &c) override; + void deleteCategory(std::string name) override; + std::optional find(std::string name) override; + using SqliteDao::SqliteDao; +}; + +#endif // CATEGORYDAOSQLITE_H diff --git a/database/database.h b/database/database.h new file mode 100644 index 0000000..040ae3e --- /dev/null +++ b/database/database.h @@ -0,0 +1,32 @@ +#ifndef DATABASE_H +#define DATABASE_H +#include +#include +#include "../user.h" +#include "../request.h" +#include "../response.h" +#include "pagedao.h" +#include "revisiondao.h" +#include "sessiondao.h" +#include "userdao.h" +#include "categorydao.h" +class Database +{ +private: + std::string connnectionstring; + public: + Database() { } + Database(std::string connstring) { this->connnectionstring = connstring; } + + virtual void beginTransaction() = 0; + virtual void rollbackTransaction() = 0; + virtual void commitTransaction() = 0; + virtual std::unique_ptr createPageDao() const = 0; + virtual std::unique_ptr createRevisionDao() const = 0; + virtual std::unique_ptr createSessionDao() const = 0; + virtual std::unique_ptr createUserDao() const = 0; + virtual std::unique_ptr createCategoryDao() const = 0; + virtual ~Database() { } +}; + +#endif diff --git a/database/databasefactory.cpp b/database/databasefactory.cpp new file mode 100644 index 0000000..f59c327 --- /dev/null +++ b/database/databasefactory.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "databasefactory.h" +#include "sqlite.h" +std::unique_ptr createDatabase(const Config &o) +{ + return std::make_unique(o.connectionstring); +} diff --git a/database/databasefactory.h b/database/databasefactory.h new file mode 100644 index 0000000..b6beb39 --- /dev/null +++ b/database/databasefactory.h @@ -0,0 +1,7 @@ +#ifndef DATABASEFACTORY_H +#define DATABASEFACTORY_H +#include "../config.h" +#include "database.h" + +std::unique_ptr createDatabase(const Config &o); +#endif // DATABASEFACTORY_H diff --git a/database/exceptions.h b/database/exceptions.h new file mode 100644 index 0000000..8cbf2c8 --- /dev/null +++ b/database/exceptions.h @@ -0,0 +1,16 @@ +#ifndef EXCEPTIONS_H +#define EXCEPTIONS_H +#include + + +class DatabaseException : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + +class DatabaseQueryException : public DatabaseException +{ + using DatabaseException::DatabaseException; +}; + +#endif // EXCEPTIONS_H diff --git a/database/hdr/sqlite_modern_cpp.h b/database/hdr/sqlite_modern_cpp.h new file mode 100644 index 0000000..e0a16a8 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp.h @@ -0,0 +1,1047 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define MODERN_SQLITE_VERSION 3002008 + +#ifdef __has_include +#if __cplusplus > 201402 && __has_include() +#define MODERN_SQLITE_STD_OPTIONAL_SUPPORT +#elif __has_include() +#define MODERN_SQLITE_EXPERIMENTAL_OPTIONAL_SUPPORT +#endif +#endif + +#ifdef __has_include +#if __cplusplus > 201402 && __has_include() +#define MODERN_SQLITE_STD_VARIANT_SUPPORT +#endif +#endif + +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT +#include +#endif + +#ifdef MODERN_SQLITE_EXPERIMENTAL_OPTIONAL_SUPPORT +#include +#define MODERN_SQLITE_STD_OPTIONAL_SUPPORT +#endif + +#ifdef _MODERN_SQLITE_BOOST_OPTIONAL_SUPPORT +#include +#endif + +#include + +#include "sqlite_modern_cpp/errors.h" +#include "sqlite_modern_cpp/utility/function_traits.h" +#include "sqlite_modern_cpp/utility/uncaught_exceptions.h" +#include "sqlite_modern_cpp/utility/utf16_utf8.h" + +#ifdef MODERN_SQLITE_STD_VARIANT_SUPPORT +#include "sqlite_modern_cpp/utility/variant.h" +#endif + +namespace sqlite { + + // std::optional support for NULL values + #ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + #ifdef MODERN_SQLITE_EXPERIMENTAL_OPTIONAL_SUPPORT + template + using optional = std::experimental::optional; + #else + template + using optional = std::optional; + #endif + #endif + + class database; + class database_binder; + + template class binder; + + typedef std::shared_ptr connection_type; + + template::value == Element)> struct tuple_iterate { + static void iterate(Tuple& t, database_binder& db) { + get_col_from_db(db, Element, std::get(t)); + tuple_iterate::iterate(t, db); + } + }; + + template struct tuple_iterate { + static void iterate(Tuple&, database_binder&) {} + }; + + class database_binder { + + public: + // database_binder is not copyable + database_binder() = delete; + database_binder(const database_binder& other) = delete; + database_binder& operator=(const database_binder&) = delete; + + database_binder(database_binder&& other) : + _db(std::move(other._db)), + _stmt(std::move(other._stmt)), + _inx(other._inx), execution_started(other.execution_started) { } + + void execute() { + _start_execute(); + int hresult; + + while((hresult = sqlite3_step(_stmt.get())) == SQLITE_ROW) {} + + if(hresult != SQLITE_DONE) { + errors::throw_sqlite_error(hresult, sql()); + } + } + + std::string sql() { +#if SQLITE_VERSION_NUMBER >= 3014000 + auto sqlite_deleter = [](void *ptr) {sqlite3_free(ptr);}; + std::unique_ptr str(sqlite3_expanded_sql(_stmt.get()), sqlite_deleter); + return str ? str.get() : original_sql(); +#else + return original_sql(); +#endif + } + + std::string original_sql() { + return sqlite3_sql(_stmt.get()); + } + + void used(bool state) { + if(!state) { + // We may have to reset first if we haven't done so already: + _next_index(); + --_inx; + } + execution_started = state; + } + bool used() const { return execution_started; } + + private: + std::shared_ptr _db; + std::unique_ptr _stmt; + utility::UncaughtExceptionDetector _has_uncaught_exception; + + int _inx; + + bool execution_started = false; + + int _next_index() { + if(execution_started && !_inx) { + sqlite3_reset(_stmt.get()); + sqlite3_clear_bindings(_stmt.get()); + } + return ++_inx; + } + void _start_execute() { + _next_index(); + _inx = 0; + used(true); + } + + void _extract(std::function call_back) { + int hresult; + _start_execute(); + + while((hresult = sqlite3_step(_stmt.get())) == SQLITE_ROW) { + call_back(); + } + + if(hresult != SQLITE_DONE) { + errors::throw_sqlite_error(hresult, sql()); + } + } + + void _extract_single_value(std::function call_back) { + int hresult; + _start_execute(); + + if((hresult = sqlite3_step(_stmt.get())) == SQLITE_ROW) { + call_back(); + } else if(hresult == SQLITE_DONE) { + throw errors::no_rows("no rows to extract: exactly 1 row expected", sql(), SQLITE_DONE); + } + + if((hresult = sqlite3_step(_stmt.get())) == SQLITE_ROW) { + throw errors::more_rows("not all rows extracted", sql(), SQLITE_ROW); + } + + if(hresult != SQLITE_DONE) { + errors::throw_sqlite_error(hresult, sql()); + } + } + + sqlite3_stmt* _prepare(const std::u16string& sql) { + return _prepare(utility::utf16_to_utf8(sql)); + } + + sqlite3_stmt* _prepare(const std::string& sql) { + int hresult; + sqlite3_stmt* tmp = nullptr; + const char *remaining; + hresult = sqlite3_prepare_v2(_db.get(), sql.data(), -1, &tmp, &remaining); + if(hresult != SQLITE_OK) errors::throw_sqlite_error(hresult, sql); + if(!std::all_of(remaining, sql.data() + sql.size(), [](char ch) {return std::isspace(ch);})) + throw errors::more_statements("Multiple semicolon separated statements are unsupported", sql); + return tmp; + } + + template + struct is_sqlite_value : public std::integral_constant< + bool, + std::is_floating_point::value + || std::is_integral::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + > { }; + template + struct is_sqlite_value< std::vector > : public std::integral_constant< + bool, + std::is_floating_point::value + || std::is_integral::value + || std::is_same::value + > { }; +#ifdef MODERN_SQLITE_STD_VARIANT_SUPPORT + template + struct is_sqlite_value< std::variant > : public std::integral_constant< + bool, + true + > { }; +#endif + + + /* for vector support */ + template friend database_binder& operator <<(database_binder& db, const std::vector& val); + template friend void get_col_from_db(database_binder& db, int inx, std::vector& val); + /* for nullptr & unique_ptr support */ + friend database_binder& operator <<(database_binder& db, std::nullptr_t); + template friend database_binder& operator <<(database_binder& db, const std::unique_ptr& val); + template friend void get_col_from_db(database_binder& db, int inx, std::unique_ptr& val); +#ifdef MODERN_SQLITE_STD_VARIANT_SUPPORT + template friend database_binder& operator <<(database_binder& db, const std::variant& val); + template friend void get_col_from_db(database_binder& db, int inx, std::variant& val); +#endif + template friend T operator++(database_binder& db, int); + // Overload instead of specializing function templates (http://www.gotw.ca/publications/mill17.htm) + friend database_binder& operator<<(database_binder& db, const int& val); + friend void get_col_from_db(database_binder& db, int inx, int& val); + friend database_binder& operator <<(database_binder& db, const sqlite_int64& val); + friend void get_col_from_db(database_binder& db, int inx, sqlite3_int64& i); + friend database_binder& operator <<(database_binder& db, const float& val); + friend void get_col_from_db(database_binder& db, int inx, float& f); + friend database_binder& operator <<(database_binder& db, const double& val); + friend void get_col_from_db(database_binder& db, int inx, double& d); + friend void get_col_from_db(database_binder& db, int inx, std::string & s); + friend database_binder& operator <<(database_binder& db, const std::string& txt); + friend void get_col_from_db(database_binder& db, int inx, std::u16string & w); + friend database_binder& operator <<(database_binder& db, const std::u16string& txt); + + +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template friend database_binder& operator <<(database_binder& db, const optional& val); + template friend void get_col_from_db(database_binder& db, int inx, optional& o); +#endif + +#ifdef _MODERN_SQLITE_BOOST_OPTIONAL_SUPPORT + template friend database_binder& operator <<(database_binder& db, const boost::optional& val); + template friend void get_col_from_db(database_binder& db, int inx, boost::optional& o); +#endif + + public: + + database_binder(std::shared_ptr db, std::u16string const & sql): + _db(db), + _stmt(_prepare(sql), sqlite3_finalize), + _inx(0) { + } + + database_binder(std::shared_ptr db, std::string const & sql): + _db(db), + _stmt(_prepare(sql), sqlite3_finalize), + _inx(0) { + } + + ~database_binder() noexcept(false) { + /* Will be executed if no >>op is found, but not if an exception + is in mid flight */ + if(!used() && !_has_uncaught_exception && _stmt) { + execute(); + } + } + + template + typename std::enable_if::value, void>::type operator>>( + Result& value) { + this->_extract_single_value([&value, this] { + get_col_from_db(*this, 0, value); + }); + } + + template + void operator>>(std::tuple&& values) { + this->_extract_single_value([&values, this] { + tuple_iterate>::iterate(values, *this); + }); + } + + template + typename std::enable_if::value, void>::type operator>>( + Function&& func) { + typedef utility::function_traits traits; + + this->_extract([&func, this]() { + binder::run(*this, func); + }); + } + }; + + namespace sql_function_binder { + template< + typename ContextType, + std::size_t Count, + typename Functions + > + inline void step( + sqlite3_context* db, + int count, + sqlite3_value** vals + ); + + template< + std::size_t Count, + typename Functions, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) && sizeof...(Values) < Count), void>::type step( + sqlite3_context* db, + int count, + sqlite3_value** vals, + Values&&... values + ); + + template< + std::size_t Count, + typename Functions, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) == Count), void>::type step( + sqlite3_context* db, + int, + sqlite3_value**, + Values&&... values + ); + + template< + typename ContextType, + typename Functions + > + inline void final(sqlite3_context* db); + + template< + std::size_t Count, + typename Function, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) < Count), void>::type scalar( + sqlite3_context* db, + int count, + sqlite3_value** vals, + Values&&... values + ); + + template< + std::size_t Count, + typename Function, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) == Count), void>::type scalar( + sqlite3_context* db, + int, + sqlite3_value**, + Values&&... values + ); + } + + enum class OpenFlags { + READONLY = SQLITE_OPEN_READONLY, + READWRITE = SQLITE_OPEN_READWRITE, + CREATE = SQLITE_OPEN_CREATE, + NOMUTEX = SQLITE_OPEN_NOMUTEX, + FULLMUTEX = SQLITE_OPEN_FULLMUTEX, + SHAREDCACHE = SQLITE_OPEN_SHAREDCACHE, + PRIVATECACH = SQLITE_OPEN_PRIVATECACHE, + URI = SQLITE_OPEN_URI + }; + inline OpenFlags operator|(const OpenFlags& a, const OpenFlags& b) { + return static_cast(static_cast(a) | static_cast(b)); + } + enum class Encoding { + ANY = SQLITE_ANY, + UTF8 = SQLITE_UTF8, + UTF16 = SQLITE_UTF16 + }; + struct sqlite_config { + OpenFlags flags = OpenFlags::READWRITE | OpenFlags::CREATE; + const char *zVfs = nullptr; + Encoding encoding = Encoding::ANY; + }; + + class database { + protected: + std::shared_ptr _db; + + public: + database(const std::string &db_name, const sqlite_config &config = {}): _db(nullptr) { + sqlite3* tmp = nullptr; + auto ret = sqlite3_open_v2(db_name.data(), &tmp, static_cast(config.flags), config.zVfs); + _db = std::shared_ptr(tmp, [=](sqlite3* ptr) { sqlite3_close_v2(ptr); }); // this will close the connection eventually when no longer needed. + if(ret != SQLITE_OK) errors::throw_sqlite_error(_db ? sqlite3_extended_errcode(_db.get()) : ret); + sqlite3_extended_result_codes(_db.get(), true); + if(config.encoding == Encoding::UTF16) + *this << R"(PRAGMA encoding = "UTF-16";)"; + } + + database(const std::u16string &db_name, const sqlite_config &config = {}): _db(nullptr) { + auto db_name_utf8 = utility::utf16_to_utf8(db_name); + sqlite3* tmp = nullptr; + auto ret = sqlite3_open_v2(db_name_utf8.data(), &tmp, static_cast(config.flags), config.zVfs); + _db = std::shared_ptr(tmp, [=](sqlite3* ptr) { sqlite3_close_v2(ptr); }); // this will close the connection eventually when no longer needed. + if(ret != SQLITE_OK) errors::throw_sqlite_error(_db ? sqlite3_extended_errcode(_db.get()) : ret); + sqlite3_extended_result_codes(_db.get(), true); + if(config.encoding != Encoding::UTF8) + *this << R"(PRAGMA encoding = "UTF-16";)"; + } + + database(std::shared_ptr db): + _db(db) {} + + database_binder operator<<(const std::string& sql) { + return database_binder(_db, sql); + } + + database_binder operator<<(const char* sql) { + return *this << std::string(sql); + } + + database_binder operator<<(const std::u16string& sql) { + return database_binder(_db, sql); + } + + database_binder operator<<(const char16_t* sql) { + return *this << std::u16string(sql); + } + + connection_type connection() const { return _db; } + + sqlite3_int64 last_insert_rowid() const { + return sqlite3_last_insert_rowid(_db.get()); + } + + template + void define(const std::string &name, Function&& func) { + typedef utility::function_traits traits; + + auto funcPtr = new auto(std::forward(func)); + if(int result = sqlite3_create_function_v2( + _db.get(), name.c_str(), traits::arity, SQLITE_UTF8, funcPtr, + sql_function_binder::scalar::type>, + nullptr, nullptr, [](void* ptr){ + delete static_cast(ptr); + })) + errors::throw_sqlite_error(result); + } + + template + void define(const std::string &name, StepFunction&& step, FinalFunction&& final) { + typedef utility::function_traits traits; + using ContextType = typename std::remove_reference>::type; + + auto funcPtr = new auto(std::make_pair(std::forward(step), std::forward(final))); + if(int result = sqlite3_create_function_v2( + _db.get(), name.c_str(), traits::arity - 1, SQLITE_UTF8, funcPtr, nullptr, + sql_function_binder::step::type>, + sql_function_binder::final::type>, + [](void* ptr){ + delete static_cast(ptr); + })) + errors::throw_sqlite_error(result); + } + + }; + + template + class binder { + private: + template < + typename Function, + std::size_t Index + > + using nth_argument_type = typename utility::function_traits< + Function + >::template argument; + + public: + // `Boundary` needs to be defaulted to `Count` so that the `run` function + // template is not implicitly instantiated on class template instantiation. + // Look up section 14.7.1 _Implicit instantiation_ of the ISO C++14 Standard + // and the [dicussion](https://github.com/aminroosta/sqlite_modern_cpp/issues/8) + // on Github. + + template< + typename Function, + typename... Values, + std::size_t Boundary = Count + > + static typename std::enable_if<(sizeof...(Values) < Boundary), void>::type run( + database_binder& db, + Function&& function, + Values&&... values + ) { + typename std::remove_cv>::type>::type value{}; + get_col_from_db(db, sizeof...(Values), value); + + run(db, function, std::forward(values)..., std::move(value)); + } + + template< + typename Function, + typename... Values, + std::size_t Boundary = Count + > + static typename std::enable_if<(sizeof...(Values) == Boundary), void>::type run( + database_binder&, + Function&& function, + Values&&... values + ) { + function(std::move(values)...); + } + }; + + // int + inline database_binder& operator<<(database_binder& db, const int& val) { + int hresult; + if((hresult = sqlite3_bind_int(db._stmt.get(), db._next_index(), val)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + return db; + } + inline void store_result_in_db(sqlite3_context* db, const int& val) { + sqlite3_result_int(db, val); + } + inline void get_col_from_db(database_binder& db, int inx, int& val) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + val = 0; + } else { + val = sqlite3_column_int(db._stmt.get(), inx); + } + } + inline void get_val_from_db(sqlite3_value *value, int& val) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + val = 0; + } else { + val = sqlite3_value_int(value); + } + } + + // sqlite_int64 + inline database_binder& operator <<(database_binder& db, const sqlite_int64& val) { + int hresult; + if((hresult = sqlite3_bind_int64(db._stmt.get(), db._next_index(), val)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + + return db; + } + inline void store_result_in_db(sqlite3_context* db, const sqlite_int64& val) { + sqlite3_result_int64(db, val); + } + inline void get_col_from_db(database_binder& db, int inx, sqlite3_int64& i) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + i = 0; + } else { + i = sqlite3_column_int64(db._stmt.get(), inx); + } + } + inline void get_val_from_db(sqlite3_value *value, sqlite3_int64& i) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + i = 0; + } else { + i = sqlite3_value_int64(value); + } + } + + // float + inline database_binder& operator <<(database_binder& db, const float& val) { + int hresult; + if((hresult = sqlite3_bind_double(db._stmt.get(), db._next_index(), double(val))) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + + return db; + } + inline void store_result_in_db(sqlite3_context* db, const float& val) { + sqlite3_result_double(db, val); + } + inline void get_col_from_db(database_binder& db, int inx, float& f) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + f = 0; + } else { + f = float(sqlite3_column_double(db._stmt.get(), inx)); + } + } + inline void get_val_from_db(sqlite3_value *value, float& f) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + f = 0; + } else { + f = float(sqlite3_value_double(value)); + } + } + + // double + inline database_binder& operator <<(database_binder& db, const double& val) { + int hresult; + if((hresult = sqlite3_bind_double(db._stmt.get(), db._next_index(), val)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + + return db; + } + inline void store_result_in_db(sqlite3_context* db, const double& val) { + sqlite3_result_double(db, val); + } + inline void get_col_from_db(database_binder& db, int inx, double& d) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + d = 0; + } else { + d = sqlite3_column_double(db._stmt.get(), inx); + } + } + inline void get_val_from_db(sqlite3_value *value, double& d) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + d = 0; + } else { + d = sqlite3_value_double(value); + } + } + + // vector + template inline database_binder& operator<<(database_binder& db, const std::vector& vec) { + void const* buf = reinterpret_cast(vec.data()); + int bytes = vec.size() * sizeof(T); + int hresult; + if((hresult = sqlite3_bind_blob(db._stmt.get(), db._next_index(), buf, bytes, SQLITE_TRANSIENT)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + return db; + } + template inline void store_result_in_db(sqlite3_context* db, const std::vector& vec) { + void const* buf = reinterpret_cast(vec.data()); + int bytes = vec.size() * sizeof(T); + sqlite3_result_blob(db, buf, bytes, SQLITE_TRANSIENT); + } + template inline void get_col_from_db(database_binder& db, int inx, std::vector& vec) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + vec.clear(); + } else { + int bytes = sqlite3_column_bytes(db._stmt.get(), inx); + T const* buf = reinterpret_cast(sqlite3_column_blob(db._stmt.get(), inx)); + vec = std::vector(buf, buf + bytes/sizeof(T)); + } + } + template inline void get_val_from_db(sqlite3_value *value, std::vector& vec) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + vec.clear(); + } else { + int bytes = sqlite3_value_bytes(value); + T const* buf = reinterpret_cast(sqlite3_value_blob(value)); + vec = std::vector(buf, buf + bytes/sizeof(T)); + } + } + + /* for nullptr support */ + inline database_binder& operator <<(database_binder& db, std::nullptr_t) { + int hresult; + if((hresult = sqlite3_bind_null(db._stmt.get(), db._next_index())) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + return db; + } + inline void store_result_in_db(sqlite3_context* db, std::nullptr_t) { + sqlite3_result_null(db); + } + /* for nullptr support */ + template inline database_binder& operator <<(database_binder& db, const std::unique_ptr& val) { + if(val) + db << *val; + else + db << nullptr; + return db; + } + + /* for unique_ptr support */ + template inline void get_col_from_db(database_binder& db, int inx, std::unique_ptr& _ptr_) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + _ptr_ = nullptr; + } else { + auto underling_ptr = new T(); + get_col_from_db(db, inx, *underling_ptr); + _ptr_.reset(underling_ptr); + } + } + template inline void get_val_from_db(sqlite3_value *value, std::unique_ptr& _ptr_) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + _ptr_ = nullptr; + } else { + auto underling_ptr = new T(); + get_val_from_db(value, *underling_ptr); + _ptr_.reset(underling_ptr); + } + } + + // std::string + inline void get_col_from_db(database_binder& db, int inx, std::string & s) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + s = std::string(); + } else { + sqlite3_column_bytes(db._stmt.get(), inx); + s = std::string(reinterpret_cast(sqlite3_column_text(db._stmt.get(), inx))); + } + } + inline void get_val_from_db(sqlite3_value *value, std::string & s) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + s = std::string(); + } else { + sqlite3_value_bytes(value); + s = std::string(reinterpret_cast(sqlite3_value_text(value))); + } + } + + // Convert char* to string to trigger op<<(..., const std::string ) + template inline database_binder& operator <<(database_binder& db, const char(&STR)[N]) { return db << std::string(STR); } + template inline database_binder& operator <<(database_binder& db, const char16_t(&STR)[N]) { return db << std::u16string(STR); } + + inline database_binder& operator <<(database_binder& db, const std::string& txt) { + int hresult; + if((hresult = sqlite3_bind_text(db._stmt.get(), db._next_index(), txt.data(), -1, SQLITE_TRANSIENT)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + + return db; + } + inline void store_result_in_db(sqlite3_context* db, const std::string& val) { + sqlite3_result_text(db, val.data(), -1, SQLITE_TRANSIENT); + } + // std::u16string + inline void get_col_from_db(database_binder& db, int inx, std::u16string & w) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + w = std::u16string(); + } else { + sqlite3_column_bytes16(db._stmt.get(), inx); + w = std::u16string(reinterpret_cast(sqlite3_column_text16(db._stmt.get(), inx))); + } + } + inline void get_val_from_db(sqlite3_value *value, std::u16string & w) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + w = std::u16string(); + } else { + sqlite3_value_bytes16(value); + w = std::u16string(reinterpret_cast(sqlite3_value_text16(value))); + } + } + + + inline database_binder& operator <<(database_binder& db, const std::u16string& txt) { + int hresult; + if((hresult = sqlite3_bind_text16(db._stmt.get(), db._next_index(), txt.data(), -1, SQLITE_TRANSIENT)) != SQLITE_OK) { + errors::throw_sqlite_error(hresult, db.sql()); + } + + return db; + } + inline void store_result_in_db(sqlite3_context* db, const std::u16string& val) { + sqlite3_result_text16(db, val.data(), -1, SQLITE_TRANSIENT); + } + + // Other integer types + template::value>::type> + inline database_binder& operator <<(database_binder& db, const Integral& val) { + return db << static_cast(val); + } + template::type>> + inline void store_result_in_db(sqlite3_context* db, const Integral& val) { + store_result_in_db(db, static_cast(val)); + } + template::value>::type> + inline void get_col_from_db(database_binder& db, int inx, Integral& val) { + sqlite3_int64 i; + get_col_from_db(db, inx, i); + val = i; + } + template::value>::type> + inline void get_val_from_db(sqlite3_value *value, Integral& val) { + sqlite3_int64 i; + get_val_from_db(value, i); + val = i; + } + + // std::optional support for NULL values +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template inline database_binder& operator <<(database_binder& db, const optional& val) { + if(val) { + return db << std::move(*val); + } else { + return db << nullptr; + } + } + template inline void store_result_in_db(sqlite3_context* db, const optional& val) { + if(val) { + store_result_in_db(db, *val); + } + sqlite3_result_null(db); + } + + template inline void get_col_from_db(database_binder& db, int inx, optional& o) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + #ifdef MODERN_SQLITE_EXPERIMENTAL_OPTIONAL_SUPPORT + o = std::experimental::nullopt; + #else + o.reset(); + #endif + } else { + OptionalT v; + get_col_from_db(db, inx, v); + o = std::move(v); + } + } + template inline void get_val_from_db(sqlite3_value *value, optional& o) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + #ifdef MODERN_SQLITE_EXPERIMENTAL_OPTIONAL_SUPPORT + o = std::experimental::nullopt; + #else + o.reset(); + #endif + } else { + OptionalT v; + get_val_from_db(value, v); + o = std::move(v); + } + } +#endif + + // boost::optional support for NULL values +#ifdef _MODERN_SQLITE_BOOST_OPTIONAL_SUPPORT + template inline database_binder& operator <<(database_binder& db, const boost::optional& val) { + if(val) { + return db << std::move(*val); + } else { + return db << nullptr; + } + } + template inline void store_result_in_db(sqlite3_context* db, const boost::optional& val) { + if(val) { + store_result_in_db(db, *val); + } + sqlite3_result_null(db); + } + + template inline void get_col_from_db(database_binder& db, int inx, boost::optional& o) { + if(sqlite3_column_type(db._stmt.get(), inx) == SQLITE_NULL) { + o.reset(); + } else { + BoostOptionalT v; + get_col_from_db(db, inx, v); + o = std::move(v); + } + } + template inline void get_val_from_db(sqlite3_value *value, boost::optional& o) { + if(sqlite3_value_type(value) == SQLITE_NULL) { + o.reset(); + } else { + BoostOptionalT v; + get_val_from_db(value, v); + o = std::move(v); + } + } +#endif + +#ifdef MODERN_SQLITE_STD_VARIANT_SUPPORT + template inline database_binder& operator <<(database_binder& db, const std::variant& val) { + std::visit([&](auto &&opt) {db << std::forward(opt);}, val); + return db; + } + template inline void store_result_in_db(sqlite3_context* db, const std::variant& val) { + std::visit([&](auto &&opt) {store_result_in_db(db, std::forward(opt));}, val); + } + template inline void get_col_from_db(database_binder& db, int inx, std::variant& val) { + utility::variant_select(sqlite3_column_type(db._stmt.get(), inx))([&](auto v) { + get_col_from_db(db, inx, v); + val = std::move(v); + }); + } + template inline void get_val_from_db(sqlite3_value *value, std::variant& val) { + utility::variant_select(sqlite3_value_type(value))([&](auto v) { + get_val_from_db(value, v); + val = std::move(v); + }); + } +#endif + + // Some ppl are lazy so we have a operator for proper prep. statemant handling. + void inline operator++(database_binder& db, int) { db.execute(); } + + // Convert the rValue binder to a reference and call first op<<, its needed for the call that creates the binder (be carefull of recursion here!) + template database_binder&& operator << (database_binder&& db, const T& val) { db << val; return std::move(db); } + + namespace sql_function_binder { + template + struct AggregateCtxt { + T obj; + bool constructed = true; + }; + + template< + typename ContextType, + std::size_t Count, + typename Functions + > + inline void step( + sqlite3_context* db, + int count, + sqlite3_value** vals + ) { + auto ctxt = static_cast*>(sqlite3_aggregate_context(db, sizeof(AggregateCtxt))); + if(!ctxt) return; + try { + if(!ctxt->constructed) new(ctxt) AggregateCtxt(); + step(db, count, vals, ctxt->obj); + return; + } catch(sqlite_exception &e) { + sqlite3_result_error_code(db, e.get_code()); + sqlite3_result_error(db, e.what(), -1); + } catch(std::exception &e) { + sqlite3_result_error(db, e.what(), -1); + } catch(...) { + sqlite3_result_error(db, "Unknown error", -1); + } + if(ctxt && ctxt->constructed) + ctxt->~AggregateCtxt(); + } + + template< + std::size_t Count, + typename Functions, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) && sizeof...(Values) < Count), void>::type step( + sqlite3_context* db, + int count, + sqlite3_value** vals, + Values&&... values + ) { + typename std::remove_cv< + typename std::remove_reference< + typename utility::function_traits< + typename Functions::first_type + >::template argument + >::type + >::type value{}; + get_val_from_db(vals[sizeof...(Values) - 1], value); + + step(db, count, vals, std::forward(values)..., std::move(value)); + } + + template< + std::size_t Count, + typename Functions, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) == Count), void>::type step( + sqlite3_context* db, + int, + sqlite3_value**, + Values&&... values + ) { + static_cast(sqlite3_user_data(db))->first(std::forward(values)...); + } + + template< + typename ContextType, + typename Functions + > + inline void final(sqlite3_context* db) { + auto ctxt = static_cast*>(sqlite3_aggregate_context(db, sizeof(AggregateCtxt))); + try { + if(!ctxt) return; + if(!ctxt->constructed) new(ctxt) AggregateCtxt(); + store_result_in_db(db, + static_cast(sqlite3_user_data(db))->second(ctxt->obj)); + } catch(sqlite_exception &e) { + sqlite3_result_error_code(db, e.get_code()); + sqlite3_result_error(db, e.what(), -1); + } catch(std::exception &e) { + sqlite3_result_error(db, e.what(), -1); + } catch(...) { + sqlite3_result_error(db, "Unknown error", -1); + } + if(ctxt && ctxt->constructed) + ctxt->~AggregateCtxt(); + } + + template< + std::size_t Count, + typename Function, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) < Count), void>::type scalar( + sqlite3_context* db, + int count, + sqlite3_value** vals, + Values&&... values + ) { + typename std::remove_cv< + typename std::remove_reference< + typename utility::function_traits::template argument + >::type + >::type value{}; + get_val_from_db(vals[sizeof...(Values)], value); + + scalar(db, count, vals, std::forward(values)..., std::move(value)); + } + + template< + std::size_t Count, + typename Function, + typename... Values + > + inline typename std::enable_if<(sizeof...(Values) == Count), void>::type scalar( + sqlite3_context* db, + int, + sqlite3_value**, + Values&&... values + ) { + try { + store_result_in_db(db, + (*static_cast(sqlite3_user_data(db)))(std::forward(values)...)); + } catch(sqlite_exception &e) { + sqlite3_result_error_code(db, e.get_code()); + sqlite3_result_error(db, e.what(), -1); + } catch(std::exception &e) { + sqlite3_result_error(db, e.what(), -1); + } catch(...) { + sqlite3_result_error(db, "Unknown error", -1); + } + } + } +} diff --git a/database/hdr/sqlite_modern_cpp/errors.h b/database/hdr/sqlite_modern_cpp/errors.h new file mode 100644 index 0000000..3460bbf --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/errors.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +#include + +namespace sqlite { + + class sqlite_exception: public std::runtime_error { + public: + sqlite_exception(const char* msg, std::string sql, int code = -1): runtime_error(msg), code(code), sql(sql) {} + sqlite_exception(int code, std::string sql): runtime_error(sqlite3_errstr(code)), code(code), sql(sql) {} + int get_code() const {return code & 0xFF;} + int get_extended_code() const {return code;} + std::string get_sql() const {return sql;} + private: + int code; + std::string sql; + }; + + namespace errors { + //One more or less trivial derived error class for each SQLITE error. + //Note the following are not errors so have no classes: + //SQLITE_OK, SQLITE_NOTICE, SQLITE_WARNING, SQLITE_ROW, SQLITE_DONE + // + //Note these names are exact matches to the names of the SQLITE error codes. +#define SQLITE_MODERN_CPP_ERROR_CODE(NAME,name,derived) \ + class name: public sqlite_exception { using sqlite_exception::sqlite_exception; };\ + derived +#define SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(BASE,SUB,base,sub) \ + class base ## _ ## sub: public base { using base::base; }; +#include "lists/error_codes.h" +#undef SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED +#undef SQLITE_MODERN_CPP_ERROR_CODE + + //Some additional errors are here for the C++ interface + class more_rows: public sqlite_exception { using sqlite_exception::sqlite_exception; }; + class no_rows: public sqlite_exception { using sqlite_exception::sqlite_exception; }; + class more_statements: public sqlite_exception { using sqlite_exception::sqlite_exception; }; // Prepared statements can only contain one statement + class invalid_utf16: public sqlite_exception { using sqlite_exception::sqlite_exception; }; + + static void throw_sqlite_error(const int& error_code, const std::string &sql = "") { + switch(error_code & 0xFF) { +#define SQLITE_MODERN_CPP_ERROR_CODE(NAME,name,derived) \ + case SQLITE_ ## NAME: switch(error_code) { \ + derived \ + default: throw name(error_code, sql); \ + } +#define SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(BASE,SUB,base,sub) \ + case SQLITE_ ## BASE ## _ ## SUB: throw base ## _ ## sub(error_code, sql); +#include "lists/error_codes.h" +#undef SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED +#undef SQLITE_MODERN_CPP_ERROR_CODE + default: throw sqlite_exception(error_code, sql); + } + } + } + namespace exceptions = errors; +} diff --git a/database/hdr/sqlite_modern_cpp/lists/error_codes.h b/database/hdr/sqlite_modern_cpp/lists/error_codes.h new file mode 100644 index 0000000..5dfa0d3 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/lists/error_codes.h @@ -0,0 +1,93 @@ +#if SQLITE_VERSION_NUMBER < 3010000 +#define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8)) +#define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8)) +#define SQLITE_AUTH_USER (SQLITE_AUTH | (1<<8)) +#endif +SQLITE_MODERN_CPP_ERROR_CODE(ERROR,error,) +SQLITE_MODERN_CPP_ERROR_CODE(INTERNAL,internal,) +SQLITE_MODERN_CPP_ERROR_CODE(PERM,perm,) +SQLITE_MODERN_CPP_ERROR_CODE(ABORT,abort, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(ABORT,ROLLBACK,abort,rollback) +) +SQLITE_MODERN_CPP_ERROR_CODE(BUSY,busy, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(BUSY,RECOVERY,busy,recovery) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(BUSY,SNAPSHOT,busy,snapshot) +) +SQLITE_MODERN_CPP_ERROR_CODE(LOCKED,locked, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(LOCKED,SHAREDCACHE,locked,sharedcache) +) +SQLITE_MODERN_CPP_ERROR_CODE(NOMEM,nomem,) +SQLITE_MODERN_CPP_ERROR_CODE(READONLY,readonly,) +SQLITE_MODERN_CPP_ERROR_CODE(INTERRUPT,interrupt,) +SQLITE_MODERN_CPP_ERROR_CODE(IOERR,ioerr, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,READ,ioerr,read) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SHORT_READ,ioerr,short_read) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,WRITE,ioerr,write) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,FSYNC,ioerr,fsync) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,DIR_FSYNC,ioerr,dir_fsync) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,TRUNCATE,ioerr,truncate) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,FSTAT,ioerr,fstat) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,UNLOCK,ioerr,unlock) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,RDLOCK,ioerr,rdlock) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,DELETE,ioerr,delete) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,BLOCKED,ioerr,blocked) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,NOMEM,ioerr,nomem) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,ACCESS,ioerr,access) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,CHECKRESERVEDLOCK,ioerr,checkreservedlock) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,LOCK,ioerr,lock) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,CLOSE,ioerr,close) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,DIR_CLOSE,ioerr,dir_close) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SHMOPEN,ioerr,shmopen) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SHMSIZE,ioerr,shmsize) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SHMLOCK,ioerr,shmlock) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SHMMAP,ioerr,shmmap) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,SEEK,ioerr,seek) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,DELETE_NOENT,ioerr,delete_noent) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,MMAP,ioerr,mmap) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,GETTEMPPATH,ioerr,gettemppath) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,CONVPATH,ioerr,convpath) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,VNODE,ioerr,vnode) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(IOERR,AUTH,ioerr,auth) +) +SQLITE_MODERN_CPP_ERROR_CODE(CORRUPT,corrupt, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CORRUPT,VTAB,corrupt,vtab) +) +SQLITE_MODERN_CPP_ERROR_CODE(NOTFOUND,notfound,) +SQLITE_MODERN_CPP_ERROR_CODE(FULL,full,) +SQLITE_MODERN_CPP_ERROR_CODE(CANTOPEN,cantopen, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CANTOPEN,NOTEMPDIR,cantopen,notempdir) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CANTOPEN,ISDIR,cantopen,isdir) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CANTOPEN,FULLPATH,cantopen,fullpath) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CANTOPEN,CONVPATH,cantopen,convpath) +) +SQLITE_MODERN_CPP_ERROR_CODE(PROTOCOL,protocol,) +SQLITE_MODERN_CPP_ERROR_CODE(EMPTY,empty,) +SQLITE_MODERN_CPP_ERROR_CODE(SCHEMA,schema,) +SQLITE_MODERN_CPP_ERROR_CODE(TOOBIG,toobig,) +SQLITE_MODERN_CPP_ERROR_CODE(CONSTRAINT,constraint, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,CHECK,constraint,check) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,COMMITHOOK,constraint,commithook) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,FOREIGNKEY,constraint,foreignkey) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,FUNCTION,constraint,function) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,NOTNULL,constraint,notnull) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,PRIMARYKEY,constraint,primarykey) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,TRIGGER,constraint,trigger) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,UNIQUE,constraint,unique) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,VTAB,constraint,vtab) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(CONSTRAINT,ROWID,constraint,rowid) +) +SQLITE_MODERN_CPP_ERROR_CODE(MISMATCH,mismatch,) +SQLITE_MODERN_CPP_ERROR_CODE(MISUSE,misuse,) +SQLITE_MODERN_CPP_ERROR_CODE(NOLFS,nolfs,) +SQLITE_MODERN_CPP_ERROR_CODE(AUTH,auth, +) +SQLITE_MODERN_CPP_ERROR_CODE(FORMAT,format,) +SQLITE_MODERN_CPP_ERROR_CODE(RANGE,range,) +SQLITE_MODERN_CPP_ERROR_CODE(NOTADB,notadb,) +SQLITE_MODERN_CPP_ERROR_CODE(NOTICE,notice, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(NOTICE,RECOVER_WAL,notice,recover_wal) + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(NOTICE,RECOVER_ROLLBACK,notice,recover_rollback) +) +SQLITE_MODERN_CPP_ERROR_CODE(WARNING,warning, + SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(WARNING,AUTOINDEX,warning,autoindex) +) diff --git a/database/hdr/sqlite_modern_cpp/log.h b/database/hdr/sqlite_modern_cpp/log.h new file mode 100644 index 0000000..3920742 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/log.h @@ -0,0 +1,101 @@ +#include "errors.h" + +#include + +#include +#include +#include + +namespace sqlite { + namespace detail { + template + using void_t = void; + template + struct is_callable : std::false_type {}; + template + struct is_callable()(std::declval()...))>> : std::true_type {}; + template + class FunctorOverload: public Functor, public FunctorOverload { + public: + template + FunctorOverload(Functor1 &&functor, Remaining &&... remaining): + Functor(std::forward(functor)), + FunctorOverload(std::forward(remaining)...) {} + using Functor::operator(); + using FunctorOverload::operator(); + }; + template + class FunctorOverload: public Functor { + public: + template + FunctorOverload(Functor1 &&functor): + Functor(std::forward(functor)) {} + using Functor::operator(); + }; + template + class WrapIntoFunctor: public Functor { + public: + template + WrapIntoFunctor(Functor1 &&functor): + Functor(std::forward(functor)) {} + using Functor::operator(); + }; + template + class WrapIntoFunctor { + ReturnType(*ptr)(Arguments...); + public: + WrapIntoFunctor(ReturnType(*ptr)(Arguments...)): ptr(ptr) {} + ReturnType operator()(Arguments... arguments) { return (*ptr)(std::forward(arguments)...); } + }; + inline void store_error_log_data_pointer(std::shared_ptr ptr) { + static std::shared_ptr stored; + stored = std::move(ptr); + } + template + std::shared_ptr::type> make_shared_inferred(T &&t) { + return std::make_shared::type>(std::forward(t)); + } + } + template + typename std::enable_if::value>::type + error_log(Handler &&handler); + template + typename std::enable_if::value>::type + error_log(Handler &&handler); + template + typename std::enable_if=2>::type + error_log(Handler &&...handler) { + return error_log(detail::FunctorOverload::type>...>(std::forward(handler)...)); + } + template + typename std::enable_if::value>::type + error_log(Handler &&handler) { + return error_log(std::forward(handler), [](const sqlite_exception&) {}); + } + template + typename std::enable_if::value>::type + error_log(Handler &&handler) { + auto ptr = detail::make_shared_inferred([handler = std::forward(handler)](int error_code, const char *errstr) mutable { + switch(error_code & 0xFF) { +#define SQLITE_MODERN_CPP_ERROR_CODE(NAME,name,derived) \ + case SQLITE_ ## NAME: switch(error_code) { \ + derived \ + default: handler(errors::name(errstr, "", error_code)); \ + };break; +#define SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED(BASE,SUB,base,sub) \ + case SQLITE_ ## BASE ## _ ## SUB: \ + handler(errors::base ## _ ## sub(errstr, "", error_code)); \ + break; +#include "lists/error_codes.h" +#undef SQLITE_MODERN_CPP_ERROR_CODE_EXTENDED +#undef SQLITE_MODERN_CPP_ERROR_CODE + default: handler(sqlite_exception(errstr, "", error_code)); \ + } + }); + + sqlite3_config(SQLITE_CONFIG_LOG, (void(*)(void*,int,const char*))[](void *functor, int error_code, const char *errstr) { + (*static_cast(functor))(error_code, errstr); + }, ptr.get()); + detail::store_error_log_data_pointer(std::move(ptr)); + } +} diff --git a/database/hdr/sqlite_modern_cpp/sqlcipher.h b/database/hdr/sqlite_modern_cpp/sqlcipher.h new file mode 100644 index 0000000..da0f018 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/sqlcipher.h @@ -0,0 +1,44 @@ +#pragma once + +#ifndef SQLITE_HAS_CODEC +#define SQLITE_HAS_CODEC +#endif + +#include "../sqlite_modern_cpp.h" + +namespace sqlite { + struct sqlcipher_config : public sqlite_config { + std::string key; + }; + + class sqlcipher_database : public database { + public: + sqlcipher_database(std::string db, const sqlcipher_config &config): database(db, config) { + set_key(config.key); + } + + sqlcipher_database(std::u16string db, const sqlcipher_config &config): database(db, config) { + set_key(config.key); + } + + void set_key(const std::string &key) { + if(auto ret = sqlite3_key(_db.get(), key.data(), key.size())) + errors::throw_sqlite_error(ret); + } + + void set_key(const std::string &key, const std::string &db_name) { + if(auto ret = sqlite3_key_v2(_db.get(), db_name.c_str(), key.data(), key.size())) + errors::throw_sqlite_error(ret); + } + + void rekey(const std::string &new_key) { + if(auto ret = sqlite3_rekey(_db.get(), new_key.data(), new_key.size())) + errors::throw_sqlite_error(ret); + } + + void rekey(const std::string &new_key, const std::string &db_name) { + if(auto ret = sqlite3_rekey_v2(_db.get(), db_name.c_str(), new_key.data(), new_key.size())) + errors::throw_sqlite_error(ret); + } + }; +} diff --git a/database/hdr/sqlite_modern_cpp/utility/function_traits.h b/database/hdr/sqlite_modern_cpp/utility/function_traits.h new file mode 100644 index 0000000..855a51c --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/function_traits.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +namespace sqlite { + namespace utility { + + template struct function_traits; + + template + struct function_traits : public function_traits< + decltype(&std::remove_reference::type::operator()) + > { }; + + template < + typename ClassType, + typename ReturnType, + typename... Arguments + > + struct function_traits< + ReturnType(ClassType::*)(Arguments...) const + > : function_traits { }; + + /* support the non-const operator () + * this will work with user defined functors */ + template < + typename ClassType, + typename ReturnType, + typename... Arguments + > + struct function_traits< + ReturnType(ClassType::*)(Arguments...) + > : function_traits { }; + + template < + typename ReturnType, + typename... Arguments + > + struct function_traits< + ReturnType(*)(Arguments...) + > { + typedef ReturnType result_type; + + template + using argument = typename std::tuple_element< + Index, + std::tuple + >::type; + + static const std::size_t arity = sizeof...(Arguments); + }; + + } +} diff --git a/database/hdr/sqlite_modern_cpp/utility/uncaught_exceptions.h b/database/hdr/sqlite_modern_cpp/utility/uncaught_exceptions.h new file mode 100644 index 0000000..17d6326 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/uncaught_exceptions.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +namespace sqlite { + namespace utility { +#ifdef __cpp_lib_uncaught_exceptions + class UncaughtExceptionDetector { + public: + operator bool() { + return count != std::uncaught_exceptions(); + } + private: + int count = std::uncaught_exceptions(); + }; +#else + class UncaughtExceptionDetector { + public: + operator bool() { + return std::uncaught_exception(); + } + }; +#endif + } +} diff --git a/database/hdr/sqlite_modern_cpp/utility/utf16_utf8.h b/database/hdr/sqlite_modern_cpp/utility/utf16_utf8.h new file mode 100644 index 0000000..5bcdce6 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/utf16_utf8.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#include "../errors.h" + +namespace sqlite { + namespace utility { + inline std::string utf16_to_utf8(const std::u16string &input) { + struct : std::codecvt { + } codecvt; + std::mbstate_t state{}; + std::string result((std::max)(input.size() * 3 / 2, std::size_t(4)), '\0'); + const char16_t *remaining_input = input.data(); + std::size_t produced_output = 0; + while(true) { + char *used_output; + switch(codecvt.out(state, remaining_input, &input[input.size()], + remaining_input, &result[produced_output], + &result[result.size() - 1] + 1, used_output)) { + case std::codecvt_base::ok: + result.resize(used_output - result.data()); + return result; + case std::codecvt_base::noconv: + // This should be unreachable + case std::codecvt_base::error: + throw errors::invalid_utf16("Invalid UTF-16 input", ""); + case std::codecvt_base::partial: + if(used_output == result.data() + produced_output) + throw errors::invalid_utf16("Unexpected end of input", ""); + produced_output = used_output - result.data(); + result.resize( + result.size() + + (std::max)((&input[input.size()] - remaining_input) * 3 / 2, + std::ptrdiff_t(4))); + } + } + } + } // namespace utility +} // namespace sqlite diff --git a/database/hdr/sqlite_modern_cpp/utility/variant.h b/database/hdr/sqlite_modern_cpp/utility/variant.h new file mode 100644 index 0000000..11a8429 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/variant.h @@ -0,0 +1,201 @@ +#pragma once + +#include "../errors.h" +#include +#include +#include + +namespace sqlite::utility { + template + struct VariantFirstNullable { + using type = void; + }; + template + struct VariantFirstNullable { + using type = typename VariantFirstNullable::type; + }; +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template + struct VariantFirstNullable, Options...> { + using type = std::optional; + }; +#endif + template + struct VariantFirstNullable, Options...> { + using type = std::unique_ptr; + }; + template + struct VariantFirstNullable { + using type = std::nullptr_t; + }; + template + inline void variant_select_null(Callback&&callback) { + if constexpr(std::is_same_v::type, void>) { + throw errors::mismatch("NULL is unsupported by this variant.", "", SQLITE_MISMATCH); + } else { + std::forward(callback)(typename VariantFirstNullable::type()); + } + } + + template + struct VariantFirstIntegerable { + using type = void; + }; + template + struct VariantFirstIntegerable { + using type = typename VariantFirstIntegerable::type; + }; +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template + struct VariantFirstIntegerable, Options...> { + using type = std::conditional_t::type, T>, std::optional, typename VariantFirstIntegerable::type>; + }; +#endif + template + struct VariantFirstIntegerable::type, T>>, std::unique_ptr, Options...> { + using type = std::conditional_t::type, T>, std::unique_ptr, typename VariantFirstIntegerable::type>; + }; + template + struct VariantFirstIntegerable { + using type = int; + }; + template + struct VariantFirstIntegerable { + using type = sqlite_int64; + }; + template + inline auto variant_select_integer(Callback&&callback) { + if constexpr(std::is_same_v::type, void>) { + throw errors::mismatch("Integer is unsupported by this variant.", "", SQLITE_MISMATCH); + } else { + std::forward(callback)(typename VariantFirstIntegerable::type()); + } + } + + template + struct VariantFirstFloatable { + using type = void; + }; + template + struct VariantFirstFloatable { + using type = typename VariantFirstFloatable::type; + }; +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template + struct VariantFirstFloatable, Options...> { + using type = std::conditional_t::type, T>, std::optional, typename VariantFirstFloatable::type>; + }; +#endif + template + struct VariantFirstFloatable, Options...> { + using type = std::conditional_t::type, T>, std::unique_ptr, typename VariantFirstFloatable::type>; + }; + template + struct VariantFirstFloatable { + using type = float; + }; + template + struct VariantFirstFloatable { + using type = double; + }; + template + inline auto variant_select_float(Callback&&callback) { + if constexpr(std::is_same_v::type, void>) { + throw errors::mismatch("Real is unsupported by this variant.", "", SQLITE_MISMATCH); + } else { + std::forward(callback)(typename VariantFirstFloatable::type()); + } + } + + template + struct VariantFirstTextable { + using type = void; + }; + template + struct VariantFirstTextable { + using type = typename VariantFirstTextable::type; + }; +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template + struct VariantFirstTextable, Options...> { + using type = std::conditional_t::type, T>, std::optional, typename VariantFirstTextable::type>; + }; +#endif + template + struct VariantFirstTextable, Options...> { + using type = std::conditional_t::type, T>, std::unique_ptr, typename VariantFirstTextable::type>; + }; + template + struct VariantFirstTextable { + using type = std::string; + }; + template + struct VariantFirstTextable { + using type = std::u16string; + }; + template + inline void variant_select_text(Callback&&callback) { + if constexpr(std::is_same_v::type, void>) { + throw errors::mismatch("Text is unsupported by this variant.", "", SQLITE_MISMATCH); + } else { + std::forward(callback)(typename VariantFirstTextable::type()); + } + } + + template + struct VariantFirstBlobable { + using type = void; + }; + template + struct VariantFirstBlobable { + using type = typename VariantFirstBlobable::type; + }; +#ifdef MODERN_SQLITE_STD_OPTIONAL_SUPPORT + template + struct VariantFirstBlobable, Options...> { + using type = std::conditional_t::type, T>, std::optional, typename VariantFirstBlobable::type>; + }; +#endif + template + struct VariantFirstBlobable, Options...> { + using type = std::conditional_t::type, T>, std::unique_ptr, typename VariantFirstBlobable::type>; + }; + template + struct VariantFirstBlobable>, std::vector, Options...> { + using type = std::vector; + }; + template + inline auto variant_select_blob(Callback&&callback) { + if constexpr(std::is_same_v::type, void>) { + throw errors::mismatch("Blob is unsupported by this variant.", "", SQLITE_MISMATCH); + } else { + std::forward(callback)(typename VariantFirstBlobable::type()); + } + } + + template + inline auto variant_select(int type) { + return [type](auto &&callback) { + using Callback = decltype(callback); + switch(type) { + case SQLITE_NULL: + variant_select_null(std::forward(callback)); + break; + case SQLITE_INTEGER: + variant_select_integer(std::forward(callback)); + break; + case SQLITE_FLOAT: + variant_select_float(std::forward(callback)); + break; + case SQLITE_TEXT: + variant_select_text(std::forward(callback)); + break; + case SQLITE_BLOB: + variant_select_blob(std::forward(callback)); + break; + default:; + /* assert(false); */ + } + }; + } +} diff --git a/database/pagedao.cpp b/database/pagedao.cpp new file mode 100644 index 0000000..42100f1 --- /dev/null +++ b/database/pagedao.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "pagedao.h" + +PageDao::PageDao() +{ + +} diff --git a/database/pagedao.h b/database/pagedao.h new file mode 100644 index 0000000..3e0608b --- /dev/null +++ b/database/pagedao.h @@ -0,0 +1,28 @@ +#ifndef PAGEDAO_H +#define PAGEDAO_H +#include +#include +#include +#include "queryoption.h" +#include "../page.h" +#include "../searchresult.h" +class PageDao +{ +public: + PageDao(); + virtual bool exists(std::string page) const = 0; + virtual bool exists(int id) const = 0; + virtual std::optional find(std::string name) = 0; + virtual std::optional find(int id) = 0; + virtual std::vector getPageList(QueryOption option) = 0; + virtual std::vector fetchCategories(std::string pagename, QueryOption option) = 0; + virtual void deletePage(std::string page) = 0; + virtual void save(const Page &page) = 0; + //TODO: this may not be the correct place for this. + virtual void setCategories(std::string pagename, const std::vector &catnames) = 0; + virtual std::vector search(std::string query, QueryOption option) = 0; + + virtual ~PageDao() { } +}; + +#endif // PAGEDAO_H diff --git a/database/pagedaosqlite.cpp b/database/pagedaosqlite.cpp new file mode 100644 index 0000000..0e8d64b --- /dev/null +++ b/database/pagedaosqlite.cpp @@ -0,0 +1,201 @@ + /* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include "pagedaosqlite.h" +#include "exceptions.h" +#include "sqlitequeryoption.h" +#include "../logger.h" + +/* TODO: copied from C version mostly, review whether access to table other than page is ok */ + +bool PageDaoSqlite::exists(int id) const +{ + auto binder = *db << "SELECT 1 from page WHERE id = ?" << id ; + return execBool(binder); +} + +bool PageDaoSqlite::exists(std::string name) const +{ + auto binder = *db << "SELECT 1 FROM page WHERE name = ?" << name; + return execBool(binder); +} + +std::optional PageDaoSqlite::find(std::string name) +{ + int pageid = fetchPageId(name); + return find(pageid); +} + +std::optional PageDaoSqlite::find(int id) +{ + Page result; + try + { + auto ps = *db << "SELECT name, lastrevision, visible FROM page WHERE id = ?"; + + ps << id >> std::tie(result.name, result.current_revision, result.listed); + } + catch(const sqlite::errors::no_rows &e) + { + return { }; + } + catch(sqlite::sqlite_exception& e) + { + throwFrom(e); + } + + return result; +} + +void PageDaoSqlite::deletePage(std::string page) +{ + int pageId = this->fetchPageId(page); + //TODO on delete cascade is better most certainly + try + { + *db << "BEGIN;"; + *db << "DELETE FROM revision WHERE page = ?;" << pageId; + *db << "DELETE FROM categorymember WHERE page = ?;" << pageId; + *db << "DELETE FROM permissions WHERE page = ?;" << pageId; + *db << "DELETE FROM page WHERE id =?;" << pageId; + *db << "COMMIT;"; + + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + + +} + +void PageDaoSqlite::save(const Page &page) +{ + try + { + *db << "INSERT INTO page (name, lastrevision, visible) VALUES(?, ?, ?)" << page.name << page.current_revision << page.listed; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} +std::vector PageDaoSqlite::getPageList(QueryOption option) +{ + std::vector result; + + try + { + + std::string queryOption = SqliteQueryOption(option).setOrderByColumn("lower(name)").setVisibleColumnName("visible").setPrependWhere(true).build(); + std::string query = "SELECT name FROM page " + queryOption; + + *db << query >> [&](std::string name) + { + result.push_back(name); + }; + } + catch(const sqlite::errors::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; +} + +std::vector PageDaoSqlite::fetchCategories(std::string pagename, QueryOption option) +{ + std::vector result; + try + { + auto query = *db << "SELECT name FROM categorymember INNNER JOIN category ON category = category.id WHERE page = (SELECT id FROM page WHERE name = ?)" << pagename; + query << " AND " << SqliteQueryOption(option).setPrependWhere(false).setOrderByColumn("name").build(); + query >> [&](std::string pagename) { result.push_back(pagename);}; + } + catch(const sqlite::exceptions::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + + return result; +} + +std::vector PageDaoSqlite::search(std::string name, QueryOption option) +{ + + std::vector result; + try + { + std::string qo = SqliteQueryOption(option).setPrependWhere(false).setOrderByColumn("rank").build(); + //TODO: what is passed here, simple gets thrown to the MATCH operator without escaping or anything and this is suboptimal + auto query = *db << "SELECT page.name FROM search INNER JOIN page ON search.page = page.id WHERE search MATCH ? " << name; + query >> [&](std::string pagename) { + SearchResult sresult; + sresult.pagename = pagename; + sresult.query = name; + result.push_back(sresult); + }; + } + catch(const sqlite::exceptions::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; +} + +void PageDaoSqlite::setCategories(std::string pagename, const std::vector &catnames) +{ + try + { + int pageid = fetchPageId(pagename); + *db << "savepoint setcategories;"; + *db << "DELETE FROM categorymember WHERE page = ?" << pageid; + for(const std::string &cat : catnames) + { + *db << "INSERT OR IGNORE INTO category (id, name) VALUES( (SELECT id FROM category WHERE lower(name) = lower(?)), lower(?))" << cat << cat; + *db << "INSERT INTO categorymember (category, page) VALUES ( (SELECT ID FROM category WHERE lower(name) = lower(?)), ?)" << cat << pageid; + + } + *db << "release setcategories;"; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + + +int PageDaoSqlite::fetchPageId(std::string pagename) +{ + auto binder = *db << "SELECT id FROM page WHERE name = ?" << pagename; + return execInt(binder); +} diff --git a/database/pagedaosqlite.h b/database/pagedaosqlite.h new file mode 100644 index 0000000..2d27aee --- /dev/null +++ b/database/pagedaosqlite.h @@ -0,0 +1,28 @@ +#ifndef PAGEDAOSQLITE_H +#define PAGEDAOSQLITE_H +#include +#include +#include +#include "../page.h" +#include "pagedao.h" +#include "sqlitedao.h" +class PageDaoSqlite : public PageDao, protected SqliteDao +{ +public: + PageDaoSqlite() { } + void deletePage(std::string page) override; + bool exists(int id) const override; + bool exists(std::string name) const override; + void save(const Page &page) override; + std::optional find(std::string name) override; + std::optional find(int id) override; + std::vector getPageList(QueryOption option) override; + std::vector fetchCategories(std::string pagename, QueryOption option) override; + using SqliteDao::SqliteDao; + int fetchPageId(std::string pagename); + std::vector search(std::string query, QueryOption option) override; + void setCategories(std::string pagename, const std::vector &catnames) override; + +}; + +#endif // PAGEDAOSQLITE_H diff --git a/database/permissionsdao.cpp b/database/permissionsdao.cpp new file mode 100644 index 0000000..dd6153d --- /dev/null +++ b/database/permissionsdao.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "permissionsdao.h" + +PermissionsDao::PermissionsDao() +{ + +} diff --git a/database/permissionsdao.h b/database/permissionsdao.h new file mode 100644 index 0000000..9b508bd --- /dev/null +++ b/database/permissionsdao.h @@ -0,0 +1,12 @@ +#ifndef PERMISSIONSDAO_H +#define PERMISSIONSDAO_H +#include "../permissions.h" +#include "../user.h" +class PermissionsDao +{ +public: + PermissionsDao(); + virtual Permissions find(std::string pagename, std::string username) = 0; +}; + +#endif // PERMISSIONSDAO_H diff --git a/database/permissionsdaosqlite.cpp b/database/permissionsdaosqlite.cpp new file mode 100644 index 0000000..1afcc83 --- /dev/null +++ b/database/permissionsdaosqlite.cpp @@ -0,0 +1,32 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "permissionsdaosqlite.h" + +PermissionsDaoSqlite::PermissionsDaoSqlite() +{ + +} + +Permissions PermissionsDaoSqlite::find(std::string pagename, std::string username) +{ + /* auto query = *db << "SELECT COALESCE( (SELECT permissions FROM permissions WHERE page = ? AND userid = ?), (SELECT permissions FROM user WHERE ID = ?))"; + exec*/ +} diff --git a/database/permissionsdaosqlite.h b/database/permissionsdaosqlite.h new file mode 100644 index 0000000..b7d38c1 --- /dev/null +++ b/database/permissionsdaosqlite.h @@ -0,0 +1,15 @@ +#ifndef PERMISSIONSDAOSQLITE_H +#define PERMISSIONSDAOSQLITE_H +#include "permissionsdao.h" +#include "sqlitedao.h" + +class PermissionsDaoSqlite : public PermissionsDao, protected SqliteDao +{ +public: + PermissionsDaoSqlite(); + + Permissions find(std::string pagename, std::string username) override; + using SqliteDao::SqliteDao; +}; + +#endif // PERMISSIONSDAOSQLITE_H diff --git a/database/queryoption.cpp b/database/queryoption.cpp new file mode 100644 index 0000000..38eb649 --- /dev/null +++ b/database/queryoption.cpp @@ -0,0 +1,22 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "queryoption.h" + diff --git a/database/queryoption.h b/database/queryoption.h new file mode 100644 index 0000000..699745c --- /dev/null +++ b/database/queryoption.h @@ -0,0 +1,19 @@ +#ifndef QUERYOPTION_H +#define QUERYOPTION_H + +enum SORT_ORDER +{ + ASCENDING=0, + DESCENDING +}; + +class QueryOption +{ +public: + unsigned int offset = 0; + unsigned int limit = 0; + SORT_ORDER order = ASCENDING; + bool includeInvisible = true; +}; + +#endif // QUERYOPTION_H diff --git a/database/revisiondao.cpp b/database/revisiondao.cpp new file mode 100644 index 0000000..8f7798d --- /dev/null +++ b/database/revisiondao.cpp @@ -0,0 +1,22 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "revisiondao.h" + diff --git a/database/revisiondao.h b/database/revisiondao.h new file mode 100644 index 0000000..b535ba9 --- /dev/null +++ b/database/revisiondao.h @@ -0,0 +1,22 @@ +#ifndef REVISIONDAO_H +#define REVISIONDAO_H +#include +#include +#include "../revision.h" +#include "queryoption.h" +class RevisionDao +{ +public: + virtual void save(const Revision &revision) = 0; + virtual std::vector getAllRevisions(QueryOption &options) = 0; + virtual std::vector getAllRevisionsForPage(std::string pagename, QueryOption &option) = 0; + virtual std::optional getCurrentForPage(std::string pagename) = 0; + virtual std::optional getRevisionForPage(std::string pagnename, unsigned int revision) = 0; + virtual unsigned int countTotalRevisions() = 0; + virtual unsigned int countTotalRevisions(std::string pagename) = 0; + + virtual ~RevisionDao() { } + +}; + +#endif // REVISIONDAO_H diff --git a/database/revisiondaosqlite.cpp b/database/revisiondaosqlite.cpp new file mode 100644 index 0000000..e30eeba --- /dev/null +++ b/database/revisiondaosqlite.cpp @@ -0,0 +1,168 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "revisiondaosqlite.h" +#include "exceptions.h" +#include "sqlitequeryoption.h" +#include "../utils.h" +RevisionDaoSqlite::RevisionDaoSqlite() +{ + +} + + + +void RevisionDaoSqlite::save(const Revision &revision) +{ + try + { + *db << "savepoint revisionsubmit;"; + *db << "INSERT INTO revision(author, comment, content, creationtime, page, revisionid) VALUES((SELECT id FROM user WHERE username = ?), ?, ?, DATETIME(), (SELECT id FROM page WHERE name = ?), (SELECT lastrevision+1 FROM page WHERE id = (SELECT id FROM page WHERE name = ?)));" << + revision.author << revision.comment << revision.content << revision.page << revision.page; + *db << "UPDATE page SET lastrevision=lastrevision+1 WHERE name = ?; " << revision.page; + *db << "release revisionsubmit;"; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + + +} + +std::vector RevisionDaoSqlite::getAllRevisions(QueryOption &options) +{ + std::vector result; + + try + { + SqliteQueryOption queryOption { options }; + std::string queryOptionSql = queryOption.setPrependWhere(true).setOrderByColumn("id").build(); + + auto query = *db << "SELECT author, comment, content, strftime('%s',creationtime), (SELECT name FROM page WHERE page.id = page ), revisionid FROM revision " + queryOptionSql; + query >> [&](std::string author, std::string comment, std::string content, time_t creationtime, std::string page, unsigned int revisionid) + { + Revision r; + r.author = author; + r.comment = comment; + r.content = content; + r.timestamp = creationtime; + r.page = page; + r.revision = revisionid; + result.push_back(r); + }; + } + catch(const sqlite::errors::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; +} + +std::vector RevisionDaoSqlite::getAllRevisionsForPage(std::string pagename, QueryOption &option) +{ + std::vector result; + + try + { + SqliteQueryOption queryOption { option }; + std::string queryOptionSql = queryOption.setPrependWhere(false).setOrderByColumn("id").build(); + + auto query = *db << "SELECT (SELECT username FROM user WHERE id = author), comment, content, strftime('%s',creationtime), (SELECT name FROM page WHERE page.id = page ), revisionid FROM revision WHERE page = (SELECT id FROM page WHERE name = ?) " + queryOptionSql << pagename; + + query >> [&](std::string author, std::string comment, std::string content, time_t creationtime, std::string page, unsigned int revisionid) + { + Revision r; + r.author = author; + r.comment = comment; + r.content = content; + r.timestamp = creationtime; + r.page = page; + r.revision = revisionid; + result.push_back(r); + }; + } + catch(const sqlite::errors::no_rows &e) + { + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; + +} + +std::optional RevisionDaoSqlite::getCurrentForPage(std::string pagename) +{ + Revision result; + try + { + auto query = *db << "SELECT (SELECT username FROM user WHERE id = author), comment, content, strftime('%s',creationtime), page, revisionid FROM revision WHERE page = (SELECT id FROM page WHERE name = ? ) AND revisionid = (SELECT lastrevision FROM page WHERE name = ?)"; + query << pagename << pagename; + query >> std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision); + } + catch(const sqlite::errors::no_rows &e) + { + return { }; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; +} + + +std::optional RevisionDaoSqlite::getRevisionForPage(std::string pagename, unsigned int revision) +{ + Revision result; + + + try + { + auto query = *db << "SELECT (SELECT username FROM user WHERE id = author), comment, content, strftime('%s',creationtime), page, revisionid FROM revision WHERE page = (SELECT id FROM page WHERE name = ? ) AND revisionid = ?"; + query << pagename << revision; + query >> std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision); + } + catch(const sqlite::exceptions::no_rows &e) + { + return { }; + } + return result; +} + +unsigned int RevisionDaoSqlite::countTotalRevisions() +{ + auto query = *db << "SELECT COUNT(ROWID) FROM revision"; + return static_cast(execInt(query)); +} + +unsigned int RevisionDaoSqlite::countTotalRevisions(std::string page) +{ + auto query = *db << "SELECT COUNT(ROWID) FROM revision WHERE page = (SELECT id FROM page WHERE name = ?)" << page; + return static_cast(execInt(query)); +} + diff --git a/database/revisiondaosqlite.h b/database/revisiondaosqlite.h new file mode 100644 index 0000000..11ed1d5 --- /dev/null +++ b/database/revisiondaosqlite.h @@ -0,0 +1,23 @@ +#ifndef REVISIONDAOSQLITE_H +#define REVISIONDAOSQLITE_H +#include +#include "revisiondao.h" +#include "sqlitedao.h" + +class RevisionDaoSqlite : public RevisionDao, protected SqliteDao +{ +public: + RevisionDaoSqlite(); + void save(const Revision &revision) override; + std::vector getAllRevisions(QueryOption &options) override; + std::vector getAllRevisionsForPage(std::string pagename, QueryOption &option) override; + std::optional getCurrentForPage(std::string pagename) override; + std::optional getRevisionForPage(std::string pagnename, unsigned int revision) override; + unsigned int countTotalRevisions() override; + unsigned int countTotalRevisions(std::string pagename) override; + using SqliteDao::SqliteDao; + + +}; + +#endif // REVISIONDAOSQLITE_H diff --git a/database/sessiondao.cpp b/database/sessiondao.cpp new file mode 100644 index 0000000..8e6ce54 --- /dev/null +++ b/database/sessiondao.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sessiondao.h" + +SessionDao::SessionDao() +{ + +} diff --git a/database/sessiondao.h b/database/sessiondao.h new file mode 100644 index 0000000..f5f3763 --- /dev/null +++ b/database/sessiondao.h @@ -0,0 +1,16 @@ +#ifndef SESSIONDAO_H +#define SESSIONDAO_H +#include +#include +#include "../session.h" +class SessionDao +{ +public: + SessionDao(); + virtual void save(const Session &session) = 0; + virtual std::optional find(std::string token) = 0; + virtual void deleteSession(std::string token) = 0; + virtual ~SessionDao() { } +}; + +#endif // SESSIONDAO_H diff --git a/database/sessiondaosqlite.cpp b/database/sessiondaosqlite.cpp new file mode 100644 index 0000000..85e2c57 --- /dev/null +++ b/database/sessiondaosqlite.cpp @@ -0,0 +1,97 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sessiondaosqlite.h" +#include "userdaosqlite.h" + +void SessionDaoSqlite::save(const Session &session) +{ + try + { + //TODO: we do not store creationtime + auto q = *db << "INSERT OR REPLACE INTO session(id, token, csrf_token, creationtime, userid) VALUES((SELECT id FROM session WHERE token = ?), ?, ?, DATETIME(), (SELECT id FROM user WHERE username = ?))"; + q << session.token << session.token << session.csrf_token << session.user.login; + q.execute(); + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +void SessionDaoSqlite::deleteSession(std::string token) +{ + try + { + auto stmt = *db << "DELETE FROM session WHERE token = ?" << token; + stmt.execute(); + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + +} + +std::optional SessionDaoSqlite::find(std::string token) +{ + Session result; + + try + { + std::string username; + auto q = *db << "SELECT userid, token, csrf_token, strftime('%s', creationtime) FROM session WHERE token = ?" << token; + int userid; + q >> std::tie(userid, result.token, result.csrf_token, result.creation_time); + + if(userid > -1) + { + UserDaoSqlite userDao { this-> db }; + auto u = userDao.find(userid); + if(u) + { + result.user = *u; + } + else + { + Logger::error() << "Session for non existent user"; + throw DatabaseQueryException("Session for non existent user"); + } + } + else + { + result.user = User::Anonymous(); + } + result.loggedIn = userid != -1; + + } + catch(const sqlite::exceptions::no_rows &e) + { + return { }; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } + return result; + +} + + diff --git a/database/sessiondaosqlite.h b/database/sessiondaosqlite.h new file mode 100644 index 0000000..3909e8a --- /dev/null +++ b/database/sessiondaosqlite.h @@ -0,0 +1,20 @@ +#ifndef SESSIONDAOSQLITE_H +#define SESSIONDAOSQLITE_H +#include "sessiondao.h" +#include "../session.h" +#include "sqlitedao.h" + + +class SessionDaoSqlite : public SessionDao, protected SqliteDao +{ +public: + SessionDaoSqlite(); + void save(const Session &session) override; + std::optional find(std::string token) override; + void deleteSession(std::string token) override; + using SqliteDao::SqliteDao; + + +}; + +#endif // SESSIONDAOSQLITE_H diff --git a/database/sqlite.cpp b/database/sqlite.cpp new file mode 100644 index 0000000..937c388 --- /dev/null +++ b/database/sqlite.cpp @@ -0,0 +1,91 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sqlite.h" +#include "../logger.h" +#include "pagedaosqlite.h" +#include "revisiondaosqlite.h" +#include "sessiondaosqlite.h" +#include "userdaosqlite.h" +#include "categorydaosqlite.h" +#include "exceptions.h" + +Sqlite::Sqlite(std::string path) : Database(path) +{ + this->db = std::make_shared(path); + + *db << "PRAGMA journal_mode=WAL;"; + + + +} + +std::unique_ptr Sqlite::createRevisionDao() const +{ + return create(); +} + +std::unique_ptr Sqlite::createPageDao() const +{ + return create(); +} + +std::unique_ptr Sqlite::createUserDao() const +{ + return create(); +} + + +std::unique_ptr Sqlite::createSessionDao() const +{ + return create(); +} + +std::unique_ptr Sqlite::createCategoryDao() const +{ + return create(); +} +void Sqlite::beginTransaction() +{ + if(!inTransaction) + { + *db << "begin;"; + inTransaction = true; + } +} + +void Sqlite::rollbackTransaction() +{ + if(inTransaction) + { + *db << "rollback;"; + inTransaction = false; + } + +} + +void Sqlite::commitTransaction() +{ + if(inTransaction) + { + *db << "commit;"; + inTransaction = false; + } +} diff --git a/database/sqlite.h b/database/sqlite.h new file mode 100644 index 0000000..97868b1 --- /dev/null +++ b/database/sqlite.h @@ -0,0 +1,31 @@ +#ifndef SQLITE_H +#define SQLITE_H +#include +#include +#include +#include "database.h" + +class Sqlite : public Database +{ +private: + bool inTransaction = false; + std::shared_ptr db; + + template std::unique_ptr create() const + { + return std::make_unique(db); + } +public: + Sqlite(std::string path); + std::unique_ptr createPageDao() const; + std::unique_ptr createRevisionDao() const; + std::unique_ptr createUserDao() const; + std::unique_ptr createSessionDao() const; + std::unique_ptr createCategoryDao() const; + void beginTransaction(); + void commitTransaction(); + void rollbackTransaction(); + +}; + +#endif // SQLITE_H diff --git a/database/sqlitedao.cpp b/database/sqlitedao.cpp new file mode 100644 index 0000000..8be75f2 --- /dev/null +++ b/database/sqlitedao.cpp @@ -0,0 +1,52 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sqlitedao.h" + + +bool SqliteDao::execBool(sqlite::database_binder &binder) const +{ + bool result; + try + { + bool result; + binder >> result; + return result; + } + catch(sqlite::sqlite_exception& e) + { + //TODO: well, we may want to check whether rows have found or not and thus log this here + return false; + } +} + +int SqliteDao::execInt(sqlite::database_binder &binder) const +{ + try + { + int result; + binder >> result; + return result; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} diff --git a/database/sqlitedao.h b/database/sqlitedao.h new file mode 100644 index 0000000..31e04d5 --- /dev/null +++ b/database/sqlitedao.h @@ -0,0 +1,36 @@ +#ifndef SQLITEDAO_H +#define SQLITEDAO_H +#include +#include +#include +#include +#include +#include "queryoption.h" +#include "exceptions.h" +#include "../logger.h" + +class SqliteDao +{ +protected: + std::shared_ptr db; +public: + SqliteDao() { } + + SqliteDao(std::shared_ptr db) { this->db = db; } + void setDb(std::shared_ptr db) { this->db = db; } + + inline void throwFrom(const sqlite::sqlite_exception &e) const + { + std::string msg = "Sqlite Error: " + std::to_string(e.get_code()) + " SQL: " + e.get_sql(); + Logger::error() << msg << " Extended code: " << e.get_extended_code(); + throw DatabaseQueryException(msg); + } + + bool execBool(sqlite::database_binder &binder) const; + int execInt(sqlite::database_binder &binder) const; + + + +}; + +#endif // SQLITEDAO_H diff --git a/database/sqlitequeryoption.cpp b/database/sqlitequeryoption.cpp new file mode 100644 index 0000000..67aebc9 --- /dev/null +++ b/database/sqlitequeryoption.cpp @@ -0,0 +1,66 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "sqlitequeryoption.h" + + +SqliteQueryOption::SqliteQueryOption(const QueryOption &o) { this->o = o; } + +SqliteQueryOption &SqliteQueryOption::setOrderByColumn(std::string name) +{ + this->orderByColumnName = name; + return *this; +} + +SqliteQueryOption &SqliteQueryOption::setVisibleColumnName(std::string name) +{ + this->visibleColumnName = name; + return *this; +} + +SqliteQueryOption &SqliteQueryOption::setPrependWhere(bool b) { this->prependWhere = b; return *this; } + +std::string SqliteQueryOption::build() +{ + std::string result; + if(!o.includeInvisible && ! this->visibleColumnName.empty()) + { + if(this->prependWhere) + { + result += "WHERE "; + } + result += this->visibleColumnName + " = 1"; + } + + result += " ORDER BY " + orderByColumnName; + if(o.order == ASCENDING) + { + result += " ASC"; + } + else + { + result += " DESC"; + } + //TODO: limits for offset? + if(o.limit > 0 ) + result += " LIMIT " + std::to_string(o.limit) + " OFFSET " + std::to_string(o.offset); + + return result; +} diff --git a/database/sqlitequeryoption.h b/database/sqlitequeryoption.h new file mode 100644 index 0000000..915d1cc --- /dev/null +++ b/database/sqlitequeryoption.h @@ -0,0 +1,26 @@ + #ifndef SQLITEQUERYOPTION_H +#define SQLITEQUERYOPTION_H +#include +#include "queryoption.h" + +class SqliteQueryOption +{ +private: + QueryOption o; + std::string visibleColumnName; + std::string orderByColumnName; + + bool prependWhere; +public: + SqliteQueryOption(const QueryOption &o); + + SqliteQueryOption &setOrderByColumn(std::string name); + + SqliteQueryOption &setVisibleColumnName(std::string name); + + SqliteQueryOption &setPrependWhere(bool b); + + std::string build(); +}; + +#endif // SQLITEQUERYOPTION_H diff --git a/database/userdao.cpp b/database/userdao.cpp new file mode 100644 index 0000000..2625f3a --- /dev/null +++ b/database/userdao.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "userdao.h" + +UserDao::UserDao() +{ + +} diff --git a/database/userdao.h b/database/userdao.h new file mode 100644 index 0000000..5322192 --- /dev/null +++ b/database/userdao.h @@ -0,0 +1,20 @@ +#ifndef USERDAO_H +#define USERDAO_H +#include +#include +#include "../user.h" +class UserDao +{ +public: + UserDao(); + virtual bool exists(std::string username) = 0; + virtual std::optional find(std::string username) = 0; + virtual std::optional find(int id) = 0; + virtual void deleteUser(std::string username) = 0; + virtual void save(const User &u) = 0; + virtual ~UserDao() { }; + + +}; + +#endif // USERDAO_H diff --git a/database/userdaosqlite.cpp b/database/userdaosqlite.cpp new file mode 100644 index 0000000..83e9fa4 --- /dev/null +++ b/database/userdaosqlite.cpp @@ -0,0 +1,93 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include +#include "userdaosqlite.h" + +UserDaoSqlite::UserDaoSqlite() +{ + +} + +bool UserDaoSqlite::exists(std::string username) +{ + auto prep = *db << "SELECT 1 FROM user WHERE username = ?" << username; + return execBool(prep); +} + +std::optional UserDaoSqlite::find(std::string username) +{ + + try + { + User user; + auto stmt = *db << "SELECT username, password, salt, permissions FROM user WHERE username = ?" << username; + + int perms = 0; + stmt >> std::tie(user.login, user.password, user.salt, perms); + user.permissions = Permissions { perms }; + + return std::move(user); + } + catch(const sqlite::errors::no_rows &e) + { + return { }; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +std::optional UserDaoSqlite::find(int id) +{ + try + { + User user; + auto stmt = *db << "SELECT username, password, salt, permissions FROM user WHERE id = ?" << id; + + int perms = 0; + stmt >> std::tie(user.login, user.password, user.salt, perms); + user.permissions = Permissions { perms }; + + return std::move(user); + } + catch(const sqlite::errors::no_rows &e) + { + return { }; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +void UserDaoSqlite::deleteUser(std::string username) +{ + //What to do with the contributions of the user? +} + +void UserDaoSqlite::save(const User &u) +{ + +} diff --git a/database/userdaosqlite.h b/database/userdaosqlite.h new file mode 100644 index 0000000..3e49b6b --- /dev/null +++ b/database/userdaosqlite.h @@ -0,0 +1,22 @@ +#ifndef USERDAOSQLITE_H +#define USERDAOSQLITE_H +#include +#include +#include +#include "userdao.h" +#include "sqlitedao.h" + +class UserDaoSqlite : public UserDao, protected SqliteDao +{ +public: + bool exists(std::string username); + std::optional find(std::string username); + std::optional find(int id); + + void deleteUser(std::string username); + void save(const User &u); + using SqliteDao::SqliteDao; + UserDaoSqlite(); +}; + +#endif // USERDAOSQLITE_H diff --git a/gateway/cgi.cpp b/gateway/cgi.cpp new file mode 100644 index 0000000..6a25606 --- /dev/null +++ b/gateway/cgi.cpp @@ -0,0 +1,115 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "cgi.h" +#include "../utils.h" +#include +#include +#include +#include +Cgi::Cgi(const Config &c) +{ + this->config = &c; +} + +bool Cgi::keepReading() +{ + return !this->responseSent; +} + +Request Cgi::readRequest() +{ + + std::string request_uri = utils::getenv("REQUEST_URI"); + if(request_uri == "") + { + throw std::runtime_error("REQUEST_URI is empty"); + } + + Request result { request_uri }; + + std::string method = utils::getenv("REQUEST_METHOD"); + if(method == "POST") + { + std::string content_type = utils::getenv("CONTENT_TYPE"); + if(content_type != "application/x-www-form-urlencoded") + { + throw "invalid content_type"; + } + std::string content_length = utils::getenv("CONTENT_LENGTH"); + int cl = std::stoi(content_length); + std::unique_ptr ptr(new char[cl+1]); + std::cin.get(ptr.get(), cl+1); + + std::string post_data { ptr.get() }; + + + + + } + + result.initCookies(utils::getenv("HTTP_COOKIE")); + result.setIp(utils::getenv("REMOTE_ADDR")); + result.setUseragent(utils::getenv("HTTP_USER_AGENT")); + + return result; + +} + +void Cgi::work(RequestWorker &worker) +{ + while(this->keepReading()) + { + Request req = readRequest(); + sendResponse(worker.processRequest(req)); + } +} + +void Cgi::sendResponse(const Response &r) +{ + std::cout << "Status: " << r.getStatus() << "\r\n"; + std::cout << "Content-Type: " << r.getContentType() <<"\r\n"; + for(auto header : r.getResponseHeaders()) + { + std::string key = header.first; + std::string second = header.second; + if(key.back() != ':') + { + std::cout << key << ":" << second << "\r\n"; + } + else + { + std::cout << key << second << "\r\n"; + } + } + for(const Cookie &c : r.getCookies()) + { + std::cout << "Set-Cookie: " << c.createHeaderValue() << "\r\n"; + } + std::cout << "\r\n"; + std::cout << r.getBody(); + std::cout.flush(); + this->responseSent = true; +} + +Cgi::~Cgi() +{ + +} diff --git a/gateway/cgi.h b/gateway/cgi.h new file mode 100644 index 0000000..098cd98 --- /dev/null +++ b/gateway/cgi.h @@ -0,0 +1,21 @@ +#ifndef CGI_H +#define CGI_H + +#include "gatewayinterface.h" +#include "../requestworker.h" +class Cgi : public GatewayInterface +{ +private: + bool responseSent = false; + const Config *config; + Request readRequest(); + void sendResponse(const Response &r); +public: + Cgi(const Config &c); + bool keepReading() override; + void work(RequestWorker &worker) override; + + ~Cgi(); +}; + +#endif // CGI_H diff --git a/gateway/gatewayfactory.cpp b/gateway/gatewayfactory.cpp new file mode 100644 index 0000000..d1d7b48 --- /dev/null +++ b/gateway/gatewayfactory.cpp @@ -0,0 +1,29 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "gatewayfactory.h" +#include "cgi.h" +#include "httpgateway.h" + +std::unique_ptr createGateway(const Config &c) +{ + return std::make_unique(c); +} + diff --git a/gateway/gatewayfactory.h b/gateway/gatewayfactory.h new file mode 100644 index 0000000..33b6105 --- /dev/null +++ b/gateway/gatewayfactory.h @@ -0,0 +1,8 @@ +#ifndef GATEWAYFACTORY_H +#define GATEWAYFACTORY_H +#include +#include "../config.h" +#include "gatewayinterface.h" +std::unique_ptr createGateway(const Config &c); + +#endif // GATEWAYFACTORY_H diff --git a/gateway/gatewayinterface.cpp b/gateway/gatewayinterface.cpp new file mode 100644 index 0000000..afcf260 --- /dev/null +++ b/gateway/gatewayinterface.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "gatewayinterface.h" + +GatewayInterface::GatewayInterface() +{ + +} diff --git a/gateway/gatewayinterface.h b/gateway/gatewayinterface.h new file mode 100644 index 0000000..3a77b37 --- /dev/null +++ b/gateway/gatewayinterface.h @@ -0,0 +1,16 @@ +#ifndef GATEWAYINTERFACE_H +#define GATEWAYINTERFACE_H +#include "../request.h" +#include "../response.h" +#include "../config.h" +#include "../requestworker.h" +class GatewayInterface +{ +public: + GatewayInterface(); + virtual bool keepReading() = 0; + virtual void work(RequestWorker &worker) = 0; + virtual ~GatewayInterface() { } +}; + +#endif // GATEWAYINTERFACE_H diff --git a/gateway/httpgateway.cpp b/gateway/httpgateway.cpp new file mode 100644 index 0000000..014d04c --- /dev/null +++ b/gateway/httpgateway.cpp @@ -0,0 +1,95 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "httpgateway.h" +#include "../logger.h" +HttpGateway::HttpGateway(const Config &config) +{ + +} + +bool HttpGateway::keepReading() +{ + return true; +} + +Request HttpGateway::convertRequest(httplib::Request request) +{ + Request result; + result.setRequestMethod(request.method); + result.setUrl(request.target); + + //TODO: this eats resources, where perhaps it does not need to. move it to request? + for (auto &it : request.params) + { + it.second = utils::html_xss(std::string { it.second }); + } + if(request.method == "GET") + { + result.setGetVars(request.params); + } + else + { + result.initGetMap(request.target); + result.setPostVars(request.params); + } + + if(request.has_header("COOKIE")) + { + result.initCookies(request.get_header_value("COOKIE")); + } + result.setIp(request.get_header_value("REMOTE_ADDR")); + + return result; +} + +httplib::Response HttpGateway::convertResponse(Response response) +{ + httplib::Response result; + result.set_content(response.getBody(), response.getContentType().c_str()); + result.status = response.getStatus(); + for(auto &header : response.getResponseHeaders()) + { + result.set_header(header.first.c_str(), header.second.c_str()); + } + + for(const Cookie &cookie : response.getCookies()) + { + result.set_header("Set-Cookie", cookie.createHeaderValue().c_str()); + } + return result; +} + + +void HttpGateway::work(RequestWorker &worker) +{ + httplib::Server server; + + auto handler = [&](const httplib::Request& req, httplib::Response& res) { + Request wikiRequest = convertRequest(req); + Logger::debug() << "httpgateway: received request " << wikiRequest; + Response wikiresponse = worker.processRequest(wikiRequest); + + res = convertResponse(wikiresponse); + }; + server.Get("/(.*)", handler); + server.Post("/(.*)", handler); + server.listen("localhost", 1234); +} diff --git a/gateway/httpgateway.h b/gateway/httpgateway.h new file mode 100644 index 0000000..6661df5 --- /dev/null +++ b/gateway/httpgateway.h @@ -0,0 +1,22 @@ +#ifndef HTTPGATEWAY_H +#define HTTPGATEWAY_H +#include "httplib.h" +#include "gatewayinterface.h" +#include "../requestworker.h" +#include "../request.h" +#include "../response.h" +#include "../utils.h" +class HttpGateway : public GatewayInterface +{ +private: + Response convertResponse(httplib::Response response); + httplib::Response convertResponse(Response response); + Request convertRequest(httplib::Request request); + // void worker(const httplib::Request& req, httplib::Response& res); +public: + HttpGateway(const Config &config); + bool keepReading() override; + void work(RequestWorker &worker) override; +}; + +#endif // HTTPGATEWAY_H diff --git a/gateway/httplib.h b/gateway/httplib.h new file mode 100644 index 0000000..4aeec7e --- /dev/null +++ b/gateway/httplib.h @@ -0,0 +1,2395 @@ +// +// httplib.h +// +// Copyright (c) 2017 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef _CPPHTTPLIB_HTTPLIB_H_ +#define _CPPHTTPLIB_HTTPLIB_H_ + +#ifdef _WIN32 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif +#ifndef _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_NONSTDC_NO_DEPRECATE +#endif + +#if defined(_MSC_VER) && _MSC_VER < 1900 +#define snprintf _snprintf_s +#endif + +#ifndef S_ISREG +#define S_ISREG(m) (((m)&S_IFREG)==S_IFREG) +#endif +#ifndef S_ISDIR +#define S_ISDIR(m) (((m)&S_IFDIR)==S_IFDIR) +#endif + +#include +#include +#include + +#undef min +#undef max + +#ifndef strcasecmp +#define strcasecmp _stricmp +#endif + +typedef SOCKET socket_t; +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef int socket_t; +#define INVALID_SOCKET (-1) +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#endif + +/* + * Configuration + */ +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND 0 + +namespace httplib +{ + +namespace detail { + +struct ci { + bool operator() (const std::string & s1, const std::string & s2) const { + return std::lexicographical_compare( + s1.begin(), s1.end(), + s2.begin(), s2.end(), + [](char c1, char c2) { + return ::tolower(c1) < ::tolower(c2); + }); + } +}; + +} // namespace detail + +enum class HttpVersion { v1_0 = 0, v1_1 }; + +typedef std::multimap Headers; + +template +std::pair make_range_header(uint64_t value, Args... args); + +typedef std::multimap Params; +typedef std::smatch Match; +typedef std::function Progress; + +struct MultipartFile { + std::string filename; + std::string content_type; + size_t offset = 0; + size_t length = 0; +}; +typedef std::multimap MultipartFiles; + +struct Request { + std::string version; + std::string method; + std::string target; + std::string path; + Headers headers; + std::string body; + Params params; + MultipartFiles files; + Match matches; + + Progress progress; + + bool has_header(const char* key) const; + std::string get_header_value(const char* key) const; + void set_header(const char* key, const char* val); + + bool has_param(const char* key) const; + std::string get_param_value(const char* key) const; + + bool has_file(const char* key) const; + MultipartFile get_file_value(const char* key) const; +}; + +struct Response { + std::string version; + int status; + Headers headers; + std::string body; + std::function streamcb; + + bool has_header(const char* key) const; + std::string get_header_value(const char* key) const; + void set_header(const char* key, const char* val); + + void set_redirect(const char* uri); + void set_content(const char* s, size_t n, const char* content_type); + void set_content(const std::string& s, const char* content_type); + + Response() : status(-1) {} +}; + +class Stream { +public: + virtual ~Stream() {} + virtual int read(char* ptr, size_t size) = 0; + virtual int write(const char* ptr, size_t size1) = 0; + virtual int write(const char* ptr) = 0; + virtual std::string get_remote_addr() = 0; + + template + void write_format(const char* fmt, const Args& ...args); +}; + +class SocketStream : public Stream { +public: + SocketStream(socket_t sock); + virtual ~SocketStream(); + + virtual int read(char* ptr, size_t size); + virtual int write(const char* ptr, size_t size); + virtual int write(const char* ptr); + virtual std::string get_remote_addr(); + +private: + socket_t sock_; +}; + +class Server { +public: + typedef std::function Handler; + typedef std::function Logger; + + Server(); + + virtual ~Server(); + + virtual bool is_valid() const; + + Server& Get(const char* pattern, Handler handler); + Server& Post(const char* pattern, Handler handler); + + Server& Put(const char* pattern, Handler handler); + Server& Delete(const char* pattern, Handler handler); + Server& Options(const char* pattern, Handler handler); + + bool set_base_dir(const char* path); + + void set_error_handler(Handler handler); + void set_logger(Logger logger); + + void set_keep_alive_max_count(size_t count); + + int bind_to_any_port(const char* host, int socket_flags = 0); + bool listen_after_bind(); + + bool listen(const char* host, int port, int socket_flags = 0); + + bool is_running() const; + void stop(); + +protected: + bool process_request(Stream& strm, bool last_connection, bool& connection_close); + + size_t keep_alive_max_count_; + +private: + typedef std::vector> Handlers; + + socket_t create_server_socket(const char* host, int port, int socket_flags) const; + int bind_internal(const char* host, int port, int socket_flags); + bool listen_internal(); + + bool routing(Request& req, Response& res); + bool handle_file_request(Request& req, Response& res); + bool dispatch_request(Request& req, Response& res, Handlers& handlers); + + bool parse_request_line(const char* s, Request& req); + void write_response(Stream& strm, bool last_connection, const Request& req, Response& res); + + virtual bool read_and_close_socket(socket_t sock); + + bool is_running_; + socket_t svr_sock_; + std::string base_dir_; + Handlers get_handlers_; + Handlers post_handlers_; + Handlers put_handlers_; + Handlers delete_handlers_; + Handlers options_handlers_; + Handler error_handler_; + Logger logger_; + + // TODO: Use thread pool... + std::mutex running_threads_mutex_; + int running_threads_; +}; + +class Client { +public: + Client( + const char* host, + int port = 80, + time_t timeout_sec = 300); + + virtual ~Client(); + + virtual bool is_valid() const; + + std::shared_ptr Get(const char* path, Progress progress = nullptr); + std::shared_ptr Get(const char* path, const Headers& headers, Progress progress = nullptr); + + std::shared_ptr Head(const char* path); + std::shared_ptr Head(const char* path, const Headers& headers); + + std::shared_ptr Post(const char* path, const std::string& body, const char* content_type); + std::shared_ptr Post(const char* path, const Headers& headers, const std::string& body, const char* content_type); + + std::shared_ptr Post(const char* path, const Params& params); + std::shared_ptr Post(const char* path, const Headers& headers, const Params& params); + + std::shared_ptr Put(const char* path, const std::string& body, const char* content_type); + std::shared_ptr Put(const char* path, const Headers& headers, const std::string& body, const char* content_type); + + std::shared_ptr Delete(const char* path); + std::shared_ptr Delete(const char* path, const Headers& headers); + + std::shared_ptr Options(const char* path); + std::shared_ptr Options(const char* path, const Headers& headers); + + bool send(Request& req, Response& res); + +protected: + bool process_request(Stream& strm, Request& req, Response& res, bool& connection_close); + + const std::string host_; + const int port_; + time_t timeout_sec_; + const std::string host_and_port_; + +private: + socket_t create_client_socket() const; + bool read_response_line(Stream& strm, Response& res); + void write_request(Stream& strm, Request& req); + + virtual bool read_and_close_socket(socket_t sock, Request& req, Response& res); +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLSocketStream : public Stream { +public: + SSLSocketStream(socket_t sock, SSL* ssl); + virtual ~SSLSocketStream(); + + virtual int read(char* ptr, size_t size); + virtual int write(const char* ptr, size_t size); + virtual int write(const char* ptr); + virtual std::string get_remote_addr(); + +private: + socket_t sock_; + SSL* ssl_; +}; + +class SSLServer : public Server { +public: + SSLServer( + const char* cert_path, const char* private_key_path); + + virtual ~SSLServer(); + + virtual bool is_valid() const; + +private: + virtual bool read_and_close_socket(socket_t sock); + + SSL_CTX* ctx_; + std::mutex ctx_mutex_; +}; + +class SSLClient : public Client { +public: + SSLClient( + const char* host, + int port = 80, + time_t timeout_sec = 300); + + virtual ~SSLClient(); + + virtual bool is_valid() const; + +private: + virtual bool read_and_close_socket(socket_t sock, Request& req, Response& res); + + SSL_CTX* ctx_; + std::mutex ctx_mutex_; +}; +#endif + +/* + * Implementation + */ +namespace detail { + +template +void split(const char* b, const char* e, char d, Fn fn) +{ + int i = 0; + int beg = 0; + + while (e ? (b + i != e) : (b[i] != '\0')) { + if (b[i] == d) { + fn(&b[beg], &b[i]); + beg = i + 1; + } + i++; + } + + if (i) { + fn(&b[beg], &b[i]); + } +} + +// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` +// to store data. The call can set memory on stack for performance. +class stream_line_reader { +public: + stream_line_reader(Stream& strm, char* fixed_buffer, size_t fixed_buffer_size) + : strm_(strm) + , fixed_buffer_(fixed_buffer) + , fixed_buffer_size_(fixed_buffer_size) { + } + + const char* ptr() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_; + } else { + return glowable_buffer_.data(); + } + } + + bool getline() { + fixed_buffer_used_size_ = 0; + glowable_buffer_.clear(); + + for (size_t i = 0; ; i++) { + char byte; + auto n = strm_.read(&byte, 1); + + if (n < 0) { + return false; + } else if (n == 0) { + if (i == 0) { + return false; + } else { + break; + } + } + + append(byte); + + if (byte == '\n') { + break; + } + } + + return true; + } + +private: + void append(char c) { + if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { + fixed_buffer_[fixed_buffer_used_size_++] = c; + fixed_buffer_[fixed_buffer_used_size_] = '\0'; + } else { + if (glowable_buffer_.empty()) { + assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); + glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); + } + glowable_buffer_ += c; + } + } + + Stream& strm_; + char* fixed_buffer_; + const size_t fixed_buffer_size_; + size_t fixed_buffer_used_size_; + std::string glowable_buffer_; +}; + +inline int close_socket(socket_t sock) +{ +#ifdef _WIN32 + return closesocket(sock); +#else + return close(sock); +#endif +} + +inline int select_read(socket_t sock, time_t sec, time_t usec) +{ + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = sec; + tv.tv_usec = usec; + + return select(sock + 1, &fds, NULL, NULL, &tv); +} + +inline bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) +{ + fd_set fdsr; + FD_ZERO(&fdsr); + FD_SET(sock, &fdsr); + + auto fdsw = fdsr; + auto fdse = fdsr; + + timeval tv; + tv.tv_sec = sec; + tv.tv_usec = usec; + + if (select(sock + 1, &fdsr, &fdsw, &fdse, &tv) < 0) { + return false; + } else if (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw)) { + int error = 0; + socklen_t len = sizeof(error); + if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error, &len) < 0 || error) { + return false; + } + } else { + return false; + } + + return true; +} + +template +inline bool read_and_close_socket(socket_t sock, size_t keep_alive_max_count, T callback) +{ + bool ret = false; + + if (keep_alive_max_count > 0) { + auto count = keep_alive_max_count; + while (count > 0 && + detail::select_read(sock, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0) { + SocketStream strm(sock); + auto last_connection = count == 1; + auto connection_close = false; + + ret = callback(strm, last_connection, connection_close); + if (!ret || connection_close) { + break; + } + + count--; + } + } else { + SocketStream strm(sock); + auto dummy_connection_close = false; + ret = callback(strm, true, dummy_connection_close); + } + + close_socket(sock); + return ret; +} + +inline int shutdown_socket(socket_t sock) +{ +#ifdef _WIN32 + return shutdown(sock, SD_BOTH); +#else + return shutdown(sock, SHUT_RDWR); +#endif +} + +template +socket_t create_socket(const char* host, int port, Fn fn, int socket_flags = 0) +{ +#ifdef _WIN32 +#define SO_SYNCHRONOUS_NONALERT 0x20 +#define SO_OPENTYPE 0x7008 + + int opt = SO_SYNCHRONOUS_NONALERT; + setsockopt(INVALID_SOCKET, SOL_SOCKET, SO_OPENTYPE, (char*)&opt, sizeof(opt)); +#endif + + // Get address info + struct addrinfo hints; + struct addrinfo *result; + + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = socket_flags; + hints.ai_protocol = 0; + + auto service = std::to_string(port); + + if (getaddrinfo(host, service.c_str(), &hints, &result)) { + return INVALID_SOCKET; + } + + for (auto rp = result; rp; rp = rp->ai_next) { + // Create a socket + auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sock == INVALID_SOCKET) { + continue; + } + + // Make 'reuse address' option available + int yes = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&yes, sizeof(yes)); + + // bind or connect + if (fn(sock, *rp)) { + freeaddrinfo(result); + return sock; + } + + close_socket(sock); + } + + freeaddrinfo(result); + return INVALID_SOCKET; +} + +inline void set_nonblocking(socket_t sock, bool nonblocking) +{ +#ifdef _WIN32 + auto flags = nonblocking ? 1UL : 0UL; + ioctlsocket(sock, FIONBIO, &flags); +#else + auto flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); +#endif +} + +inline bool is_connection_error() +{ +#ifdef _WIN32 + return WSAGetLastError() != WSAEWOULDBLOCK; +#else + return errno != EINPROGRESS; +#endif +} + +inline std::string get_remote_addr(socket_t sock) { + struct sockaddr_storage addr; + socklen_t len = sizeof(addr); + + if (!getpeername(sock, (struct sockaddr*)&addr, &len)) { + char ipstr[NI_MAXHOST]; + + if (!getnameinfo((struct sockaddr*)&addr, len, + ipstr, sizeof(ipstr), nullptr, 0, NI_NUMERICHOST)) { + return ipstr; + } + } + + return std::string(); +} + +inline bool is_file(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); +} + +inline bool is_dir(const std::string& path) +{ + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); +} + +inline bool is_valid_path(const std::string& path) { + size_t level = 0; + size_t i = 0; + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + + while (i < path.size()) { + // Read component + auto beg = i; + while (i < path.size() && path[i] != '/') { + i++; + } + + auto len = i - beg; + assert(len > 0); + + if (!path.compare(beg, len, ".")) { + ; + } else if (!path.compare(beg, len, "..")) { + if (level == 0) { + return false; + } + level--; + } else { + level++; + } + + // Skip slash + while (i < path.size() && path[i] == '/') { + i++; + } + } + + return true; +} + +inline void read_file(const std::string& path, std::string& out) +{ + std::ifstream fs(path, std::ios_base::binary); + fs.seekg(0, std::ios_base::end); + auto size = fs.tellg(); + fs.seekg(0); + out.resize(static_cast(size)); + fs.read(&out[0], size); +} + +inline std::string file_extension(const std::string& path) +{ + std::smatch m; + auto pat = std::regex("\\.([a-zA-Z0-9]+)$"); + if (std::regex_search(path, m, pat)) { + return m[1].str(); + } + return std::string(); +} + +inline const char* find_content_type(const std::string& path) +{ + auto ext = file_extension(path); + if (ext == "txt") { + return "text/plain"; + } else if (ext == "html") { + return "text/html"; + } else if (ext == "css") { + return "text/css"; + } else if (ext == "jpeg" || ext == "jpg") { + return "image/jpg"; + } else if (ext == "png") { + return "image/png"; + } else if (ext == "gif") { + return "image/gif"; + } else if (ext == "svg") { + return "image/svg+xml"; + } else if (ext == "ico") { + return "image/x-icon"; + } else if (ext == "json") { + return "application/json"; + } else if (ext == "pdf") { + return "application/pdf"; + } else if (ext == "js") { + return "application/javascript"; + } else if (ext == "xml") { + return "application/xml"; + } else if (ext == "xhtml") { + return "application/xhtml+xml"; + } + return nullptr; +} + +inline const char* status_message(int status) +{ + switch (status) { + case 200: return "OK"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 400: return "Bad Request"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 415: return "Unsupported Media Type"; + default: + case 500: return "Internal Server Error"; + } +} + +inline bool has_header(const Headers& headers, const char* key) +{ + return headers.find(key) != headers.end(); +} + +inline const char* get_header_value( + const Headers& headers, const char* key, const char* def = nullptr) +{ + auto it = headers.find(key); + if (it != headers.end()) { + return it->second.c_str(); + } + return def; +} + +inline int get_header_value_int(const Headers& headers, const char* key, int def = 0) +{ + auto it = headers.find(key); + if (it != headers.end()) { + return std::stoi(it->second); + } + return def; +} + +inline bool read_headers(Stream& strm, Headers& headers) +{ + static std::regex re(R"((.+?):\s*(.+?)\s*\r\n)"); + + const auto bufsiz = 2048; + char buf[bufsiz]; + + stream_line_reader reader(strm, buf, bufsiz); + + for (;;) { + if (!reader.getline()) { + return false; + } + if (!strcmp(reader.ptr(), "\r\n")) { + break; + } + std::cmatch m; + if (std::regex_match(reader.ptr(), m, re)) { + auto key = std::string(m[1]); + auto val = std::string(m[2]); + headers.emplace(key, val); + } + } + + return true; +} + +inline bool read_content_with_length(Stream& strm, std::string& out, size_t len, Progress progress) +{ + out.assign(len, 0); + size_t r = 0; + while (r < len){ + auto n = strm.read(&out[r], len - r); + if (n <= 0) { + return false; + } + + r += n; + + if (progress) { + if (!progress(r, len)) { + return false; + } + } + } + + return true; +} + +inline bool read_content_without_length(Stream& strm, std::string& out) +{ + for (;;) { + char byte; + auto n = strm.read(&byte, 1); + if (n < 0) { + return false; + } else if (n == 0) { + return true; + } + out += byte; + } + + return true; +} + +inline bool read_content_chunked(Stream& strm, std::string& out) +{ + const auto bufsiz = 16; + char buf[bufsiz]; + + stream_line_reader reader(strm, buf, bufsiz); + + if (!reader.getline()) { + return false; + } + + auto chunk_len = std::stoi(reader.ptr(), 0, 16); + + while (chunk_len > 0){ + std::string chunk; + if (!read_content_with_length(strm, chunk, chunk_len, nullptr)) { + return false; + } + + if (!reader.getline()) { + return false; + } + + if (strcmp(reader.ptr(), "\r\n")) { + break; + } + + out += chunk; + + if (!reader.getline()) { + return false; + } + + chunk_len = std::stoi(reader.ptr(), 0, 16); + } + + if (chunk_len == 0) { + // Reader terminator after chunks + if (!reader.getline() || strcmp(reader.ptr(), "\r\n")) + return false; + } + + return true; +} + +template +bool read_content(Stream& strm, T& x, Progress progress = Progress()) +{ + if (has_header(x.headers, "Content-Length")) { + auto len = get_header_value_int(x.headers, "Content-Length", 0); + if (len == 0) { + const auto& encoding = get_header_value(x.headers, "Transfer-Encoding", ""); + if (!strcasecmp(encoding, "chunked")) { + return read_content_chunked(strm, x.body); + } + } + return read_content_with_length(strm, x.body, len, progress); + } else { + const auto& encoding = get_header_value(x.headers, "Transfer-Encoding", ""); + if (!strcasecmp(encoding, "chunked")) { + return read_content_chunked(strm, x.body); + } + return read_content_without_length(strm, x.body); + } + return true; +} + +template +inline void write_headers(Stream& strm, const T& info) +{ + for (const auto& x: info.headers) { + strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); + } + strm.write("\r\n"); +} + +inline std::string encode_url(const std::string& s) +{ + std::string result; + + for (auto i = 0; s[i]; i++) { + switch (s[i]) { + case ' ': result += "+"; break; + case '\'': result += "%27"; break; + case ',': result += "%2C"; break; + case ':': result += "%3A"; break; + case ';': result += "%3B"; break; + default: + auto c = static_cast(s[i]); + if (c >= 0x80) { + result += '%'; + char hex[4]; + size_t len = snprintf(hex, sizeof(hex) - 1, "%02X", c); + assert(len == 2); + result.append(hex, len); + } else { + result += s[i]; + } + break; + } + } + + return result; +} + +inline bool is_hex(char c, int& v) +{ + if (0x20 <= c && isdigit(c)) { + v = c - '0'; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } + return false; +} + +inline bool from_hex_to_i(const std::string& s, size_t i, size_t cnt, int& val) +{ + if (i >= s.size()) { + return false; + } + + val = 0; + for (; cnt; i++, cnt--) { + if (!s[i]) { + return false; + } + int v = 0; + if (is_hex(s[i], v)) { + val = val * 16 + v; + } else { + return false; + } + } + return true; +} + +inline std::string from_i_to_hex(uint64_t n) +{ + const char *charset = "0123456789abcdef"; + std::string ret; + do { + ret = charset[n & 15] + ret; + n >>= 4; + } while (n > 0); + return ret; +} + +inline size_t to_utf8(int code, char* buff) +{ + if (code < 0x0080) { + buff[0] = (code & 0x7F); + return 1; + } else if (code < 0x0800) { + buff[0] = (0xC0 | ((code >> 6) & 0x1F)); + buff[1] = (0x80 | (code & 0x3F)); + return 2; + } else if (code < 0xD800) { + buff[0] = (0xE0 | ((code >> 12) & 0xF)); + buff[1] = (0x80 | ((code >> 6) & 0x3F)); + buff[2] = (0x80 | (code & 0x3F)); + return 3; + } else if (code < 0xE000) { // D800 - DFFF is invalid... + return 0; + } else if (code < 0x10000) { + buff[0] = (0xE0 | ((code >> 12) & 0xF)); + buff[1] = (0x80 | ((code >> 6) & 0x3F)); + buff[2] = (0x80 | (code & 0x3F)); + return 3; + } else if (code < 0x110000) { + buff[0] = (0xF0 | ((code >> 18) & 0x7)); + buff[1] = (0x80 | ((code >> 12) & 0x3F)); + buff[2] = (0x80 | ((code >> 6) & 0x3F)); + buff[3] = (0x80 | (code & 0x3F)); + return 4; + } + + // NOTREACHED + return 0; +} + +inline std::string decode_url(const std::string& s) +{ + std::string result; + + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '%' && i + 1 < s.size()) { + if (s[i + 1] == 'u') { + int val = 0; + if (from_hex_to_i(s, i + 2, 4, val)) { + // 4 digits Unicode codes + char buff[4]; + size_t len = to_utf8(val, buff); + if (len > 0) { + result.append(buff, len); + } + i += 5; // 'u0000' + } else { + result += s[i]; + } + } else { + int val = 0; + if (from_hex_to_i(s, i + 1, 2, val)) { + // 2 digits hex codes + result += val; + i += 2; // '00' + } else { + result += s[i]; + } + } + } else if (s[i] == '+') { + result += ' '; + } else { + result += s[i]; + } + } + + return result; +} + +inline void parse_query_text(const std::string& s, Params& params) +{ + split(&s[0], &s[s.size()], '&', [&](const char* b, const char* e) { + std::string key; + std::string val; + split(b, e, '=', [&](const char* b, const char* e) { + if (key.empty()) { + key.assign(b, e); + } else { + val.assign(b, e); + } + }); + params.emplace(key, decode_url(val)); + }); +} + +inline bool parse_multipart_boundary(const std::string& content_type, std::string& boundary) +{ + auto pos = content_type.find("boundary="); + if (pos == std::string::npos) { + return false; + } + + boundary = content_type.substr(pos + 9); + return true; +} + +inline bool parse_multipart_formdata( + const std::string& boundary, const std::string& body, MultipartFiles& files) +{ + static std::string dash = "--"; + static std::string crlf = "\r\n"; + + static std::regex re_content_type( + "Content-Type: (.*?)", std::regex_constants::icase); + + static std::regex re_content_disposition( + "Content-Disposition: form-data; name=\"(.*?)\"(?:; filename=\"(.*?)\")?", + std::regex_constants::icase); + + auto dash_boundary = dash + boundary; + + auto pos = body.find(dash_boundary); + if (pos != 0) { + return false; + } + + pos += dash_boundary.size(); + + auto next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + pos = next_pos + crlf.size(); + + while (pos < body.size()) { + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + std::string name; + MultipartFile file; + + auto header = body.substr(pos, (next_pos - pos)); + + while (pos != next_pos) { + std::smatch m; + if (std::regex_match(header, m, re_content_type)) { + file.content_type = m[1]; + } else if (std::regex_match(header, m, re_content_disposition)) { + name = m[1]; + file.filename = m[2]; + } + + pos = next_pos + crlf.size(); + + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + header = body.substr(pos, (next_pos - pos)); + } + + pos = next_pos + crlf.size(); + + next_pos = body.find(crlf + dash_boundary, pos); + + if (next_pos == std::string::npos) { + return false; + } + + file.offset = pos; + file.length = next_pos - pos; + + pos = next_pos + crlf.size() + dash_boundary.size(); + + next_pos = body.find(crlf, pos); + if (next_pos == std::string::npos) { + return false; + } + + files.emplace(name, file); + + pos = next_pos + crlf.size(); + } + + return true; +} + +inline std::string to_lower(const char* beg, const char* end) +{ + std::string out; + auto it = beg; + while (it != end) { + out += ::tolower(*it); + it++; + } + return out; +} + +inline void make_range_header_core(std::string&) {} + +template +inline void make_range_header_core(std::string& field, uint64_t value) +{ + if (!field.empty()) { + field += ", "; + } + field += std::to_string(value) + "-"; +} + +template +inline void make_range_header_core(std::string& field, uint64_t value1, uint64_t value2, Args... args) +{ + if (!field.empty()) { + field += ", "; + } + field += std::to_string(value1) + "-" + std::to_string(value2); + make_range_header_core(field, args...); +} + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +inline bool can_compress(const std::string& content_type) { + return !content_type.find("text/") || + content_type == "image/svg+xml" || + content_type == "application/javascript" || + content_type == "application/json" || + content_type == "application/xml" || + content_type == "application/xhtml+xml"; +} + +inline void compress(std::string& content) +{ + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + auto ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + return; + } + + strm.avail_in = content.size(); + strm.next_in = (Bytef *)content.data(); + + std::string compressed; + + const auto bufsiz = 16384; + char buff[bufsiz]; + do { + strm.avail_out = bufsiz; + strm.next_out = (Bytef *)buff; + deflate(&strm, Z_FINISH); + compressed.append(buff, bufsiz - strm.avail_out); + } while (strm.avail_out == 0); + + content.swap(compressed); + + deflateEnd(&strm); +} + +inline void decompress(std::string& content) +{ + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + + // 15 is the value of wbits, which should be at the maximum possible value to ensure + // that any gzip stream can be decoded. The offset of 16 specifies that the stream + // to decompress will be formatted with a gzip wrapper. + auto ret = inflateInit2(&strm, 16 + 15); + if (ret != Z_OK) { + return; + } + + strm.avail_in = content.size(); + strm.next_in = (Bytef *)content.data(); + + std::string decompressed; + + const auto bufsiz = 16384; + char buff[bufsiz]; + do { + strm.avail_out = bufsiz; + strm.next_out = (Bytef *)buff; + inflate(&strm, Z_NO_FLUSH); + decompressed.append(buff, bufsiz - strm.avail_out); + } while (strm.avail_out == 0); + + content.swap(decompressed); + + inflateEnd(&strm); +} +#endif + +#ifdef _WIN32 +class WSInit { +public: + WSInit() { + WSADATA wsaData; + WSAStartup(0x0002, &wsaData); + } + + ~WSInit() { + WSACleanup(); + } +}; + +static WSInit wsinit_; +#endif + +} // namespace detail + +// Header utilities +template +inline std::pair make_range_header(uint64_t value, Args... args) +{ + std::string field; + detail::make_range_header_core(field, value, args...); + field.insert(0, "bytes="); + return std::make_pair("Range", field); +} + +// Request implementation +inline bool Request::has_header(const char* key) const +{ + return detail::has_header(headers, key); +} + +inline std::string Request::get_header_value(const char* key) const +{ + return detail::get_header_value(headers, key, ""); +} + +inline void Request::set_header(const char* key, const char* val) +{ + headers.emplace(key, val); +} + +inline bool Request::has_param(const char* key) const +{ + return params.find(key) != params.end(); +} + +inline std::string Request::get_param_value(const char* key) const +{ + auto it = params.find(key); + if (it != params.end()) { + return it->second; + } + return std::string(); +} + +inline bool Request::has_file(const char* key) const +{ + return files.find(key) != files.end(); +} + +inline MultipartFile Request::get_file_value(const char* key) const +{ + auto it = files.find(key); + if (it != files.end()) { + return it->second; + } + return MultipartFile(); +} + +// Response implementation +inline bool Response::has_header(const char* key) const +{ + return headers.find(key) != headers.end(); +} + +inline std::string Response::get_header_value(const char* key) const +{ + return detail::get_header_value(headers, key, ""); +} + +inline void Response::set_header(const char* key, const char* val) +{ + headers.emplace(key, val); +} + +inline void Response::set_redirect(const char* url) +{ + set_header("Location", url); + status = 302; +} + +inline void Response::set_content(const char* s, size_t n, const char* content_type) +{ + body.assign(s, n); + set_header("Content-Type", content_type); +} + +inline void Response::set_content(const std::string& s, const char* content_type) +{ + body = s; + set_header("Content-Type", content_type); +} + +// Rstream implementation +template +inline void Stream::write_format(const char* fmt, const Args& ...args) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + +#if defined(_MSC_VER) && _MSC_VER < 1900 + auto n = _snprintf_s(buf, bufsiz, bufsiz - 1, fmt, args...); +#else + auto n = snprintf(buf, bufsiz - 1, fmt, args...); +#endif + if (n > 0) { + if (n >= bufsiz - 1) { + std::vector glowable_buf(bufsiz); + + while (n >= static_cast(glowable_buf.size() - 1)) { + glowable_buf.resize(glowable_buf.size() * 2); +#if defined(_MSC_VER) && _MSC_VER < 1900 + n = _snprintf_s(&glowable_buf[0], glowable_buf.size(), glowable_buf.size() - 1, fmt, args...); +#else + n = snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...); +#endif + } + write(&glowable_buf[0], n); + } else { + write(buf, n); + } + } +} + +// Socket stream implementation +inline SocketStream::SocketStream(socket_t sock): sock_(sock) +{ +} + +inline SocketStream::~SocketStream() +{ +} + +inline int SocketStream::read(char* ptr, size_t size) +{ + return recv(sock_, ptr, size, 0); +} + +inline int SocketStream::write(const char* ptr, size_t size) +{ + return send(sock_, ptr, size, 0); +} + +inline int SocketStream::write(const char* ptr) +{ + return write(ptr, strlen(ptr)); +} + +inline std::string SocketStream::get_remote_addr() { + return detail::get_remote_addr(sock_); +} + +// HTTP server implementation +inline Server::Server() + : keep_alive_max_count_(5) + , is_running_(false) + , svr_sock_(INVALID_SOCKET) + , running_threads_(0) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} + +inline Server::~Server() +{ +} + +inline Server& Server::Get(const char* pattern, Handler handler) +{ + get_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline Server& Server::Post(const char* pattern, Handler handler) +{ + post_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline Server& Server::Put(const char* pattern, Handler handler) +{ + put_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline Server& Server::Delete(const char* pattern, Handler handler) +{ + delete_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline Server& Server::Options(const char* pattern, Handler handler) +{ + options_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); + return *this; +} + +inline bool Server::set_base_dir(const char* path) +{ + if (detail::is_dir(path)) { + base_dir_ = path; + return true; + } + return false; +} + +inline void Server::set_error_handler(Handler handler) +{ + error_handler_ = handler; +} + +inline void Server::set_logger(Logger logger) +{ + logger_ = logger; +} + +inline void Server::set_keep_alive_max_count(size_t count) +{ + keep_alive_max_count_ = count; +} + +inline int Server::bind_to_any_port(const char* host, int socket_flags) +{ + return bind_internal(host, 0, socket_flags); +} + +inline bool Server::listen_after_bind() { + return listen_internal(); +} + +inline bool Server::listen(const char* host, int port, int socket_flags) +{ + if (bind_internal(host, port, socket_flags) < 0) + return false; + return listen_internal(); +} + +inline bool Server::is_running() const +{ + return is_running_; +} + +inline void Server::stop() +{ + if (is_running_) { + assert(svr_sock_ != INVALID_SOCKET); + auto sock = svr_sock_; + svr_sock_ = INVALID_SOCKET; + detail::shutdown_socket(sock); + detail::close_socket(sock); + } +} + +inline bool Server::parse_request_line(const char* s, Request& req) +{ + static std::regex re("(GET|HEAD|POST|PUT|DELETE|OPTIONS) (([^?]+)(?:\\?(.+?))?) (HTTP/1\\.[01])\r\n"); + + std::cmatch m; + if (std::regex_match(s, m, re)) { + req.version = std::string(m[5]); + req.method = std::string(m[1]); + req.target = std::string(m[2]); + req.path = detail::decode_url(m[3]); + + // Parse query text + auto len = std::distance(m[4].first, m[4].second); + if (len > 0) { + detail::parse_query_text(m[4], req.params); + } + + return true; + } + + return false; +} + +inline void Server::write_response(Stream& strm, bool last_connection, const Request& req, Response& res) +{ + assert(res.status != -1); + + if (400 <= res.status && error_handler_) { + error_handler_(req, res); + } + + // Response line + strm.write_format("HTTP/1.1 %d %s\r\n", + res.status, + detail::status_message(res.status)); + + // Headers + if (last_connection || + req.get_header_value("Connection") == "close") { + res.set_header("Connection", "close"); + } + + if (!last_connection && + req.get_header_value("Connection") == "Keep-Alive") { + res.set_header("Connection", "Keep-Alive"); + } + + if (!res.body.empty()) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + // TODO: 'Accpet-Encoding' has gzip, not gzip;q=0 + const auto& encodings = req.get_header_value("Accept-Encoding"); + if (encodings.find("gzip") != std::string::npos && + detail::can_compress(res.get_header_value("Content-Type"))) { + detail::compress(res.body); + res.set_header("Content-Encoding", "gzip"); + } +#endif + + if (!res.has_header("Content-Type")) { + res.set_header("Content-Type", "text/plain"); + } + + auto length = std::to_string(res.body.size()); + res.set_header("Content-Length", length.c_str()); + } else if (res.streamcb) { + // Streamed response + bool chunked_response = !res.has_header("Content-Length"); + if (chunked_response) + res.set_header("Transfer-Encoding", "chunked"); + } + + detail::write_headers(strm, res); + + // Body + if (req.method != "HEAD") { + if (!res.body.empty()) { + strm.write(res.body.c_str(), res.body.size()); + } else if (res.streamcb) { + bool chunked_response = !res.has_header("Content-Length"); + uint64_t offset = 0; + bool data_available = true; + while (data_available) { + std::string chunk = res.streamcb(offset); + offset += chunk.size(); + data_available = !chunk.empty(); + // Emit chunked response header and footer for each chunk + if (chunked_response) + chunk = detail::from_i_to_hex(chunk.size()) + "\r\n" + chunk + "\r\n"; + if (strm.write(chunk.c_str(), chunk.size()) < 0) + break; // Stop on error + } + } + } + + // Log + if (logger_) { + logger_(req, res); + } +} + +inline bool Server::handle_file_request(Request& req, Response& res) +{ + if (!base_dir_.empty() && detail::is_valid_path(req.path)) { + std::string path = base_dir_ + req.path; + + if (!path.empty() && path.back() == '/') { + path += "index.html"; + } + + if (detail::is_file(path)) { + detail::read_file(path, res.body); + auto type = detail::find_content_type(path); + if (type) { + res.set_header("Content-Type", type); + } + res.status = 200; + return true; + } + } + + return false; +} + +inline socket_t Server::create_server_socket(const char* host, int port, int socket_flags) const +{ + return detail::create_socket(host, port, + [](socket_t sock, struct addrinfo& ai) -> bool { + if (::bind(sock, ai.ai_addr, ai.ai_addrlen)) { + return false; + } + if (::listen(sock, 5)) { // Listen through 5 channels + return false; + } + return true; + }, socket_flags); +} + +inline int Server::bind_internal(const char* host, int port, int socket_flags) +{ + if (!is_valid()) { + return -1; + } + + svr_sock_ = create_server_socket(host, port, socket_flags); + if (svr_sock_ == INVALID_SOCKET) { + return -1; + } + + if (port == 0) { + struct sockaddr_storage address; + socklen_t len = sizeof(address); + if (getsockname(svr_sock_, reinterpret_cast(&address), &len) == -1) { + return -1; + } + if (address.ss_family == AF_INET) { + return ntohs(reinterpret_cast(&address)->sin_port); + } else if (address.ss_family == AF_INET6) { + return ntohs(reinterpret_cast(&address)->sin6_port); + } else { + return -1; + } + } else { + return port; + } +} + +inline bool Server::listen_internal() +{ + auto ret = true; + + is_running_ = true; + + for (;;) { + auto val = detail::select_read(svr_sock_, 0, 100000); + + if (val == 0) { // Timeout + if (svr_sock_ == INVALID_SOCKET) { + // The server socket was closed by 'stop' method. + break; + } + continue; + } + + socket_t sock = accept(svr_sock_, NULL, NULL); + + if (sock == INVALID_SOCKET) { + if (svr_sock_ != INVALID_SOCKET) { + detail::close_socket(svr_sock_); + ret = false; + } else { + ; // The server socket was closed by user. + } + break; + } + + // TODO: Use thread pool... + std::thread([=]() { + { + std::lock_guard guard(running_threads_mutex_); + running_threads_++; + } + + read_and_close_socket(sock); + + { + std::lock_guard guard(running_threads_mutex_); + running_threads_--; + } + }).detach(); + } + + // TODO: Use thread pool... + for (;;) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::lock_guard guard(running_threads_mutex_); + if (!running_threads_) { + break; + } + } + + is_running_ = false; + + return ret; +} + +inline bool Server::routing(Request& req, Response& res) +{ + if (req.method == "GET" && handle_file_request(req, res)) { + return true; + } + + if (req.method == "GET" || req.method == "HEAD") { + return dispatch_request(req, res, get_handlers_); + } else if (req.method == "POST") { + return dispatch_request(req, res, post_handlers_); + } else if (req.method == "PUT") { + return dispatch_request(req, res, put_handlers_); + } else if (req.method == "DELETE") { + return dispatch_request(req, res, delete_handlers_); + } else if (req.method == "OPTIONS") { + return dispatch_request(req, res, options_handlers_); + } + return false; +} + +inline bool Server::dispatch_request(Request& req, Response& res, Handlers& handlers) +{ + for (const auto& x: handlers) { + const auto& pattern = x.first; + const auto& handler = x.second; + + if (std::regex_match(req.path, req.matches, pattern)) { + handler(req, res); + return true; + } + } + return false; +} + +inline bool Server::process_request(Stream& strm, bool last_connection, bool& connection_close) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + + detail::stream_line_reader reader(strm, buf, bufsiz); + + // Connection has been closed on client + if (!reader.getline()) { + return false; + } + + Request req; + Response res; + + res.version = "HTTP/1.1"; + + // Request line and headers + if (!parse_request_line(reader.ptr(), req) || !detail::read_headers(strm, req.headers)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return true; + } + + if (req.get_header_value("Connection") == "close") { + connection_close = true; + } + + req.set_header("REMOTE_ADDR", strm.get_remote_addr().c_str()); + + // Body + if (req.method == "POST" || req.method == "PUT") { + if (!detail::read_content(strm, req)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return true; + } + + const auto& content_type = req.get_header_value("Content-Type"); + + if (req.get_header_value("Content-Encoding") == "gzip") { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + detail::decompress(req.body); +#else + res.status = 415; + write_response(strm, last_connection, req, res); + return true; +#endif + } + + if (!content_type.find("application/x-www-form-urlencoded")) { + detail::parse_query_text(req.body, req.params); + } else if(!content_type.find("multipart/form-data")) { + std::string boundary; + if (!detail::parse_multipart_boundary(content_type, boundary) || + !detail::parse_multipart_formdata(boundary, req.body, req.files)) { + res.status = 400; + write_response(strm, last_connection, req, res); + return true; + } + } + } + + if (routing(req, res)) { + if (res.status == -1) { + res.status = 200; + } + } else { + res.status = 404; + } + + write_response(strm, last_connection, req, res); + return true; +} + +inline bool Server::is_valid() const +{ + return true; +} + +inline bool Server::read_and_close_socket(socket_t sock) +{ + return detail::read_and_close_socket( + sock, + keep_alive_max_count_, + [this](Stream& strm, bool last_connection, bool& connection_close) { + return process_request(strm, last_connection, connection_close); + }); +} + +// HTTP client implementation +inline Client::Client( + const char* host, int port, time_t timeout_sec) + : host_(host) + , port_(port) + , timeout_sec_(timeout_sec) + , host_and_port_(host_ + ":" + std::to_string(port_)) +{ +} + +inline Client::~Client() +{ +} + +inline bool Client::is_valid() const +{ + return true; +} + +inline socket_t Client::create_client_socket() const +{ + return detail::create_socket(host_.c_str(), port_, + [=](socket_t sock, struct addrinfo& ai) -> bool { + detail::set_nonblocking(sock, true); + + auto ret = connect(sock, ai.ai_addr, ai.ai_addrlen); + if (ret < 0) { + if (detail::is_connection_error() || + !detail::wait_until_socket_is_ready(sock, timeout_sec_, 0)) { + detail::close_socket(sock); + return false; + } + } + + detail::set_nonblocking(sock, false); + return true; + }); +} + +inline bool Client::read_response_line(Stream& strm, Response& res) +{ + const auto bufsiz = 2048; + char buf[bufsiz]; + + detail::stream_line_reader reader(strm, buf, bufsiz); + + if (!reader.getline()) { + return false; + } + + const static std::regex re("(HTTP/1\\.[01]) (\\d+?) .+\r\n"); + + std::cmatch m; + if (std::regex_match(reader.ptr(), m, re)) { + res.version = std::string(m[1]); + res.status = std::stoi(std::string(m[2])); + } + + return true; +} + +inline bool Client::send(Request& req, Response& res) +{ + if (req.path.empty()) { + return false; + } + + auto sock = create_client_socket(); + if (sock == INVALID_SOCKET) { + return false; + } + + return read_and_close_socket(sock, req, res); +} + +inline void Client::write_request(Stream& strm, Request& req) +{ + auto path = detail::encode_url(req.path); + + // Request line + strm.write_format("%s %s HTTP/1.1\r\n", + req.method.c_str(), + path.c_str()); + + // Headers + req.set_header("Host", host_and_port_.c_str()); + + if (!req.has_header("Accept")) { + req.set_header("Accept", "*/*"); + } + + if (!req.has_header("User-Agent")) { + req.set_header("User-Agent", "cpp-httplib/0.2"); + } + + // TODO: Support KeepAlive connection + // if (!req.has_header("Connection")) { + req.set_header("Connection", "close"); + // } + + if (req.body.empty()) { + if (req.method == "POST" || req.method == "PUT") { + req.set_header("Content-Length", "0"); + } + } else { + if (!req.has_header("Content-Type")) { + req.set_header("Content-Type", "text/plain"); + } + + auto length = std::to_string(req.body.size()); + req.set_header("Content-Length", length.c_str()); + } + + detail::write_headers(strm, req); + + // Body + if (!req.body.empty()) { + if (req.get_header_value("Content-Type") == "application/x-www-form-urlencoded") { + auto str = detail::encode_url(req.body); + strm.write(str.c_str(), str.size()); + } else { + strm.write(req.body.c_str(), req.body.size()); + } + } +} + +inline bool Client::process_request(Stream& strm, Request& req, Response& res, bool& connection_close) +{ + // Send request + write_request(strm, req); + + // Receive response and headers + if (!read_response_line(strm, res) || !detail::read_headers(strm, res.headers)) { + return false; + } + + if (res.get_header_value("Connection") == "close" || res.version == "HTTP/1.0") { + connection_close = true; + } + + // Body + if (req.method != "HEAD") { + if (!detail::read_content(strm, res, req.progress)) { + return false; + } + + if (res.get_header_value("Content-Encoding") == "gzip") { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + detail::decompress(res.body); +#else + return false; +#endif + } + } + + return true; +} + +inline bool Client::read_and_close_socket(socket_t sock, Request& req, Response& res) +{ + return detail::read_and_close_socket( + sock, + 0, + [&](Stream& strm, bool /*last_connection*/, bool& connection_close) { + return process_request(strm, req, res, connection_close); + }); +} + +inline std::shared_ptr Client::Get(const char* path, Progress progress) +{ + return Get(path, Headers(), progress); +} + +inline std::shared_ptr Client::Get(const char* path, const Headers& headers, Progress progress) +{ + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.progress = progress; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::Head(const char* path) +{ + return Head(path, Headers()); +} + +inline std::shared_ptr Client::Head(const char* path, const Headers& headers) +{ + Request req; + req.method = "HEAD"; + req.headers = headers; + req.path = path; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::Post( + const char* path, const std::string& body, const char* content_type) +{ + return Post(path, Headers(), body, content_type); +} + +inline std::shared_ptr Client::Post( + const char* path, const Headers& headers, const std::string& body, const char* content_type) +{ + Request req; + req.method = "POST"; + req.headers = headers; + req.path = path; + + req.headers.emplace("Content-Type", content_type); + req.body = body; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::Post(const char* path, const Params& params) +{ + return Post(path, Headers(), params); +} + +inline std::shared_ptr Client::Post(const char* path, const Headers& headers, const Params& params) +{ + std::string query; + for (auto it = params.begin(); it != params.end(); ++it) { + if (it != params.begin()) { + query += "&"; + } + query += it->first; + query += "="; + query += it->second; + } + + return Post(path, headers, query, "application/x-www-form-urlencoded"); +} + +inline std::shared_ptr Client::Put( + const char* path, const std::string& body, const char* content_type) +{ + return Put(path, Headers(), body, content_type); +} + +inline std::shared_ptr Client::Put( + const char* path, const Headers& headers, const std::string& body, const char* content_type) +{ + Request req; + req.method = "PUT"; + req.headers = headers; + req.path = path; + + req.headers.emplace("Content-Type", content_type); + req.body = body; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::Delete(const char* path) +{ + return Delete(path, Headers()); +} + +inline std::shared_ptr Client::Delete(const char* path, const Headers& headers) +{ + Request req; + req.method = "DELETE"; + req.path = path; + req.headers = headers; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +inline std::shared_ptr Client::Options(const char* path) +{ + return Options(path, Headers()); +} + +inline std::shared_ptr Client::Options(const char* path, const Headers& headers) +{ + Request req; + req.method = "OPTIONS"; + req.path = path; + req.headers = headers; + + auto res = std::make_shared(); + + return send(req, *res) ? res : nullptr; +} + +/* + * SSL Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { + +template +inline bool read_and_close_socket_ssl( + socket_t sock, size_t keep_alive_max_count, + // TODO: OpenSSL 1.0.2 occasionally crashes... + // The upcoming 1.1.0 is going to be thread safe. + SSL_CTX* ctx, std::mutex& ctx_mutex, + U SSL_connect_or_accept, V setup, + T callback) +{ + SSL* ssl = nullptr; + { + std::lock_guard guard(ctx_mutex); + + ssl = SSL_new(ctx); + if (!ssl) { + return false; + } + } + + auto bio = BIO_new_socket(sock, BIO_NOCLOSE); + SSL_set_bio(ssl, bio, bio); + + setup(ssl); + + SSL_connect_or_accept(ssl); + + bool ret = false; + + if (keep_alive_max_count > 0) { + auto count = keep_alive_max_count; + while (count > 0 && + detail::select_read(sock, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, + CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0) { + SSLSocketStream strm(sock, ssl); + auto last_connection = count == 1; + auto connection_close = false; + + ret = callback(strm, last_connection, connection_close); + if (!ret || connection_close) { + break; + } + + count--; + } + } else { + SSLSocketStream strm(sock, ssl); + auto dummy_connection_close = false; + ret = callback(strm, true, dummy_connection_close); + } + + SSL_shutdown(ssl); + + { + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); + } + + close_socket(sock); + + return ret; +} + +class SSLInit { +public: + SSLInit() { + SSL_load_error_strings(); + SSL_library_init(); + } +}; + +static SSLInit sslinit_; + +} // namespace detail + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream(socket_t sock, SSL* ssl) + : sock_(sock), ssl_(ssl) +{ +} + +inline SSLSocketStream::~SSLSocketStream() +{ +} + +inline int SSLSocketStream::read(char* ptr, size_t size) +{ + return SSL_read(ssl_, ptr, size); +} + +inline int SSLSocketStream::write(const char* ptr, size_t size) +{ + return SSL_write(ssl_, ptr, size); +} + +inline int SSLSocketStream::write(const char* ptr) +{ + return write(ptr, strlen(ptr)); +} + +inline std::string SSLSocketStream::get_remote_addr() { + return detail::get_remote_addr(sock_); +} + +// SSL HTTP server implementation +inline SSLServer::SSLServer(const char* cert_path, const char* private_key_path) +{ + ctx_ = SSL_CTX_new(SSLv23_server_method()); + + if (ctx_) { + SSL_CTX_set_options(ctx_, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + // auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + // SSL_CTX_set_tmp_ecdh(ctx_, ecdh); + // EC_KEY_free(ecdh); + + if (SSL_CTX_use_certificate_file(ctx_, cert_path, SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLServer::~SSLServer() +{ + if (ctx_) { + SSL_CTX_free(ctx_); + } +} + +inline bool SSLServer::is_valid() const +{ + return ctx_; +} + +inline bool SSLServer::read_and_close_socket(socket_t sock) +{ + return detail::read_and_close_socket_ssl( + sock, + keep_alive_max_count_, + ctx_, ctx_mutex_, + SSL_accept, + [](SSL* /*ssl*/) {}, + [this](Stream& strm, bool last_connection, bool& connection_close) { + return process_request(strm, last_connection, connection_close); + }); +} + +// SSL HTTP client implementation +inline SSLClient::SSLClient(const char* host, int port, time_t timeout_sec) + : Client(host, port, timeout_sec) +{ + ctx_ = SSL_CTX_new(SSLv23_client_method()); +} + +inline SSLClient::~SSLClient() +{ + if (ctx_) { + SSL_CTX_free(ctx_); + } +} + +inline bool SSLClient::is_valid() const +{ + return ctx_; +} + +inline bool SSLClient::read_and_close_socket(socket_t sock, Request& req, Response& res) +{ + return is_valid() && detail::read_and_close_socket_ssl( + sock, 0, + ctx_, ctx_mutex_, + SSL_connect, + [&](SSL* ssl) { + SSL_set_tlsext_host_name(ssl, host_.c_str()); + }, + [&](Stream& strm, bool /*last_connection*/, bool& connection_close) { + return process_request(strm, req, res, connection_close); + }); +} +#endif + +} // namespace httplib + +#endif + +// vim: et ts=4 sw=4 cin cino={1s ff=unix diff --git a/handlers/handler.cpp b/handlers/handler.cpp new file mode 100644 index 0000000..8203661 --- /dev/null +++ b/handlers/handler.cpp @@ -0,0 +1,79 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handler.h" + + +void Handler::setGeneralVars(TemplatePage &page) +{ + if(userSession->loggedIn) + { + page.setVar("loginstatus", "Logged in as " + userSession->user.login); + } + else + { + page.setVar("loginstatus", "not logged in"); + } + page.setVar("csrf_token", utils::toString(this->userSession->csrf_token)); +} +Response Handler::errorResponse(std::string errortitle, std::string errormessage, int status) +{ + TemplatePage &error = this->templ->getPage("error"); + error.setVar("errortitle", errortitle); + error.setVar("errormessage", errormessage); + //TODO: log? + setGeneralVars(error); + return { status, error.render()}; +} + +QueryOption Handler::queryOption(const Request &r) const +{ + QueryOption result; + result.includeInvisible = false; + try + { + result.limit = utils::toUInt(r.get("limit")); + + } + catch(std::exception &e) + { + result.limit = 0; + } + try + { + result.offset = utils::toUInt(r.get("offset")); + + } + catch(std::exception &e) + { + result.offset = 0; + } + std::string order = r.get("sort"); + if(order == "0") + { + result.order = ASCENDING; + } + else + { + result.order = DESCENDING; + } + + return result; +} diff --git a/handlers/handler.h b/handlers/handler.h new file mode 100644 index 0000000..641cc5c --- /dev/null +++ b/handlers/handler.h @@ -0,0 +1,37 @@ +#ifndef HANDLER_H +#define HANDLER_H + +#include "../response.h" +#include "../request.h" +#include "../template.h" +#include "../database/database.h" +#include "../urlprovider.h" +#include "../database/queryoption.h" +#include "../logger.h" +#include "../cache/icache.h" +class Handler +{ +protected: + ICache *cache; + Template *templ; + Database *database; + Session *userSession; + UrlProvider *urlProvider; + + QueryOption queryOption(const Request &r) const; +public: + Handler(Template &templ, Database &db, Session &userSession, UrlProvider &provider, ICache &cache) + { + this->templ = &templ; + this->database = &db; + this->userSession = &userSession; + this->urlProvider = &provider; + this->cache = &cache; + } + virtual Response handle(const Request &r) = 0; + void setGeneralVars(TemplatePage &page); + virtual ~Handler() { } + Response errorResponse(std::string errortitle, std::string errormessage, int status = 200); +}; + +#endif // HANDLER_H diff --git a/handlers/handlerallcategories.cpp b/handlers/handlerallcategories.cpp new file mode 100644 index 0000000..4868889 --- /dev/null +++ b/handlers/handlerallcategories.cpp @@ -0,0 +1,44 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerallcategories.h" +#include "../urlprovider.h" +#include "../logger.h" +Response HandlerAllCategories::handle(const Request &r) +{ + auto categoryDao = this->database->createCategoryDao(); + QueryOption qo = queryOption(r); + auto resultList = categoryDao->fetchList(qo); + if(resultList.size() == 0) + { + return errorResponse("No categories", "This wiki does not have any categories defined yet or your query options did not yield any results"); + } + TemplatePage &searchPage = this->templ->getPage("allcategories"); + std::string body = this->templ->renderSearch(resultList, [&](std::string str) { + return this->urlProvider->category(str); +}); + searchPage.setVar("categorylist", body); + setGeneralVars(searchPage); + + Response response; + response.setBody(searchPage.render()); + response.setStatus(200); + return response; +} diff --git a/handlers/handlerallcategories.h b/handlers/handlerallcategories.h new file mode 100644 index 0000000..010f1ba --- /dev/null +++ b/handlers/handlerallcategories.h @@ -0,0 +1,15 @@ +#ifndef HANDLERALLCATEGORIES_H +#define HANDLERALLCATEGORIES_H + + +#include "handler.h" + +class HandlerAllCategories : public Handler +{ +public: + HandlerAllCategories(); + using Handler::Handler; + Response handle(const Request &r) override; +}; + +#endif // HANDLERALLCATEGORIES_H diff --git a/handlers/handlerallpages.cpp b/handlers/handlerallpages.cpp new file mode 100644 index 0000000..329ef5f --- /dev/null +++ b/handlers/handlerallpages.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerallpages.h" + + + +Response HandlerAllPages::handle(const Request &r) +{ + try + { + Response response; + auto pageDao = this->database->createPageDao(); + QueryOption qo = queryOption(r); + auto resultList = pageDao->getPageList(qo); + if(resultList.size() == 0) + { + return errorResponse("No pages", "This wiki does not have any pages yet"); + } + TemplatePage &searchPage = this->templ->getPage("allpages"); + std::string body = this->templ->renderSearch(resultList); + searchPage.setVar("pagelist", body); + setGeneralVars(searchPage); + response.setBody(searchPage.render()); + response.setStatus(200); + return response; + } + catch(std::exception &e) + { + Logger::error() << "Error during allpages Handler" << e.what(); + return errorResponse("Error", "An unknown error occured"); + } + +} diff --git a/handlers/handlerallpages.h b/handlers/handlerallpages.h new file mode 100644 index 0000000..1fb9ed5 --- /dev/null +++ b/handlers/handlerallpages.h @@ -0,0 +1,13 @@ +#ifndef HANDLERALLPAGES_H +#define HANDLERALLPAGES_H + +#include "handler.h" +class HandlerAllPages : public Handler +{ +public: + HandlerAllPages(); + using Handler::Handler; + Response handle(const Request &r) override; +}; + +#endif // HANDLERALLPAGES_H diff --git a/handlers/handlercategory.cpp b/handlers/handlercategory.cpp new file mode 100644 index 0000000..8d600af --- /dev/null +++ b/handlers/handlercategory.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlercategory.h" + + +Response HandlerCategory::handle(const Request &r) +{ + try + { + Response response; + std::string categoryname = r.get("category"); + auto categoryDao = this->database->createCategoryDao(); + if(! categoryDao->find(categoryname)) + { + return this->errorResponse("No such category", "A category with the provided name does not exist", 404); + } + QueryOption qo = queryOption(r); + auto resultList = categoryDao->fetchMembers(categoryname, qo); + TemplatePage &searchPage = this->templ->getPage("show_category"); + std::string body = this->templ->renderSearch(resultList); + searchPage.setVar("pagelist", body); + searchPage.setVar("categoryname", categoryname); + setGeneralVars(searchPage); + response.setBody(searchPage.render()); + response.setStatus(200); + return response; + } + catch(std::exception &e) + { + Logger::error() << "Error during category Handler" << e.what(); + return errorResponse("Error", "An unknown error occured"); + } +} diff --git a/handlers/handlercategory.h b/handlers/handlercategory.h new file mode 100644 index 0000000..8bbf10c --- /dev/null +++ b/handlers/handlercategory.h @@ -0,0 +1,13 @@ +#ifndef HANDLERCATEGORY_H +#define HANDLERCATEGORY_H +#include "handler.h" + +class HandlerCategory : public Handler +{ +public: + HandlerCategory(); + using Handler::Handler; + Response handle(const Request &r) override; +}; + +#endif // HANDLERCATEGORY_H diff --git a/handlers/handlerdefault.cpp b/handlers/handlerdefault.cpp new file mode 100644 index 0000000..ec60b2e --- /dev/null +++ b/handlers/handlerdefault.cpp @@ -0,0 +1,32 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerdefault.h" + + +Response HandlerDefault::handle(const Request &r) +{ + return Response::redirectTemporarily(this->urlProvider->index()); +} + +HandlerDefault::~HandlerDefault() +{ + +} diff --git a/handlers/handlerdefault.h b/handlers/handlerdefault.h new file mode 100644 index 0000000..15547e2 --- /dev/null +++ b/handlers/handlerdefault.h @@ -0,0 +1,13 @@ +#ifndef HANDLERDEFAULT_H +#define HANDLERDEFAULT_H + +#include "handler.h" +class HandlerDefault : public Handler +{ +public: + Response handle(const Request &r) override; + ~HandlerDefault() override; + using Handler::Handler; +}; + +#endif // HANDLERDEFAULT_H diff --git a/handlers/handlerfactory.cpp b/handlers/handlerfactory.cpp new file mode 100644 index 0000000..8fa7448 --- /dev/null +++ b/handlers/handlerfactory.cpp @@ -0,0 +1,101 @@ + /* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerfactory.h" +#include "handler.h" +#include "handlerdefault.h" +#include "handlerpageview.h" +#include "handlerinvalidaction.h" +#include "handlerlogin.h" +#include "handlerpageedit.h" +#include "handlersearch.h" +#include "handlerallpages.h" +#include "handlerallcategories.h" +#include "handlercategory.h" +#include "handlerhistory.h" +#include "handlerpagedelete.h" +class Factory +{ + Template &templ; + Database &db; + Session &userSession; + UrlProvider &urlProvider; + ICache &cache; +public: + + Factory(Template &templ, Database &db, Session &usersession, UrlProvider &urlprovider, ICache &cache) : templ(templ) ,db(db), userSession(usersession), urlProvider(urlprovider), cache(cache) { } + + template + inline std::unique_ptr produce() + { + return std::make_unique(templ, db, userSession, urlProvider, cache); + } +}; + +std::unique_ptr createHandler(const std::string &action, Template &templ, Database + &db, Session &usersession, UrlProvider &urlprovider, ICache &cache) +{ + + Factory producer(templ, db, usersession, urlprovider, cache); + + if(action == "" || action == "index") + { + return producer.produce(); + } + if(action == "show") + { + return producer.produce(); + } + if(action == "edit") + { + return producer.produce(); + } + if(action == "login") + { + return producer.produce(); + } + if(action == "search") + { + return producer.produce(); + } + if(action == "delete") + { + return producer.produce(); + } + if(action == "allpages") + { + return producer.produce(); + } + if(action == "allcategories") + { + return producer.produce(); + } + if(action == "showcat") + { + return producer.produce(); + } + if(action == "recent") + { + return producer.produce(); + } + + + return producer.produce(); +} diff --git a/handlers/handlerfactory.h b/handlers/handlerfactory.h new file mode 100644 index 0000000..5281ff7 --- /dev/null +++ b/handlers/handlerfactory.h @@ -0,0 +1,8 @@ +#ifndef HANDLERFACTORY_H +#define HANDLERFACTORY_H +#include +#include "handler.h" +#include "../template.h" + +std::unique_ptr createHandler(const std::string &action, Template &templ, Database &db, Session &usersession, UrlProvider &urlprovider, ICache &cache); +#endif // HANDLERFACTORY_H diff --git a/handlers/handlerhistory.cpp b/handlers/handlerhistory.cpp new file mode 100644 index 0000000..a1151ce --- /dev/null +++ b/handlers/handlerhistory.cpp @@ -0,0 +1,106 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerhistory.h" +#include "handler.h" +#include "../htmllink.h" +#include "../logger.h" +#include "../database/exceptions.h" +Response HandlerHistory::handle(const Request &r) +{ + QueryOption qo = queryOption(r); + std::string page = r.get("page"); + + unsigned int count = 0; + std::vector resultList; + auto revisionDao = this->database->createRevisionDao(); + + auto makeSortedLink = [&](unsigned int limit, unsigned int offset, unsigned int order) + { + if(!page.empty()) + { + return this->urlProvider->pageHistorySort(page, limit, offset, order); + } + return this->urlProvider->recentSorted(limit, offset, order); + }; + std::string templatename = "recentchanges"; + try + { + if(!page.empty()) + { + auto pageDao = this->database->createPageDao(); + if(!pageDao->exists(page)) + { + return errorResponse("No such page", "No such page exists to show history for", 404); + } + count = revisionDao->countTotalRevisions(page); + resultList = revisionDao->getAllRevisionsForPage(page, qo); + templatename = "page_history"; + + } + else + { + count = revisionDao->countTotalRevisions(); + if(count == 0) + { + return errorResponse("No revisions", "This wiki does not have any pages with revisions yet"); + } + resultList = revisionDao->getAllRevisions(qo); + } + } + catch(const DatabaseException &e) + { + Logger::error() << "DatabaseException in handlerhistory: " << e.what(); + return errorResponse("Database error", "While trying to fetch revision list, a database error occured"); + } + TemplatePage historyPage = this->templ->getPage(templatename); + setGeneralVars(historyPage); + + if( (qo.offset + (unsigned int)resultList.size()) < count) + { + HtmlLink link; + link.href = makeSortedLink(qo.limit, qo.offset + qo.limit, qo.order); + link.innervalue = "Next page"; + + historyPage.setVar("nextpage", link.render()); + } + + unsigned int prevoffset = qo.offset - qo.limit; + if(prevoffset > count) + { + prevoffset = 0; + } + if(qo.offset > 0 && qo.offset < count) + { + HtmlLink link; + link.href = makeSortedLink(qo.limit, prevoffset, qo.order); + link.innervalue = "Previous page"; + + historyPage.setVar("prevpage", link.render()); + } + + unsigned int neworder = ( qo.order == DESCENDING ) ? ASCENDING : DESCENDING ; + historyPage.setVar("linkrecentsort", makeSortedLink(qo.limit, qo.offset, neworder)); + historyPage.setVar("revisionlist", this->templ->renderRevisionList(resultList, page.empty())); + Response response; + response.setBody(historyPage.render()); + response.setStatus(200); + return response; +} diff --git a/handlers/handlerhistory.h b/handlers/handlerhistory.h new file mode 100644 index 0000000..fea08a6 --- /dev/null +++ b/handlers/handlerhistory.h @@ -0,0 +1,14 @@ +#ifndef HANDLERHISTORY_H +#define HANDLERHISTORY_H +#include "handler.h" + +class HandlerHistory : public Handler +{ + +public: + HandlerHistory(); + using Handler::Handler; + Response handle(const Request &r) override; +}; + +#endif // HANDLERHISTORY_H diff --git a/handlers/handlerinvalidaction.cpp b/handlers/handlerinvalidaction.cpp new file mode 100644 index 0000000..58c1e7c --- /dev/null +++ b/handlers/handlerinvalidaction.cpp @@ -0,0 +1,27 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerinvalidaction.h" + + +Response HandlerInvalidAction::handle(const Request &r) +{ + return errorResponse("Invalid action", "No action defined for this action"); +} diff --git a/handlers/handlerinvalidaction.h b/handlers/handlerinvalidaction.h new file mode 100644 index 0000000..37690c4 --- /dev/null +++ b/handlers/handlerinvalidaction.h @@ -0,0 +1,14 @@ +#ifndef HANDLERINVALIDACTION_H +#define HANDLERINVALIDACTION_H +#include "handler.h" + +class HandlerInvalidAction : public Handler +{ +public: + Response handle(const Request &r) override; + ~HandlerInvalidAction() override { } + using Handler::Handler; +}; + + +#endif // HANDLERINVALIDACTION_H diff --git a/handlers/handlerlogin.cpp b/handlers/handlerlogin.cpp new file mode 100644 index 0000000..5c84f9b --- /dev/null +++ b/handlers/handlerlogin.cpp @@ -0,0 +1,123 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include "handlerlogin.h" +#include "../logger.h" +struct LoginFail +{ + unsigned int count; + time_t lastfail; +}; +static std::map loginFails; + +//TODO: make configurable +bool HandlerLogin::isBanned(std::string ip) +{ + if(utils::hasKey(loginFails, ip)) + { + LoginFail &fl = loginFails[ip]; + return fl.count > 5 && (time(nullptr) - fl.lastfail) < 1200; + + } + return false; +} + +void HandlerLogin::incFailureCount(std::string ip) +{ + LoginFail &fl = loginFails[ip]; + fl.count+=1; + fl.lastfail = time(nullptr); +} + +std::vector HandlerLogin::pbkdf5(std::string password, const std::vector &salt) +{ + unsigned char hash[32]; + const EVP_MD *sha256 = EVP_sha256(); + const unsigned char *rawsalt = reinterpret_cast(salt.data()); + PKCS5_PBKDF2_HMAC(password.c_str(), password.size(), rawsalt, salt.size(), 300000, sha256, sizeof(hash), hash); + + std::vector result; + + for(size_t i = 0; i < sizeof(hash); i++) + { + + result.push_back(static_cast(hash[i])); + } + + return result; +} + + +Response HandlerLogin::handle(const Request &r) +{ + auto createErrorReesponse = [&]() { return errorResponse("Login error", "The supplied credenetials are incorrect"); }; + + if(isBanned(r.getIp())) + { + return errorResponse("Banned", "You have been banned for too many login attempts. Try again later"); + } + if(r.param("submit") == "1") + { + std::string password = r.post("password"); + std::string username = r.post("user"); + + auto userDao = this->database->createUserDao(); + std::optional user = userDao->find(username); + if(!user) + { + return createErrorReesponse(); + } + + auto hashresult = pbkdf5(password, user.value().salt); + //TODO: timing attack + if(hashresult == user.value().password) + { + loginFails.erase(r.getIp()); + Response r = Response::redirectTemporarily(urlProvider->index()); + *(this->userSession) = Session(user.value()); + return r; + + } + else + { + //TODO: only if wanted by config + incFailureCount(r.getIp()); + return createErrorReesponse(); + } + + + // auto pbkdf5 = pbkdf5(password, user->) + + + + } + std::string page = r.get("page"); + if(page.empty()) + page = "index"; + + TemplatePage &loginTemplatePage = this->templ->getPage("login"); + setGeneralVars(loginTemplatePage); + loginTemplatePage.setVar("loginurl", urlProvider->login(page)); + Response result; + result.setStatus(200); + result.setBody(loginTemplatePage.render()); + return result; +} diff --git a/handlers/handlerlogin.h b/handlers/handlerlogin.h new file mode 100644 index 0000000..7ffd250 --- /dev/null +++ b/handlers/handlerlogin.h @@ -0,0 +1,19 @@ +#ifndef HANDLERLOGIN_H +#define HANDLERLOGIN_H +#include +#include "handler.h" + +class HandlerLogin : public Handler +{ +private: + bool isBanned(std::string ip); + void incFailureCount(std::string ip); + std::vector pbkdf5(std::string password, const std::vector &salt); +public: + HandlerLogin(); + Response handle(const Request &r) override; + ~HandlerLogin() override { } + using Handler::Handler; +}; + +#endif // HANDERLOGIN_H diff --git a/handlers/handlerpage.cpp b/handlers/handlerpage.cpp new file mode 100644 index 0000000..6f693eb --- /dev/null +++ b/handlers/handlerpage.cpp @@ -0,0 +1,94 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerpage.h" + + +Response HandlerPage::handle(const Request &r) +{ + std::string pagename = r.get("page"); + auto pageDao = this->database->createPageDao(); + if(pagename.empty()) + { + return errorResponse("No page given", "No page given to request"); + } + + if(pageMustExist() && !pageDao->exists(pagename)) + { + std::string createlink = this->urlProvider->editPage(pagename); + return errorResponse("Page not found", "The requested page was not found. Do you want to create it?", 404); + } + + if(!canAccess(pagename)) + { + return errorResponse("Permission denied", accessErrorMessage()); + } + + return this->handleRequest(*pageDao, pagename, r); +} + +std::string HandlerPage::accessErrorMessage() +{ + return "You don't have permission to access this page"; +} + +bool HandlerPage::pageMustExist() +{ + return true; +} + +void HandlerPage::setPageVars(TemplatePage &page, std::string pagename) +{ + setGeneralVars(page); + + if(!pagename.empty()) + { + std::string headerlinks; + TemplatePage &headerlink = this->templ->getPage("_headerlink"); + auto addHeaderLink = [&headerlinks, &headerlink](std::string href, std::string value) + { + headerlink.setVar("href", href); + headerlink.setVar("value", value); + headerlinks += headerlink.render(); + }; + Permissions &perms = this->userSession->user.permissions; + + if(perms.canEdit()) + { + addHeaderLink(this->urlProvider->editPage(pagename), "Edit"); + addHeaderLink(this->urlProvider->pageSettings(pagename), "Page settings"); + } + if(perms.canDelete()) + { + addHeaderLink(this->urlProvider->pageDelete(pagename), "Delete"); + } + if(perms.canSeePageHistory()) + { + addHeaderLink(this->urlProvider->pageHistory(pagename), "Show history"); + } + + page.setVar("headerlinks", headerlinks); + page.setVar("page", pagename); + } + + + + +} diff --git a/handlers/handlerpage.h b/handlers/handlerpage.h new file mode 100644 index 0000000..0d6ec20 --- /dev/null +++ b/handlers/handlerpage.h @@ -0,0 +1,21 @@ +#ifndef HANDLERPAGE_H +#define HANDLERPAGE_H +#include "handler.h" + +class HandlerPage : public Handler +{ +protected: + virtual bool canAccess(std::string page) = 0; + virtual bool pageMustExist(); + virtual std::string accessErrorMessage(); + +public: + Response handle(const Request &r) override; + virtual Response handleRequest(PageDao &pageDao, std::string pagename, const Request &r) =0; + ~HandlerPage() override { } + using Handler::Handler; + + void setPageVars(TemplatePage &page, std::string pagename); +}; + +#endif // HANDLERPAGE_H diff --git a/handlers/handlerpagedelete.cpp b/handlers/handlerpagedelete.cpp new file mode 100644 index 0000000..b429fdd --- /dev/null +++ b/handlers/handlerpagedelete.cpp @@ -0,0 +1,51 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerpagedelete.h" +#include "../database/exceptions.h" + +Response HandlerPageDelete::handleRequest(PageDao &pageDao, std::string pagename, const Request &r) +{ + try + { + + if(r.getRequestMethod() == "POST") + { + pageDao.deletePage(pagename); + this->cache->removePrefix("page:"); //TODO: overkill? + return Response::redirectTemporarily(this->urlProvider->index()); + + + } + TemplatePage delPage = this->templ->getPage("page_deletion"); + delPage.setVar("deletionurl", this->urlProvider->pageDelete(pagename)); + setPageVars(delPage, pagename); + Response r; + r.setBody(delPage.render()); + return r; + } + catch(const DatabaseException &e) + { + Logger::debug() << "Error delete page: " << e.what(); + return errorResponse("Database error", "A database error occured while trying to delete this page"); + } + + +} diff --git a/handlers/handlerpagedelete.h b/handlers/handlerpagedelete.h new file mode 100644 index 0000000..d565ba2 --- /dev/null +++ b/handlers/handlerpagedelete.h @@ -0,0 +1,26 @@ +#ifndef HANDLERPAGEDELETE_H +#define HANDLERPAGEDELETE_H +#include "handlerpage.h" + +class HandlerPageDelete : public HandlerPage +{ + bool pageMustExist() override + { + return true; + } + + bool canAccess(std::string page) override + { + return this->userSession->user.permissions.canDelete(); + } + + std::string accessErrorMessage() override + { + return "You don't have permission to delete pages"; + } +public: + Response handleRequest(PageDao &pageDao, std::string pagename, const Request &r) override; + using HandlerPage::HandlerPage; +}; + +#endif // HANDLERPAGEDELETE_H diff --git a/handlers/handlerpageedit.cpp b/handlers/handlerpageedit.cpp new file mode 100644 index 0000000..8d564a1 --- /dev/null +++ b/handlers/handlerpageedit.cpp @@ -0,0 +1,122 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerpageedit.h" +#include "../database/exceptions.h" +#include "../request.h" + +#include "../parser.h" +bool HandlerPageEdit::canAccess(std::string page) +{ + return this->userSession->user.permissions.canEdit(); +} + +bool HandlerPageEdit::pageMustExist() +{ + return false; +} + + +Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename, const Request &r) +{ + bool pageexists = pageDao.exists(pagename); + if(!pageexists && !this->userSession->user.permissions.canCreate()) + { + return errorResponse("No permission", "You don't have permission to create new pages"); + } + auto revisiondao = this->database->createRevisionDao(); + auto revision = this->database->createRevisionDao()->getCurrentForPage(pagename); + std::string body; + + if(revision) + { + body = revision->content; + } + if(r.getRequestMethod() == "POST") + { + if(r.post("do") == "submit") + { + std::string newContent = r.post("content"); + std::string newComment = r.post("comment"); + + Revision newRevision; + newRevision.author = this->userSession->user.login; + newRevision.comment = newComment; + newRevision.page = pagename; + newRevision.content = newContent; + + + //TODO: must check, whether categories differ, and perhaps don't allow every user + //to set categories + Parser parser; + std::vector cats = parser.extractCategories(newContent); + try + { + this->database->beginTransaction(); + if(!pageexists) + { + Page newPage; + newPage.current_revision = 0; + newPage.listed = true; + newPage.name = pagename; + pageDao.save(newPage); + + } + revisiondao->save(newRevision); + pageDao.setCategories(pagename, cats); + this->database->commitTransaction(); + this->cache->removePrefix("page:"); //TODO: overkill? + } + catch(const DatabaseException &e) + { + Logger::debug() << "Error saving revision: " << e.what(); + return errorResponse("Database error", "A database error occured while trying to save this revision"); + } + + return Response::redirectTemporarily(urlProvider->page(pagename)); + + + } + if(r.post("do") == "preview") + { + std::string newContent = r.post("content"); + Parser parser; + TemplatePage templatePage = this->templ->getPage("page_creation_preview"); + templatePage.setVar("actionurl", urlProvider->editPage(pagename)); + templatePage.setVar("preview_content", parser.parse(pageDao, *this->urlProvider, newContent) ); + templatePage.setVar("content", newContent); + setPageVars(templatePage, pagename); + + Response response; + response.setBody(templatePage.render()); + return response; + + } + } + + TemplatePage &templatePage = this->templ->getPage("page_creation"); + templatePage.setVar("actionurl", urlProvider->editPage(pagename)); + templatePage.setVar("content", body); + setPageVars(templatePage, pagename); + Response response; + response.setBody(templatePage.render()); + return response; + +} diff --git a/handlers/handlerpageedit.h b/handlers/handlerpageedit.h new file mode 100644 index 0000000..66db5b7 --- /dev/null +++ b/handlers/handlerpageedit.h @@ -0,0 +1,20 @@ +#ifndef HANDLERPAGEEDI_H +#define HANDLERPAGEEDI_H + +#include "handlerpage.h" +#include "../page.h" + + +class HandlerPageEdit : public HandlerPage +{ +protected: + bool pageMustExist() override; + bool canAccess(std::string page) override; +public: + Response handleRequest(PageDao &pageDao, std::string pagename, const Request &r) override; + + ~HandlerPageEdit() override { } + using HandlerPage::HandlerPage; +}; + +#endif // HANDLERPAGEEDI_H diff --git a/handlers/handlerpageview.cpp b/handlers/handlerpageview.cpp new file mode 100644 index 0000000..a15177c --- /dev/null +++ b/handlers/handlerpageview.cpp @@ -0,0 +1,170 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlerpageview.h" +#include "../database/exceptions.h" +#include "../logger.h" +#include "../parser.h" +#include "../htmllink.h" + +bool HandlerPageView::canAccess(std::string page) +{ + return this->userSession->user.permissions.canRead(); +} + +std::string HandlerPageView::createIndexContent(IParser &parser, std::string content) +{ + std::vector headlines = parser.extractHeadlines(content); + std::string indexcontent = ""; + unsigned int l = 0; + for(const Headline &h : headlines) + { + if(h.level > l) + { + indexcontent += "
    "; + } + else if(h.level < l) + { + indexcontent += "
"; + } + l = h.level; + HtmlLink link; + link.href="#" + h.title; + link.innervalue = h.title; + link.cssclass = "indexlink"; + indexcontent += "
  • "+link.render()+"
  • "; + } + indexcontent += ""; + return indexcontent; +} + + +Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename, const Request &r) +{ + + std::string revisionparam = r.get("revision"); + unsigned int revisionid=0; + if(!revisionparam.empty()) + { + try + { + revisionid = utils::toUInt(revisionparam); + } + catch(const std::exception &e) + { + return errorResponse("Error", "Supplied revisionid is misformated"); + } + } + + std::optional revision; + std::string templatepartname; + try + { + if(revisionid > 0 ) + { + revision = this->database->createRevisionDao()->getRevisionForPage(pagename, revisionid); + if(!revision) + { + return errorResponse("Revision not found", "No such revision found"); + } + templatepartname = "page_view_revision"; + } + else + { + if(! this->userSession->loggedIn) + { + auto content = this->cache->get("page:foranon:" + pagename); + if(content) + { + Response r; + r.setBody(*content); + //TODO: etag? + return r; + } + } + revision = this->database->createRevisionDao()->getCurrentForPage(pagename); + templatepartname = "page_view"; + } + + } + catch(const DatabaseException &e) + { + Logger::error() << "DatabaseException in handlerpageview: " << e.what(); + return errorResponse("Database error", "While trying to fetch revision, a database error occured"); + } + + + TemplatePage &page = this->templ->getPage(templatepartname); + + Parser parser; + Response result; + result.setStatus(200); + std::string indexcontent; + std::string parsedcontent; + + if(revisionid > 0) + { + indexcontent = createIndexContent(parser, revision->content); + parsedcontent = parser.parse(pageDao, *this->urlProvider, revision->content); + } + else + { + std::string cachekeyindexcontent = "page:indexcontent:" + pagename; + std::string cachekeyparsedcontent = "page:parsedcontent:" + pagename; + auto cachedindexcontent = this->cache->get(cachekeyindexcontent); + auto cachedparsedcontent = this->cache->get(cachekeyparsedcontent); + if(cachedindexcontent) + { + indexcontent = *cachedindexcontent; + } + else + { + indexcontent = createIndexContent(parser, revision->content); + this->cache->put(cachekeyindexcontent, indexcontent); + } + if(cachedparsedcontent) + { + parsedcontent = *cachedparsedcontent; + } + else + { + parsedcontent = parser.parse(pageDao, *this->urlProvider, revision->content); + this->cache->put(cachekeyparsedcontent, parsedcontent); + + } + } + page.setVar("content", parsedcontent); + page.setVar("index", indexcontent); + page.setVar("editedby", revision->author ); + page.setVar("editedon", utils::toISODate(revision->timestamp)); + page.setVar("historyurl", this->urlProvider->pageHistory(pagename)); + page.setVar("revision", revisionparam); + setPageVars(page, pagename); + std::string body = page.render(); + if(revisionid == 0 && ! this->userSession->loggedIn) + { + this->cache->put("page:foranon:" + pagename, body); + } + result.addHeader("ETAG", std::to_string(revision->revision)+ "+" + std::to_string(this->userSession->loggedIn)); + result.setBody(body); + return result; + + +} diff --git a/handlers/handlerpageview.h b/handlers/handlerpageview.h new file mode 100644 index 0000000..fd8485c --- /dev/null +++ b/handlers/handlerpageview.h @@ -0,0 +1,25 @@ +#ifndef HANDLERPAGEVIEW_H +#define HANDLERPAGEVIEW_H + +#include "handler.h" +#include "handlerpage.h" +#include "../page.h" +#include "../iparser.h" +class HandlerPageView : public HandlerPage +{ +protected: + bool canAccess(std::string page) override; + std::string accessErrorMessage() override + { + return "You don't have permission to view this page"; + } + std::string createIndexContent(IParser &parser, std::string content); + +public: + Response handleRequest(PageDao &pageDao, std::string pagename, const Request &r) override; + ~HandlerPageView() override { } + using HandlerPage::HandlerPage; +}; + + +#endif // HANDLERPAGEVIEW_H diff --git a/handlers/handlersearch.cpp b/handlers/handlersearch.cpp new file mode 100644 index 0000000..2f7df6a --- /dev/null +++ b/handlers/handlersearch.cpp @@ -0,0 +1,63 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "handlersearch.h" +Response HandlerSearch::handle(const Request &r) +{ + Response response; + std::string q = r.get("q"); + if(q.empty()) + { + return errorResponse("Missing search term", "No search term supplied"); + } + + for(int x : q) + { + if(!isalnum(x) && !isspace(x)) + { + return errorResponse("Invalid char", "Currently, the search is limited and so only supports alpha numeric characters and spaces"); + } + } + auto pageDao = this->database->createPageDao(); + QueryOption qo = queryOption(r); + try + { + auto resultList = pageDao->search(q, qo); + if(resultList.size() == 0) + { + return errorResponse("No results", "Your search for " + q + " did not yield any results."); + } + TemplatePage &searchPage = this->templ->getPage("search"); + std::string body = this->templ->renderSearch(resultList); + searchPage.setVar("pagelist", body); + searchPage.setVar("searchterm", q); + setGeneralVars(searchPage); + response.setBody(searchPage.render()); + response.setStatus(200); + return response; + } + catch(std::exception &e) + { + Logger::error() << "Search failed, q: " << q << "Error: " << e.what(); + return errorResponse("Technical Error", "The system failed to perform your search"); + } + +} + diff --git a/handlers/handlersearch.h b/handlers/handlersearch.h new file mode 100644 index 0000000..0d166b0 --- /dev/null +++ b/handlers/handlersearch.h @@ -0,0 +1,13 @@ +#ifndef HANDLERSEARCH_H +#define HANDLERSEARCH_H +#include +#include "handler.h" +class HandlerSearch : public Handler +{ +public: + HandlerSearch(); + using Handler::Handler; + Response handle(const Request &r) override; +}; + +#endif // HANDLERSEARCH_H diff --git a/headline.cpp b/headline.cpp new file mode 100644 index 0000000..7153779 --- /dev/null +++ b/headline.cpp @@ -0,0 +1,21 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "headline.h" diff --git a/headline.h b/headline.h new file mode 100644 index 0000000..782710c --- /dev/null +++ b/headline.h @@ -0,0 +1,12 @@ +#ifndef HEADLINE_H +#define HEADLINE_H + +#include +class Headline +{ +public: + unsigned int level; + std::string title; +}; + +#endif // HEADLINE_H diff --git a/htmllink.cpp b/htmllink.cpp new file mode 100644 index 0000000..a4aeb58 --- /dev/null +++ b/htmllink.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "htmllink.h" + +HtmlLink::HtmlLink() +{ + +} diff --git a/htmllink.h b/htmllink.h new file mode 100644 index 0000000..335eee8 --- /dev/null +++ b/htmllink.h @@ -0,0 +1,21 @@ +#ifndef HTMLLINK_H +#define HTMLLINK_H +#include + + +class HtmlLink +{ +public: + HtmlLink(); + std::string href; + std::string innervalue; + std::string cssclass; + + std::string render() + { + return "" + innervalue + ""; + } + +}; + +#endif // HTMLLINK_H diff --git a/iparser.h b/iparser.h new file mode 100644 index 0000000..4b14060 --- /dev/null +++ b/iparser.h @@ -0,0 +1,18 @@ +#ifndef IPARSER_H +#define IPARSER_H +#include +#include +#include "headline.h" +#include "database/pagedao.h" +#include "urlprovider.h" +class IParser +{ +public: + virtual std::vector extractHeadlines(std::string content) const = 0; + virtual std::string parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const = 0; + virtual std::vector extractCategories(std::string content) const = 0; + + virtual ~IParser() { }; +}; + +#endif // PARSER_H diff --git a/logger.cpp b/logger.cpp new file mode 100644 index 0000000..06077be --- /dev/null +++ b/logger.cpp @@ -0,0 +1,23 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "logger.h" +std::ostream* Logger::out = &std::cerr; +int Logger::logLevel = 3; diff --git a/logger.h b/logger.h new file mode 100644 index 0000000..3e278f0 --- /dev/null +++ b/logger.h @@ -0,0 +1,72 @@ +#ifndef LOGGER_H +#define LOGGER_H +#include +#include +class Logger +{ +private: + class LogEntry + { + bool headerSent; + std::ostream *out; + std::string prefix; + public: + LogEntry(std::ostream *out, std::string prefix) : out(out), prefix(prefix) {} + + template + LogEntry & operator <<(const T &val) + { + if(out == nullptr) + return *this; + if(!headerSent) + { + (*out) << time(0) << " " << prefix; + } + (*out) << val; + headerSent = true; + return *this; //or maybe out itself? probably not. + } + ~LogEntry() + { + if(out != nullptr) + { + (*out) << std::endl; + (*out).flush(); + } + } + }; + + + + public: + static std::ostream *out; + static int logLevel; + static void setStream(std::ostream *out) + { + Logger::out = out; + } + + static LogEntry debug() + { + if(Logger::logLevel >= 3) + return LogEntry(out, "Debug: "); + + return LogEntry(nullptr, ""); + } + + static LogEntry error() + { + return LogEntry(out, "Error: "); + } + + static LogEntry log() + { + if(Logger::logLevel >= 2) + return LogEntry(out, "Log: "); + + return LogEntry(nullptr, ""); + + } +}; + +#endif // LOGGER_H diff --git a/page.cpp b/page.cpp new file mode 100644 index 0000000..aa3e641 --- /dev/null +++ b/page.cpp @@ -0,0 +1,26 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "page.h" + +Page::Page() +{ + +} diff --git a/page.h b/page.h new file mode 100644 index 0000000..e9b6b97 --- /dev/null +++ b/page.h @@ -0,0 +1,15 @@ +#ifndef PAGE_H +#define PAGE_H +#include + +class Page +{ +public: + Page(); + std::string name; + bool listed; + unsigned int current_revision; + +}; + +#endif // PAGE_H diff --git a/parser.cpp b/parser.cpp new file mode 100644 index 0000000..4027591 --- /dev/null +++ b/parser.cpp @@ -0,0 +1,143 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include +#include +#include +#include "parser.h" +#include "utils.h" +#include "htmllink.h" + +std::vector Parser::extractHeadlines(std::string content) const +{ + std::vector result; + std::string reg = R"(\[h(1|2|3)\](.*?)\[/h\1\])"; + std::regex headerfinder(reg); + auto begin = std::sregex_iterator(content.begin(), content.end(), headerfinder); + auto end = std::sregex_iterator(); + + for(auto it = begin; it != end; it++) + { + auto smatch = *it; + Headline h; + h.level = utils::toUInt(smatch.str(1)); + h.title = smatch.str(2); + result.push_back(h); + } + return result; +} + +std::vector Parser::extractCategories(std::string content) const +{ + std::vector result; + std::string reg = R"(\[category\](.*?)\[/category\])"; + std::regex headerfinder(reg); + auto begin = std::sregex_iterator(content.begin(), content.end(), headerfinder); + auto end = std::sregex_iterator(); + + for(auto it = begin; it != end; it++) + { + auto smatch = *it; + result.emplace_back(smatch.str(1)); + + } + return result; +} +std::string Parser::processLink(const PageDao &pageDao, UrlProvider &urlProvider, std::smatch &match) const +{ + std::string linktag = match.str(1); + std::string inside = match.str(2); + + std::vector splitted = utils::splitByChar(inside, '|'); + HtmlLink htmllink; + if(splitted.size() == 2) + { + htmllink.innervalue = splitted[1]; + htmllink.href= splitted[0]; + } + else + { + htmllink.innervalue = inside; + htmllink.href = inside; + } + + if(linktag == "wikilink") { + if(pageDao.exists(htmllink.href)) + { + htmllink.cssclass = "exists"; + } + else + { + htmllink.cssclass = "notexists"; + } + + htmllink.href = urlProvider.page(htmllink.href); + + } + + return htmllink.render(); + +} +std::string Parser::parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const +{ + std::string result; + std::regex tagfinder(R"(\[(.*?)\](.*?)\[/\1\])"); + result = utils::regex_callback_replacer(tagfinder, content, [&](std::smatch &match) + { + std::string tag = match.str(1); + std::string content = match.str(2); + std::string justreplace[] = { "b", "i", "u"}; + if(std::find(std::begin(justreplace), std::end(justreplace), tag) != std::end(justreplace)) + { + return "<" + tag + ">" + content + ""; + } + if(tag == "link" || tag == "wikilink") + { + return this->processLink(pagedao, provider, match); //TODO: recreate this so we don't check inside the function stuff again + } + if(tag[0] == 'h') + { + return "<" + tag + " id='" + content + "'>" + content + ""; + } + return std::string(""); + + + + + + }); + result = utils::strreplace(result, "\r\n", "
    "); + return result; +} + +/* +std::string Parser::parse(std::string content) +{ + std::string result; + std::regex linkfinder("\\[((?:wiki)?link)\\](.*?)\\[/(?:wiki)?link\\]"); + result = utils::regex_callback_replacer(linkfinder, content, [&](std::smatch &match) { return this->processLink(match); }); + std::regex tagfinderregex("\\[(/?)(b|i|u|h1|h2|h3|table|tr|td|ul|ol|li|code|blockquote)\\]"); + result = std::regex_replace(result, tagfinderregex, "<$1$2>"); + result = utils::strreplace(result, "\r\n", "
    "); + return result; +}*/ diff --git a/parser.h b/parser.h new file mode 100644 index 0000000..39204a4 --- /dev/null +++ b/parser.h @@ -0,0 +1,19 @@ +#ifndef PARSER_H +#define PARSER_H +#include +#include "iparser.h" + + +class Parser : public IParser +{ +private: + std::string processLink(const PageDao &pageDao, UrlProvider &urlProvider, std::smatch &match) const; +public: + std::vector extractHeadlines(std::string content) const override ; + std::vector extractCategories(std::string content) const override; + std::string parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const override; + using IParser::IParser; + ~Parser() { }; +}; + +#endif // PARSER_H diff --git a/permissions.cpp b/permissions.cpp new file mode 100644 index 0000000..dc5139b --- /dev/null +++ b/permissions.cpp @@ -0,0 +1,38 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "permissions.h" + +Permissions::Permissions(int permissions) +{ + this->permissions = permissions; +} + +Permissions::Permissions(const std::string &str) +{ + for(auto permission : permmap) + { + if(str.find(permission.first) != std::string::npos) + { + this->permissions |= permission.second; + } + + } +} diff --git a/permissions.h b/permissions.h new file mode 100644 index 0000000..1465b78 --- /dev/null +++ b/permissions.h @@ -0,0 +1,77 @@ +#ifndef PERMISSIONS_H +#define PERMISSIONS_H + +#define PERM_CAN_READ 1 << 0 +#define PERM_CAN_EDIT 1 << 1 +#define PERM_CAN_PAGE_HISTORY 1 << 2 +#define PERM_CAN_GLOBAL_HISTORY 1 << 3 +#define PERM_CAN_DELETE 1 << 4 +#define PERM_CAN_SEE_PAGE_LIST 1 << 5 +#define PERM_CAN_CREATE 1 << 6 +#define PERM_CAN_SEE_CATEGORY_LIST 1 << 7 +#define PERM_CAN_SEE_LINKS_HERE 1 << 8 +#define PERM_CAN_SEARCH 1 << 9 + +#include +#include +class Permissions +{ +private: + int permissions; + const std::map permmap = + { + { "can_read", PERM_CAN_READ }, + { "can_edit", PERM_CAN_EDIT}, + { "can_page_history", PERM_CAN_PAGE_HISTORY}, + { "can_global_history", PERM_CAN_GLOBAL_HISTORY}, + { "can_delete", PERM_CAN_DELETE}, + { "can_see_page_list", PERM_CAN_SEE_PAGE_LIST}, + { "can_create", PERM_CAN_CREATE}, + { "can_see_category_list", PERM_CAN_SEE_CATEGORY_LIST}, + { "can_see_links_here", PERM_CAN_SEE_LINKS_HERE}, + { "can_search", PERM_CAN_SEARCH} + }; +public: + Permissions() { this->permissions = 0; } + Permissions(int permissions); + Permissions(const std::string &str); + Permissions (Permissions &&o) + { + this->permissions = o.permissions; + } + + Permissions (const Permissions &o) + { + this->permissions = o.permissions; + } + Permissions &operator=(const Permissions &o) + { + this->permissions = o.permissions; + return *this; + } + + Permissions &operator=(Permissions &&o) + { + this->permissions = o.permissions; + return *this; + } + + + int getPermissions() const { return this->permissions; } + + bool canRead() const { return this->permissions & PERM_CAN_READ; } + bool canEdit() const { return this->permissions & PERM_CAN_EDIT; } + bool canSeePageHistory() const { return this->permissions & PERM_CAN_PAGE_HISTORY; } + bool canSeeGlobalHistory() const { return this->permissions & PERM_CAN_GLOBAL_HISTORY; } + bool canCreate() const { return this->permissions & PERM_CAN_CREATE; } + bool canSeeCategoryList() const { return this->permissions & PERM_CAN_SEE_CATEGORY_LIST; } + bool canSeeLinksHere() const { return this->permissions & PERM_CAN_SEE_LINKS_HERE; } + bool canSearch() const { return this->permissions & PERM_CAN_SEARCH; } + bool canDelete() const { return this->permissions & PERM_CAN_DELETE; } + bool canSeePageList() const { return this->permissions & PERM_CAN_SEE_PAGE_LIST; } + + + +}; + +#endif // PERMISSIONS_H diff --git a/qswiki.cpp b/qswiki.cpp new file mode 100644 index 0000000..d67b397 --- /dev/null +++ b/qswiki.cpp @@ -0,0 +1,112 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include +#include +#include +#include "gateway/gatewayinterface.h" +#include "gateway/gatewayfactory.h" +#include "handlers/handlerfactory.h" +#include "database/databasefactory.h" +#include "config.h" +#include "template.h" +#include "session.h" +#include "logger.h" +#include "urlprovider.h" +#include "requestworker.h" +#include "cache/fscache.h" +void sigterm_handler(int arg) +{ + //TODO: proper shutdown. + exit(EXIT_SUCCESS); +} + + +void setup_signal_handlers() +{ + struct sigaction sigtermaction; + sigtermaction.sa_handler = &sigterm_handler; + + int ret = sigaction(SIGTERM, &sigtermaction, NULL); + if(ret == -1) + { + perror("sigaction"); + exit(EXIT_FAILURE); + } +} + +std::unique_ptr createCache(const Config &config) +{ + + std::string path = config.getConfig("cache_fs_dir"); + + return std::make_unique(config.getConfig("cache_fs_dir")); +} +int main(int argc, char **argv) +{ + if(geteuid() == 0) + { + std::cerr << "Do not run this as root!" << std::endl; + return 1; + } + if(argc < 2) + { + std::cerr << "no path to config file provided" << std::endl; + return 1; + } + + try + { + ConfigReader configreader(argv[1]); + Config config = configreader.readConfig(); + + setup_signal_handlers(); + + std::fstream logstream; + logstream.open(config.logfile, std::fstream::out | std::fstream::app); + Logger::setStream(&logstream); + + User anon; + anon.login = config.anon_username; + anon.permissions = config.anon_permissions; + User::setAnon(anon); + + auto database = createDatabase(config); + Template siteTemplate { config }; + UrlProvider urlprovider { config }; + + auto cache = createCache(config); + cache->clear(); + RequestWorker requestWorker (*database, siteTemplate, urlprovider, *cache ); + + auto interface = createGateway(config); + interface->work(requestWorker); + } + catch(const std::exception &e) + { + Logger::error() << e.what(); + std::cerr << e.what() << std::endl; + } + return 0; + +} diff --git a/random.cpp b/random.cpp new file mode 100644 index 0000000..27a6b45 --- /dev/null +++ b/random.cpp @@ -0,0 +1,54 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include "random.h" +#include "logger.h" + + +Random::Random() +{ + +} + +std::string Random::getRandomHexString(unsigned int bytes) +{ + std::stringstream stream; + + auto buffer = std::make_unique(bytes); + int r = getrandom(buffer.get(), bytes, GRND_NONBLOCK); + if(r != -1 && (size_t) r == bytes) + { + for(size_t i = 0; i < bytes; i++) + { + unsigned char c = (unsigned char) buffer[i]; + stream << std::hex << (unsigned int) c; + } + return stream.str(); + } + else + { + + Logger::error() << "Random generator failed to get bytes: " + std::to_string(r); + throw std::runtime_error("Random generator failed"); + } +} diff --git a/random.h b/random.h new file mode 100644 index 0000000..d3eeaaa --- /dev/null +++ b/random.h @@ -0,0 +1,53 @@ +#ifndef RANDOM_H +#define RANDOM_H +#include +#include +#ifdef __linux__ +#include +#endif +#ifndef SYS_getrandom +#include +#endif +#include +#include +#include +#include + +//dirty hacks +#ifdef SYS_getrandom +inline int getrandom(void *buf, size_t buflen, unsigned int flags) +{ + return syscall(SYS_getrandom, buf, buflen, flags); +} +#else + +#if __linux__ +//ancient linux systems +#define GRND_NONBLOCK 0 +inline int getrandom(void *buf, size_t buflen, unsigned int flags) +{ + int result = RAND_bytes(buf, buflen); + if(result == 1) + { + return (int) buflen; + } + return -1; +} +#endif +#if __OpenBSD__ +inline int getrandom(void *buf, size_t buflen, unsigned int flags) +{ + arc4random_buf(buf, buflen); + return 0; +} +#endif +#endif +/* TODO: if the >=C++11 prngr are good enough, use them */ +class Random +{ +public: + Random(); + std::string getRandomHexString(unsigned int bytes ); +}; + +#endif // RANDOM_H diff --git a/request.cpp b/request.cpp new file mode 100644 index 0000000..25f0cbe --- /dev/null +++ b/request.cpp @@ -0,0 +1,129 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "request.h" +#include "utils.h" +Request::Request(std::string url) +{ + this->url = url; + size_t question = url.find_first_of('?'); + if(question != std::string::npos) + { + initGetMap(url.substr(question+1)); + } +} + +std::pair Request::createPairFromVar(std::string var) +{ + size_t equal = var.find_first_of('='); + if(equal == std::string::npos) + { + return std::make_pair(std::move(var), ""); + } + else + { + std::string key = var.substr(0, equal); + std::string val = utils::html_xss(var.substr(equal+1)); + return std::make_pair(std::move(key), std::move(val)); + } +} + +void Request::initMultiMap(std::multimap &map, const std::string &url) +{ + auto splitted = utils::splitByChar(url, '&'); + for(const std::string &part : splitted) + { + auto pair = createPairFromVar(part); + map.insert(pair); + } +} +void Request::initGetMap(const std::string &url) +{ + size_t question = url.find_first_of('?'); + if(question != std::string::npos) + { + initMultiMap(getVars, url.substr(question+1)); + } + else + { + initMultiMap(getVars, url); + } +} + +void Request::initPostMap(const std::string &url) +{ + initMultiMap(postVars, url); +} + +void Request::initCookies(const std::string &cookiestr) +{ + //TODO: find out what it really should be, ";" or "; "? + auto cookiesplitted = utils::splitByRegex(cookiestr, ";+\\s?"); + for(const std::string &part : cookiesplitted) + { + auto pair = createPairFromVar(part); + cookies.push_back(Cookie(pair.first, pair.second)); + } + +} + +std::string Request::get(const std::string &key) const +{ + return utils::getKeyOrEmpty(this->getVars, key); + +} + +std::string Request::post(const std::string &key) const +{ + return utils::getKeyOrEmpty(this->postVars, key); +} + +std::string Request::param(const std::string &key) const +{ + std::string getvar = get(key); + if(getvar.empty()) { + return post(key); +} + return getvar; +} +std::string Request::cookie(const std::string &key) const +{ + for(const Cookie &c : cookies) + { + if(c.key == key) + { + return c.value; + } + } + + return ""; + +} + +std::vector Request::allGet(const std::string &key) +{ + return utils::getAll(this->getVars, key); +} + +std::vector Request::allPost(const std::string &key) +{ + return utils::getAll(this->postVars, key); +} + diff --git a/request.h b/request.h new file mode 100644 index 0000000..0198fc2 --- /dev/null +++ b/request.h @@ -0,0 +1,114 @@ +#include + +#include + +#ifndef REQUEST_H +#define REQUEST_H +#include +#include +#include +#include +#include +#include "cookie.h" +class Request +{ +private: + std::multimap getVars; + std::multimap postVars; + + std::string url; + std::string ip; + std::string useragent; + std::vector cookies; + std::string request_method; + + void initMultiMap(std::multimap &map, const std::string &url); + std::pair createPairFromVar(std::string var); + +public: + Request() { } + Request(std::string url); + std::string get(const std::string &key) const; + std::string post(const std::string &key) const; + std::string cookie(const std::string &key) const; + std::string param(const std::string &key) const; + std::vector allGet(const std::string &key); + std::vector allPost(const std::string &key); + + + const std::vector &getCookies() const + { + return this->cookies; + } + + void setCookies(std::vector cookies) + { + this->cookies = std::move(cookies); + } + + void setGetVars(std::multimap getVars) + { + this->getVars = std::move(getVars); + } + + void setPostVars(std::multimap postVars) + { + this->postVars = std::move(postVars); + } + + void setIp(const std::string &ip) + { + this->ip = ip; + } + + void setUseragent(const std::string &agent) + { + this->useragent = agent; + } + + void setUrl(const std::string &url) + { + this->url = url; + } + + + std::string getUrl() const + { + return url; + } + + std::string getIp() const + { + return ip; + } + + std::string getUseragent() const + { + return useragent; + } + + std::string getRequestMethod() const + { + return request_method; + } + + inline void setRequestMethod(std::string request_method) + { + this->request_method = request_method; + } + + void initGetMap(const std::string &url); + void initPostMap(const std::string &url); + void initCookies(const std::string &cookiestr); + + friend std::ostream& operator<< (std::ostream &os, const Request &req); + + +}; + +inline std::ostream& operator<< (std::ostream &os, const Request &req) +{ + os << req.request_method << " " << req.url << " " << req.ip; + return os; +} +#endif // REQUEST_H diff --git a/requestworker.cpp b/requestworker.cpp new file mode 100644 index 0000000..549b27b --- /dev/null +++ b/requestworker.cpp @@ -0,0 +1,93 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "requestworker.h" +#include "handlers/handlerfactory.h" + + +Session RequestWorker::retrieveSession(std::string token) const +{ + if(token.empty()) + { + return Session::createAnon(); + } + + auto sess = this->sessionDao->find(token); + if(sess) + { + sess->creation_time = time(0); + return *sess; + } + else + { + return Session::createAnon(); + } +} + + +Response RequestWorker::processRequest(const Request &r) +{ + std::string sessiontoken = r.cookie("sessiontoken"); + Session session; + if(sessiontoken != "") + { + session = retrieveSession(sessiontoken); + } + else + { + session = Session::createAnon(); + } + + if(r.getRequestMethod() == "POST") + { + //TODO: also protect non-logged in users (with a mechanism not involving cookies) + if(session.loggedIn && session.csrf_token != r.post("csrf_token")) + { + //TODO: this is code duplication + TemplatePage &error = this->templ->getPage("error"); + error.setVar("errortitle", "Invalid csrf token"); + error.setVar("errormessage", "Invalid csrf token"); + return { 403, error.render()}; + } + } + + auto handler = createHandler(r.param("action"), *this->templ, *this->db, session, *this->urlProvider, *this->cache); + + try + { + Response response = handler->handle(r); + if(session.loggedIn) + { + Cookie sessionCookie {"sessiontoken", session.token}; + response.addCookie(sessionCookie); + this->sessionDao->save(session); + } + return response; + + } + catch(std::exception &e) + { + Logger::error() << "Exception catched by requestworker: " << e.what(); + Response response; + response.setBody("General unknown error"); + response.setContentType("text/plain"); + return response; + } +} diff --git a/requestworker.h b/requestworker.h new file mode 100644 index 0000000..5a67179 --- /dev/null +++ b/requestworker.h @@ -0,0 +1,35 @@ +#ifndef REQUESTWORKER_H +#define REQUESTWORKER_H + +#include "request.h" +#include "response.h" +#include "session.h" +#include "template.h" +#include "database/database.h" +#include "urlprovider.h" +#include "database/sessiondao.h" +#include "cache/fscache.h" +class RequestWorker +{ + Database *db; + Template *templ; + UrlProvider *urlProvider; + ICache *cache; + std::unique_ptr sessionDao; +private: + Session retrieveSession(std::string token) const; +public: + RequestWorker(Database &db, Template &templ, UrlProvider &provider, ICache &cache) + { + this->db = &db; + this->templ = &templ; + this->urlProvider = &provider; + this->sessionDao = db.createSessionDao(); + this->cache = &cache; + + } + + Response processRequest(const Request &r); +}; + +#endif // REQUESTWORKER_H diff --git a/response.cpp b/response.cpp new file mode 100644 index 0000000..e1c7b9d --- /dev/null +++ b/response.cpp @@ -0,0 +1,47 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "response.h" + +#include + +Response::Response() +{ + +} + +Response::Response(int http_status_code, std::string html) +{ + this->status_code = http_status_code; + this->html = std::move(html); +} + +void Response::addHeader(const std::string &key, const std::string &value) +{ + this->responseHeaders.insert(std::make_pair(key, value)); +} + +Response Response::redirectTemporarily(const std::string &url) +{ + Response result; + result.addHeader("Location", url); + result.setStatus(302); + return result; +} diff --git a/response.h b/response.h new file mode 100644 index 0000000..776dbc4 --- /dev/null +++ b/response.h @@ -0,0 +1,60 @@ +#ifndef RESPONSE_H +#define RESPONSE_H + +#include +#include +#include +#include "cookie.h" +class Response +{ +private: + int status_code = 200; + std::string html; + std::string content_type = "text/html"; + std::map responseHeaders; + std::vector cookies; +public: + Response(); + Response(int http_status_code, std::string html); + + int getStatus() const { return this->status_code; } + std::string getBody() const { return this->html; } + + void addHeader(const std::string &key, const std::string &value); + static Response redirectTemporarily(const std::string &url); + + void setStatus(int status) { this->status_code = status; } + void setBody(std::string body) { this->html = body; } + + const std::map &getResponseHeaders() const + { + return this->responseHeaders; + } + + //TODO: maybe "getEffectiveResponseHeaders?" that would include cookies etc. + + const std::vector &getCookies() const + { + return this->cookies; + } + + void addCookie(Cookie cookie) + { + this->cookies.push_back(cookie); + } + + void setContentType(const std::string &type) + { + this->content_type = type; + } + + std::string getContentType() const + { + return this->content_type; + } + + + +}; + +#endif // RESPONSE_H diff --git a/revision.cpp b/revision.cpp new file mode 100644 index 0000000..481decf --- /dev/null +++ b/revision.cpp @@ -0,0 +1,27 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "revision.h" + +Revision::Revision() +{ + this->timestamp = time(nullptr); + this->revision = 0; +} diff --git a/revision.h b/revision.h new file mode 100644 index 0000000..629ebdd --- /dev/null +++ b/revision.h @@ -0,0 +1,18 @@ +#ifndef REVISION_H +#define REVISION_H +#include +#include +class Revision +{ +public: + unsigned int revision; + time_t timestamp; + std::string comment; + std::string page; + std::string author; + std::string content; + + Revision(); +}; + +#endif // REVISION_H diff --git a/searchresult.h b/searchresult.h new file mode 100644 index 0000000..8973254 --- /dev/null +++ b/searchresult.h @@ -0,0 +1,10 @@ +#ifndef SEARCHRESULT_H +#define SEARCHRESULT_H +#include +class SearchResult +{ +public: + std::string query; + std::string pagename; +}; +#endif // SEARCHRESULT_H diff --git a/session.cpp b/session.cpp new file mode 100644 index 0000000..a35794a --- /dev/null +++ b/session.cpp @@ -0,0 +1,48 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include "session.h" +#include "random.h" + +Session::Session(User u) +{ + Random r; + this->user = std::move(u); + this->creation_time = time(0); + this->csrf_token = r.getRandomHexString(16); + this->token = r.getRandomHexString(16); + this->loggedIn = true; + +} +Session Session::createAnon() +{ + Random r; + Session result; + result.creation_time = time(0); + // result.csrf_token = r.getRandomHexString(16); + // result.token = r.getRandomHexString(16); + result.user = User::Anonymous(); + result.loggedIn = false; + return result; +} + + diff --git a/session.h b/session.h new file mode 100644 index 0000000..1ed7c75 --- /dev/null +++ b/session.h @@ -0,0 +1,19 @@ +#ifndef SESSION_H +#define SESSION_H +#include +#include "user.h" +class Session +{ + public: + Session() { } + Session(User u); + bool loggedIn; + User user; + std::string token; + std::string csrf_token; + time_t creation_time; + + static Session createAnon(); +}; + +#endif diff --git a/setup/config b/setup/config new file mode 100644 index 0000000..d8c95e1 --- /dev/null +++ b/setup/config @@ -0,0 +1,35 @@ +wikiname NameOfWiki +wikipath / +templatepath /path/to/template +logfile /path/to/template/logfile +csspath /static/style.css +query_limit 200 +session_max_lifetime 600 +connectionstring /path/to/sqlite.db +anon_username anonymouse +max_pagename_length 256 +fs_cache_dir /var/tmp/qswiki/ +linkindex ?action=show&page=index +linkrecent ?action=recent +linkrecentsort wiki?action=recent&limit={limit}&offset={offset}&sort={sort} +linkallpages ?action=allpages +linkallcats ?action=allcategories +linkshere wiki?action=linkshere&page={page} +linkpage wiki?action=show&page={page} +linkrevision wiki?action=show&page={page}&revision={revisionid} +linkhistory ?action=recent&page={page} +linkhistorysort wiki?action=recent&page={page}&limit={limit}&offset={offset}&sort={sort} +linkedit wiki?action=edit&page={page} +linksettings wiki?action=settings&page={page} +linkdelete wiki?action=delete&page={page} +linklogin wiki?action=login +linklogout wiki?action=logout&token=%s +linkcategory wiki?action=showcat&category={category} +loginurl wiki?action=login&submit=1&page={page} +actionurl wiki?action=%s&page={page} +settingsurl wiki?action=settings&page={page} +deletionurl wiki?action=delete&page={page} +refreshsessionurl wiki?action=refreshsession +adminregisterurl wiki?action=adminregister +userchangepwurl wiki?action=userchangepw +anon_permissions can_read,can_global_history,can_page_history,can_see_page_list,can_see_category_list,can_see_links_here,can_search diff --git a/setup/sqlite.sql b/setup/sqlite.sql new file mode 100644 index 0000000..8d3ecc5 --- /dev/null +++ b/setup/sqlite.sql @@ -0,0 +1,48 @@ +CREATE TABLE page(id INTEGER PRIMARY KEY, name varchar(256), lastrevision integer, visible integer DEFAULT 1); +CREATE TABLE user(id INTEGER PRIMARY KEY,username varchar(64), +password blob, salt blob, permissions integer); +CREATE TABLE session(id INTEGER PRIMARY KEY, csrf_token varchar(32), +creationtime date, userid integer , token varchar(32)); +CREATE TABLE permissions(id INTEGER PRIMARY KEY, permissions integer, +userid integer REFERENCES user(id), page integer REFERENCES page(id ) ); +CREATE TABLE revision +( + +id INTEGER PRIMARY KEY, +author integer REFERENCES user(id), +comment text, +content text, +creationtime date, +page integer REFERENCES page(id ), +revisionid integer +); +CREATE TABLE loginattempt +( +id INTEGER PRIMARY KEY, +ip varchar(16), +count integer +); +CREATE TABLE category(id INTEGER PRIMARY KEY, name varchar(255)); +CREATE TABLE categorymember(id INTEGER PRIMARY KEY, category REFERENCES category(id), page REFERENCES page (id)); +CREATE INDEX revisionid ON revision (revisionid DESC); +CREATE INDEX pagename ON page (name) +; +CREATE INDEX token ON session (token) +; +CREATE TRIGGER search_ai AFTER INSERT ON revision BEGIN + DELETE FROM search WHERE page = new.page; + INSERT INTO search(rowid, content, page) VALUES (new.id, new.content, new.page); +END; +CREATE TRIGGER search_au AFTER UPDATE ON revision BEGIN + DELETE FROM search WHERE page = old.page; + INSERT INTO search(rowid, content, page) VALUES (new.id, new.content, new.page); +END; +CREATE VIRTUAL TABLE search USING fts5(content, page UNINDEXED, content=revision,content_rowid=id) +/* search(content,page) */; +CREATE TABLE IF NOT EXISTS 'search_data'(id INTEGER PRIMARY KEY, block BLOB); +CREATE TABLE IF NOT EXISTS 'search_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID; +CREATE TABLE IF NOT EXISTS 'search_docsize'(id INTEGER PRIMARY KEY, sz BLOB); +CREATE TABLE IF NOT EXISTS 'search_config'(k PRIMARY KEY, v) WITHOUT ROWID; +CREATE TRIGGER search_ad AFTER DELETE ON revision BEGIN + INSERT INTO search(search, rowid, content, page) VALUES('delete', old.id, old.content, old.page); +END; diff --git a/template.cpp b/template.cpp new file mode 100644 index 0000000..6b926c7 --- /dev/null +++ b/template.cpp @@ -0,0 +1,180 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "template.h" +#include "varreplacer.h" +#include "urlprovider.h" +#include "htmllink.h" +#include "logger.h" +Template::Template(const Config &config) +{ + this->config = &config; +} + +std::string Template::getPartPath(std::string_view partname) +{ + //TODO: utils::concatPath? C++17 paths? + return this->config->templatepath + "/" + std::string(partname); +} +std::string Template::loadPartContent(std::string_view partname) +{ + std::string partpath = getPartPath(partname); + return utils::readCompleteFile(partpath); +} + +std::string Template::loadResolvedPart(std::string_view partname) +{ + return resolveIncludes(loadPartContent(partname)); +} + + +std::string Template::resolveIncludes(std::string_view content) +{ + Varreplacer replacer(this->config->templateprefix); + replacer.addResolver("include", [&](std::string_view key) { return loadResolvedPart(key); }); + return replacer.parse(content); +} + +TemplatePage Template::createPage(std::string name) +{ + std::string content = loadResolvedPart(name); + Varreplacer replacer(this->config->templateprefix); + replacer.addResolver("config", [&](std::string_view key) { return this->config->getConfig(std::string(key)); }); + //TODO: Varreplacer is not recursive, but since includes might add new vars, it may not be this bad anyway. + // + return TemplatePage(replacer.parse(content)); +} + +TemplatePage &Template::getPage(const std::string &pagename) +{ + if(utils::hasKey(pagesMap, pagename)) + { + return pagesMap[pagename]; + } + pagesMap.insert(std::make_pair(pagename,createPage(pagename))); + return pagesMap[pagename] ; + + +} + +//TODO: this restricts template a bit +std::string Template::renderSearch(const std::vector &results, std::function callback) const +{ + HtmlLink link; + + std::string result; + char lastchar = 0; + for(const std::string &str: results) + { + int upper = toupper(str[0]); //TODO: this is not unicode safe. + if(lastchar != upper) + { + lastchar = upper; + + result += std::string ( "
    " ) + lastchar + std::string ( "
    " ); + } + link.href = callback(str); + link.innervalue = str; + result += link.render() + "
    "; + } + return result; + +} +std::string Template::renderSearch(const std::vector &results) const +{ + UrlProvider urlprovider(*this->config); + return renderSearch(results, [&urlprovider](std::string s) { return urlprovider.page(s);}); + +} +std::string Template::renderSearch(const std::vector &results) const +{ + UrlProvider urlprovider(*this->config); + HtmlLink link; + char lastchar = 0; + std::string result; + for(const SearchResult &sr : results) + { + int upper = toupper(sr.pagename[0]); //TODO: this is not unicode safe. + if(lastchar != upper) + { + lastchar = upper; + + result += std::string ( "
    " ) + lastchar + std::string ( "
    " ); + } + link.href = urlprovider.page(sr.pagename); + link.innervalue = sr.pagename; + result += link.render() + "
    "; + } + return result; + + + +} + +std::string Template::renderRevisionList(const std::vector &revisions, bool withpage) const +{ + std::stringstream stream; + UrlProvider urlprovider(*this->config); + + auto genwithoutpage = [&] { + for(const Revision &revision : revisions) + { + + Logger::debug() << "processing: " << revision.revision; + stream << "" << revision.revision << "" + <<"" << revision.author << "" + << "" << revision.comment << "" + << "" << utils::toISODate(revision.timestamp) << ""; + + + } + + }; + + auto genwithpage = [&] { + for(const Revision &revision : revisions) + { + + stream << "" << revision.page << "" + << "" << revision.revision << "" + <<"" << revision.author << "" + << "" << revision.comment << "" + << "" << utils::toISODate(revision.timestamp) << ""; + + + } + }; + + if(withpage) + { + + genwithpage(); + } + else + { + genwithoutpage(); + } + + return stream.str(); +} diff --git a/template.h b/template.h new file mode 100644 index 0000000..27d6d0b --- /dev/null +++ b/template.h @@ -0,0 +1,39 @@ +#ifndef TEMPLATE_H +#define TEMPLATE_H +#include +#include +#include "config.h" +#include "templatepage.h" +#include "utils.h" +#include "response.h" +#include "searchresult.h" +#include "revision.h" +class Template +{ + private: + const Config *config; + std::map pagesMap; + std::string resolveIncludes(std::string_view content); + + std::string getPartPath(std::string_view partname); + std::string loadResolvedPart(std::string_view partname); + std::string loadPartContent(std::string_view partname); + TemplatePage createPage(std::string name); + + +public: + Template(const Config &config); + /* TODO: returning this as a reference is by no means a risk free business, + because between requests, different vars can be set conditionally, + thus creating a mess + */ + TemplatePage &getPage(const std::string &pagename) ; + + std::string renderSearch(const std::vector &results, std::function callback) const; + std::string renderSearch(const std::vector &results) const; + std::string renderSearch(const std::vector &results) const; + std::string renderRevisionList(const std::vector &revisions, bool withpage=false) const; + +}; + +#endif // TEMPLATE_H diff --git a/template/default/_headerlink b/template/default/_headerlink new file mode 100644 index 0000000..01f7c10 --- /dev/null +++ b/template/default/_headerlink @@ -0,0 +1 @@ +%s diff --git a/template/default/allcategories b/template/default/allcategories new file mode 100644 index 0000000..5ae74a9 --- /dev/null +++ b/template/default/allcategories @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All categories

    +{wikiqs:var:categorylist} +
    +{wikiqs:include:general_footer} diff --git a/template/default/allpages b/template/default/allpages new file mode 100644 index 0000000..ccedf3b --- /dev/null +++ b/template/default/allpages @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All pages

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/default/error b/template/default/error new file mode 100644 index 0000000..6219d36 --- /dev/null +++ b/template/default/error @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    {wikiqs:var:errortitle}

    +{wikiqs:var:errormessage} +
    +{wikiqs:include:general_footer} diff --git a/template/default/general_footer b/template/default/general_footer new file mode 100644 index 0000000..6f18737 --- /dev/null +++ b/template/default/general_footer @@ -0,0 +1,16 @@ +
    +
    +{wikiqs:var:loginstatus} - Powered by wikiQS, built on {wikiqs:var:buildinfo} +
    + + + diff --git a/template/default/general_header b/template/default/general_header new file mode 100644 index 0000000..86d86b1 --- /dev/null +++ b/template/default/general_header @@ -0,0 +1,19 @@ + + +{wikiqs:var:title} + +
    +
    +
    +

    {wikiqs:var:title}

    +
    + +
    +
    +
    + + + + diff --git a/template/default/login b/template/default/login new file mode 100644 index 0000000..f2d8258 --- /dev/null +++ b/template/default/login @@ -0,0 +1,11 @@ +{wikiqs:include:general_header} +
    +

    Login

    +
    +Username:
    +Password:
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/default/login_already b/template/default/login_already new file mode 100644 index 0000000..8ab7e01 --- /dev/null +++ b/template/default/login_already @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +
    +

    Login

    +You are already logged in as {wikiqs:var:username}
    +Logout +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_creation b/template/default/page_creation new file mode 100644 index 0000000..31cd7b3 --- /dev/null +++ b/template/default/page_creation @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_creation_preview b/template/default/page_creation_preview new file mode 100644 index 0000000..bfa7212 --- /dev/null +++ b/template/default/page_creation_preview @@ -0,0 +1,18 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +This is a preview of your changes:
    +{wikiqs:var:preview_content} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_deletion b/template/default/page_deletion new file mode 100644 index 0000000..2455cdf --- /dev/null +++ b/template/default/page_deletion @@ -0,0 +1,11 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +

    Page deletion

    +Do you really want to delete page {wikiqs:var:page} +
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_footer b/template/default/page_footer new file mode 100644 index 0000000..c2e2bf2 --- /dev/null +++ b/template/default/page_footer @@ -0,0 +1,7 @@ +
    Page categories: +{wikiqs:var:categorieslist} +
    +
    +What links here? - Edited {wikiqs:var:editedon} by {wikiqs:var:editedby} +
    + diff --git a/template/default/page_header b/template/default/page_header new file mode 100644 index 0000000..fb28410 --- /dev/null +++ b/template/default/page_header @@ -0,0 +1,3 @@ +
    +{wikiqs:var:headerlinks} +
    diff --git a/template/default/page_history b/template/default/page_history new file mode 100644 index 0000000..355f97a --- /dev/null +++ b/template/default/page_history @@ -0,0 +1,9 @@ +{wikiqs:include:general_header} +
    + +{wikiqs:var:revisionlist} +
    RevisionAuthorCommentDate
    + +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_settings b/template/default/page_settings new file mode 100644 index 0000000..ef03d0e --- /dev/null +++ b/template/default/page_settings @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +

    Page settings: {wikiqs:var:page}

    +
    +

    Categories:

    +
    +

    Rename:

    +
    +Redirect old page + + + +
    +{wikiqs:include:general_footer} diff --git a/template/default/page_view b/template/default/page_view new file mode 100644 index 0000000..53bd1ad --- /dev/null +++ b/template/default/page_view @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} +{wikiqs:include:general_footer} diff --git a/template/default/page_view_revision b/template/default/page_view_revision new file mode 100644 index 0000000..5b221b9 --- /dev/null +++ b/template/default/page_view_revision @@ -0,0 +1,8 @@ +{wikiqs:include:general_header} +{wikiqs:include:page_header} +
    +Showing revision: {wikiqs:var:revision}
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} +{wikiqs:include:general_footer} diff --git a/template/default/recentchanges b/template/default/recentchanges new file mode 100644 index 0000000..44f0df7 --- /dev/null +++ b/template/default/recentchanges @@ -0,0 +1,9 @@ +{wikiqs:include:general_header} +
    + + +{wikiqs:var:revisionlist} +
    PageRevisionAuthorCommentDate
    +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/default/search b/template/default/search new file mode 100644 index 0000000..a407937 --- /dev/null +++ b/template/default/search @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Search for: {wikiqs:var:searchterm}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/default/show_category b/template/default/show_category new file mode 100644 index 0000000..f5adba4 --- /dev/null +++ b/template/default/show_category @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Category: {wikiqs:var:categoryname}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/default/show_whatlinkshere b/template/default/show_whatlinkshere new file mode 100644 index 0000000..77ff4b5 --- /dev/null +++ b/template/default/show_whatlinkshere @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Links to: {wikiqs:var:pagename}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/_headerlink b/template/greensimple/_headerlink new file mode 100644 index 0000000..2eb24d5 --- /dev/null +++ b/template/greensimple/_headerlink @@ -0,0 +1 @@ +
  • %s
  • diff --git a/template/greensimple/admin_register b/template/greensimple/admin_register new file mode 100644 index 0000000..4b010c2 --- /dev/null +++ b/template/greensimple/admin_register @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +
    +

    Login

    +Register a new user + +Username:
    +Password:
    +Repeat password:
    + + + + + +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/allcategories b/template/greensimple/allcategories new file mode 100644 index 0000000..5ae74a9 --- /dev/null +++ b/template/greensimple/allcategories @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All categories

    +{wikiqs:var:categorylist} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/allpages b/template/greensimple/allpages new file mode 100644 index 0000000..ccedf3b --- /dev/null +++ b/template/greensimple/allpages @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All pages

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/error b/template/greensimple/error new file mode 100644 index 0000000..6219d36 --- /dev/null +++ b/template/greensimple/error @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    {wikiqs:var:errortitle}

    +{wikiqs:var:errormessage} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/general_footer b/template/greensimple/general_footer new file mode 100644 index 0000000..bca43e5 --- /dev/null +++ b/template/greensimple/general_footer @@ -0,0 +1,18 @@ + + + + diff --git a/template/greensimple/general_header b/template/greensimple/general_header new file mode 100644 index 0000000..913f74f --- /dev/null +++ b/template/greensimple/general_header @@ -0,0 +1,20 @@ + + + + +{wikiqs:var:title} + + diff --git a/template/greensimple/login b/template/greensimple/login new file mode 100644 index 0000000..ce14aab --- /dev/null +++ b/template/greensimple/login @@ -0,0 +1,11 @@ +{wikiqs:include:general_header} +
    +

    Login

    +
    +Username:
    +Password:
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/login_already b/template/greensimple/login_already new file mode 100644 index 0000000..53df5f1 --- /dev/null +++ b/template/greensimple/login_already @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +
    +

    Login

    +You are already logged in as {wikiqs:var:username}
    +Logout +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_creation b/template/greensimple/page_creation new file mode 100644 index 0000000..7f148ea --- /dev/null +++ b/template/greensimple/page_creation @@ -0,0 +1,14 @@ +{wikiqs:include:page_header} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_creation_preview b/template/greensimple/page_creation_preview new file mode 100644 index 0000000..71b1e30 --- /dev/null +++ b/template/greensimple/page_creation_preview @@ -0,0 +1,17 @@ +{wikiqs:include:page_header} +
    +This is a preview of your changes:
    +{wikiqs:var:preview_content} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_deletion b/template/greensimple/page_deletion new file mode 100644 index 0000000..0a1c501 --- /dev/null +++ b/template/greensimple/page_deletion @@ -0,0 +1,10 @@ +{wikiqs:include:page_header} +
    +

    Page deletion

    +Do you really want to delete page {wikiqs:var:page} +
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_footer b/template/greensimple/page_footer new file mode 100644 index 0000000..7058c47 --- /dev/null +++ b/template/greensimple/page_footer @@ -0,0 +1,20 @@ + + + + diff --git a/template/greensimple/page_header b/template/greensimple/page_header new file mode 100644 index 0000000..e3129a4 --- /dev/null +++ b/template/greensimple/page_header @@ -0,0 +1,24 @@ + + + + +{wikiqs:var:title} + + diff --git a/template/greensimple/page_history b/template/greensimple/page_history new file mode 100644 index 0000000..ca50987 --- /dev/null +++ b/template/greensimple/page_history @@ -0,0 +1,8 @@ +{wikiqs:include:page_header} +
    + +{wikiqs:var:revisionlist} +
    RevisionAuthorCommentDate
    +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_settings b/template/greensimple/page_settings new file mode 100644 index 0000000..6e24d6d --- /dev/null +++ b/template/greensimple/page_settings @@ -0,0 +1,16 @@ +{wikiqs:include:page_header} +
    +

    Page settings: {wikiqs:var:page}

    +
    +

    Categories:

    +
    +

    Rename:

    +
    +Redirect old page +
    +
    + Show page in lists etc. +
    + +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/page_view b/template/greensimple/page_view new file mode 100644 index 0000000..26e00aa --- /dev/null +++ b/template/greensimple/page_view @@ -0,0 +1,5 @@ +{wikiqs:include:page_header} +
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} diff --git a/template/greensimple/page_view_revision b/template/greensimple/page_view_revision new file mode 100644 index 0000000..2227ad3 --- /dev/null +++ b/template/greensimple/page_view_revision @@ -0,0 +1,6 @@ +{wikiqs:include:page_header} +
    +Showing revision: {wikiqs:var:revision}
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} diff --git a/template/greensimple/recentchanges b/template/greensimple/recentchanges new file mode 100644 index 0000000..75ade1d --- /dev/null +++ b/template/greensimple/recentchanges @@ -0,0 +1,8 @@ +{wikiqs:include:general_header} +
    + +{wikiqs:var:revisionlist} +
    PageRevisionAuthorCommentDate
    +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/search b/template/greensimple/search new file mode 100644 index 0000000..29e4fc3 --- /dev/null +++ b/template/greensimple/search @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +
    +

    Search for: {wikiqs:var:searchterm}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} + diff --git a/template/greensimple/show_category b/template/greensimple/show_category new file mode 100644 index 0000000..f0b22ca --- /dev/null +++ b/template/greensimple/show_category @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Category: {wikiqs:var:categoryname}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/show_whatlinkshere b/template/greensimple/show_whatlinkshere new file mode 100644 index 0000000..4a629ad --- /dev/null +++ b/template/greensimple/show_whatlinkshere @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Links to: {wikiqs:var:pagename}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/greensimple/style.css b/template/greensimple/style.css new file mode 100644 index 0000000..5ce00e8 --- /dev/null +++ b/template/greensimple/style.css @@ -0,0 +1,172 @@ +body +{ + padding: 0; + margin: 0; + font-family: Verdana; + background-color: white; + display: flex; + min-height: 100vh; + flex-direction: column; +} + +header +{ + margin: 0; + paddin: 0; +} + +h1 +{ + margin: 0; + padding: 0; +} +h2 +{ + margin: 0; + padding: 0; +} +nav +{ + //width: 100%; + padding: 0px; + margin: 0px; + display: flex; + background-color: #229638; + justify-content: space-between; + flex-wrap: wrap; + +} +nav ul +{ + background-color: #229638; + color: white; + margin: 0; + padding: 0; + list-style-type: none; + display: flex; + align-items: center; + flex-wrap: wrap; + + +} +nav li +{ + margin: 0; + padding: 0; + + + +} + +nav a, nav a:visited +{ + padding: 10px; + text-decoration: none; + color: white; + display: block; + font-weight: bold; + text-align: center; + line-height: 100%; + +} + +nav a:hover, nav a:focus +{ + padding: 10px; + text-decoration: none; + background-color: white; + color: #229638; + display: block; + font-weight: bold; +} + + + +a, a:visited +{ + color: #229638;; +} + +a:hover +{ + background-color: #229638; + color: white; + +} +#content +{ +padding: 15px; +font-family: monospace; +font-size: 14pt; +flex: 1; +} + +#content a, a:visited +{ + color: #229638; +} + +#content a:hover +{ + background-color: #229638; + color: white; + + +} +footer +{ + width: 100%; + display: block; + color: white; + background-color: #229638; + font-weight: bold; +} + +footer ul +{ + background-color: #229638; + margin: 0px; + padding: 0px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: center; +} +footer li +{ + margin: 0; + padding: 0; + display: inline-block; + line-height: 45px; + color: white; + font-weight: bold; + //flex: 1 1 0; + text-align: center; +} + +footer a, a:visited +{ + text-decoration: none; + + color: white; + display: inline-block; +} + +footer a:hover, ul#nav a:focus +{ + text-decoration: none; + color: #229638; + background-color: white; + display: inline-block; +} + +#cats +{ +background-color: #229638; +} + +.letter_search_result +{ + text-decoration: underline; + font-weight: bold; +} diff --git a/template/greensimple/user_changepw b/template/greensimple/user_changepw new file mode 100644 index 0000000..5f94341 --- /dev/null +++ b/template/greensimple/user_changepw @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +
    +

    Login

    +Change your current password + +Current password:
    +New Password:
    +Repeat password:
    + + + + + +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/_headerlink b/template/quitesimple/_headerlink new file mode 100644 index 0000000..9bb4ed6 --- /dev/null +++ b/template/quitesimple/_headerlink @@ -0,0 +1 @@ +
  • {wikiqs:var:value}
  • diff --git a/template/quitesimple/admin_register b/template/quitesimple/admin_register new file mode 100644 index 0000000..4b010c2 --- /dev/null +++ b/template/quitesimple/admin_register @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +
    +

    Login

    +Register a new user +
    +Username:
    +Password:
    +Repeat password:
    + + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/allcategories b/template/quitesimple/allcategories new file mode 100644 index 0000000..5ae74a9 --- /dev/null +++ b/template/quitesimple/allcategories @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All categories

    +{wikiqs:var:categorylist} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/allpages b/template/quitesimple/allpages new file mode 100644 index 0000000..ccedf3b --- /dev/null +++ b/template/quitesimple/allpages @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    All pages

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/error b/template/quitesimple/error new file mode 100644 index 0000000..86cf70e --- /dev/null +++ b/template/quitesimple/error @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    {wikiqs:var:errortitle}


    +{wikiqs:var:errormessage} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/general_footer b/template/quitesimple/general_footer new file mode 100644 index 0000000..bca43e5 --- /dev/null +++ b/template/quitesimple/general_footer @@ -0,0 +1,18 @@ + + + + diff --git a/template/quitesimple/general_header b/template/quitesimple/general_header new file mode 100644 index 0000000..b5d511b --- /dev/null +++ b/template/quitesimple/general_header @@ -0,0 +1,21 @@ + + + + + +{wikiqs:var:title} + + diff --git a/template/quitesimple/login b/template/quitesimple/login new file mode 100644 index 0000000..ce14aab --- /dev/null +++ b/template/quitesimple/login @@ -0,0 +1,11 @@ +{wikiqs:include:general_header} +
    +

    Login

    +
    +Username:
    +Password:
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/login_already b/template/quitesimple/login_already new file mode 100644 index 0000000..53df5f1 --- /dev/null +++ b/template/quitesimple/login_already @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +
    +

    Login

    +You are already logged in as {wikiqs:var:username}
    +Logout +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_creation b/template/quitesimple/page_creation new file mode 100644 index 0000000..7f148ea --- /dev/null +++ b/template/quitesimple/page_creation @@ -0,0 +1,14 @@ +{wikiqs:include:page_header} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_creation_preview b/template/quitesimple/page_creation_preview new file mode 100644 index 0000000..71b1e30 --- /dev/null +++ b/template/quitesimple/page_creation_preview @@ -0,0 +1,17 @@ +{wikiqs:include:page_header} +
    +This is a preview of your changes:
    +{wikiqs:var:preview_content} +
    +
    + + + +
    Comment:
    +
    + + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_deletion b/template/quitesimple/page_deletion new file mode 100644 index 0000000..8d72b05 --- /dev/null +++ b/template/quitesimple/page_deletion @@ -0,0 +1,10 @@ +{wikiqs:include:page_header} +
    +

    Page deletion


    +Do you really want to delete page {wikiqs:var:page}? +
    + + +
    +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_footer b/template/quitesimple/page_footer new file mode 100644 index 0000000..7058c47 --- /dev/null +++ b/template/quitesimple/page_footer @@ -0,0 +1,20 @@ + + + + diff --git a/template/quitesimple/page_header b/template/quitesimple/page_header new file mode 100644 index 0000000..667627d --- /dev/null +++ b/template/quitesimple/page_header @@ -0,0 +1,25 @@ + + + + + +{wikiqs:var:title} + + diff --git a/template/quitesimple/page_history b/template/quitesimple/page_history new file mode 100644 index 0000000..ca50987 --- /dev/null +++ b/template/quitesimple/page_history @@ -0,0 +1,8 @@ +{wikiqs:include:page_header} +
    + +{wikiqs:var:revisionlist} +
    RevisionAuthorCommentDate
    +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_settings b/template/quitesimple/page_settings new file mode 100644 index 0000000..93b4068 --- /dev/null +++ b/template/quitesimple/page_settings @@ -0,0 +1,16 @@ +{wikiqs:include:page_header} +
    +

    Page settings: {wikiqs:var:page}

    +
    +

    Categories:


    +
    +

    Rename:

    +
    +Redirect old page +
    +
    + Show page in lists etc. +
    + +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/page_view b/template/quitesimple/page_view new file mode 100644 index 0000000..26898fe --- /dev/null +++ b/template/quitesimple/page_view @@ -0,0 +1,11 @@ +{wikiqs:include:page_header} + +
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} diff --git a/template/quitesimple/page_view_revision b/template/quitesimple/page_view_revision new file mode 100644 index 0000000..2227ad3 --- /dev/null +++ b/template/quitesimple/page_view_revision @@ -0,0 +1,6 @@ +{wikiqs:include:page_header} +
    +Showing revision: {wikiqs:var:revision}
    +{wikiqs:var:content} +
    +{wikiqs:include:page_footer} diff --git a/template/quitesimple/recentchanges b/template/quitesimple/recentchanges new file mode 100644 index 0000000..75ade1d --- /dev/null +++ b/template/quitesimple/recentchanges @@ -0,0 +1,8 @@ +{wikiqs:include:general_header} +
    + +{wikiqs:var:revisionlist} +
    PageRevisionAuthorCommentDate
    +{wikiqs:var:prevpage} {wikiqs:var:nextpage} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/search b/template/quitesimple/search new file mode 100644 index 0000000..29e4fc3 --- /dev/null +++ b/template/quitesimple/search @@ -0,0 +1,7 @@ +{wikiqs:include:general_header} +
    +

    Search for: {wikiqs:var:searchterm}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} + diff --git a/template/quitesimple/show_category b/template/quitesimple/show_category new file mode 100644 index 0000000..f0b22ca --- /dev/null +++ b/template/quitesimple/show_category @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Category: {wikiqs:var:categoryname}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/show_whatlinkshere b/template/quitesimple/show_whatlinkshere new file mode 100644 index 0000000..4a629ad --- /dev/null +++ b/template/quitesimple/show_whatlinkshere @@ -0,0 +1,6 @@ +{wikiqs:include:general_header} +
    +

    Links to: {wikiqs:var:pagename}

    +{wikiqs:var:pagelist} +
    +{wikiqs:include:general_footer} diff --git a/template/quitesimple/style.css b/template/quitesimple/style.css new file mode 100644 index 0000000..4a9f09c --- /dev/null +++ b/template/quitesimple/style.css @@ -0,0 +1,213 @@ +body +{ + padding: 0; + margin: 0; + font-family: Verdana; + background-color: white; + display: grid; + min-height: 100vh; + grid-template-rows: auto 1fr auto; + grid-template-areas: "nav nav" + "main side" + "footer footer"; + grid-template-columns: 1fr auto; +} + +header +{ + margin: 0; + paddin: 0; +} + +h1, h2, h3 +{ + margin: 0; + padding: 0; + display: inline; +} + +nav +{ + padding: 0px; + margin: 0px; + display: flex; + background-color: #062463; + justify-content: space-between; + flex-wrap: wrap; + grid-area: nav; + +} +nav ul +{ + background-color: #062463; + color: white; + margin: 0; + padding: 0; + list-style-type: none; + display: flex; + align-items: center; + flex-wrap: wrap; + + +} +nav li +{ + margin: 0; + padding: 0; + + + +} + +nav a, nav a:visited +{ + padding: 10px; + text-decoration: none; + color: white; + display: block; + font-weight: bold; + text-align: center; + line-height: 100%; + +} + +nav a:hover, nav a:focus +{ + padding: 10px; + text-decoration: none; + background-color: white; + color: #062463; + display: block; + font-weight: bold; +} + + + +a, a:visited +{ + color: #062463;; +} + +a:hover +{ + background-color: #062463; + color: white; + +} + +#content +{ +padding: 15px; +font-family: monospace; +font-size: 14pt; +flex: 1; +grid-area: main +} + +#sidebar +{ +grid-area: side; + +} + +#sidebar ul +{ +list-style-type: none; + +} + +#sidebar a, a:visited +{ + color: #062463; + +} + +#sidebar a:hover +{ + background-color: #062463; + color: white; +} + +#content a, a:visited +{ + color: #062463; +} + +#content a:hover +{ + background-color: #062463; + color: white; + + +} +footer +{ + width: 100%; + display: block; + color: white; + background-color: #062463; + font-weight: bold; + grid-area: footer; +} + +footer ul +{ + background-color: #062463; + margin: 0px; + padding: 0px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + align-items: center; +} +footer li +{ + margin: 0; + padding: 0; + display: inline-block; + line-height: 45px; + color: white; + font-weight: bold; + //flex: 1 1 0; + text-align: center; +} + +footer a, a:visited +{ + text-decoration: none; + + color: white; + display: inline-block; +} + +footer a:hover, ul#nav a:focus +{ + text-decoration: none; + color: #062463; + background-color: white; + display: inline-block; +} + +#cats +{ +background-color: #062463; +} + +.letter_search_result +{ + text-decoration: underline; + font-weight: bold; +} +ol +{ + counter-reset: item; +} +.indexlink +{ +display: block; +} +.notexists +{ + color: red !important; + font-weight: bold; +} diff --git a/template/quitesimple/user_changepw b/template/quitesimple/user_changepw new file mode 100644 index 0000000..5f94341 --- /dev/null +++ b/template/quitesimple/user_changepw @@ -0,0 +1,15 @@ +{wikiqs:include:general_header} +
    +

    Login

    +Change your current password + +Current password:
    +New Password:
    +Repeat password:
    + + + + + +
    +{wikiqs:include:general_footer} diff --git a/templatepage.cpp b/templatepage.cpp new file mode 100644 index 0000000..350a9f5 --- /dev/null +++ b/templatepage.cpp @@ -0,0 +1,44 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "templatepage.h" +#include "varreplacer.h" +#include "utils.h" +TemplatePage::TemplatePage() +{ + +} + +TemplatePage::TemplatePage(std::string content) +{ + this->content = content; +} + +void TemplatePage::setVar(const std::string &key, std::string value) +{ + this->varsMap[key] = value; +} + +std::string TemplatePage::render() const +{ + Varreplacer replacer("{wikiqs:"); + replacer.addResolver("var", [&](std::string_view key) { return utils::getKeyOrEmpty(this->varsMap, std::string(key));}); + return replacer.parse(this->content); +} diff --git a/templatepage.h b/templatepage.h new file mode 100644 index 0000000..acf42c8 --- /dev/null +++ b/templatepage.h @@ -0,0 +1,20 @@ +#ifndef TEMPLATEPAGE_H +#define TEMPLATEPAGE_H +#include +#include +#include +class TemplatePage +{ +private: + std::string content; + std::map varsMap; +public: + TemplatePage(); + TemplatePage(std::string content); + + std::string render() const; + + void setVar(const std::string &key, std::string value); +}; + +#endif // TEMPLATEPAGE_H diff --git a/tests/parser.h b/tests/parser.h new file mode 100644 index 0000000..aa97f47 --- /dev/null +++ b/tests/parser.h @@ -0,0 +1,11 @@ +#ifndef PARSER_H +#define PARSER_H + + +class parser +{ +public: + parser(); +}; + +#endif // PARSER_H \ No newline at end of file diff --git a/tests/request.cpp b/tests/request.cpp new file mode 100644 index 0000000..de774f6 --- /dev/null +++ b/tests/request.cpp @@ -0,0 +1,60 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "../request.h" + + +TEST(Request, initGetMap) +{ + Request r; + r.initGetMap("hi=there&bye=here"); + ASSERT_TRUE(r.get("hi") == "there"); + ASSERT_TRUE(r.get("bye") == "here"); + + Request r2; + r2.initGetMap("hi==="); + ASSERT_TRUE(r2.get("hi") == "=="); + + Request r3; + r3.initGetMap("abcdef=aaa&&&&&dddddd=2"); + ASSERT_TRUE(r3.get("abcdef") == "aaa"); + ASSERT_TRUE(r3.get("dddddd") == "2"); + ASSERT_TRUE(r3.get("&") == ""); + + Request xss; + xss.initGetMap("q=\">x\""); + + ASSERT_TRUE(xss.get("q") != ""); + ASSERT_TRUE(xss.get("q").find("<") == std::string::npos); + + +} + +TEST(Request, initCookies) +{ + Request r1; + r1.initCookies("aaaa=22aa"); + ASSERT_TRUE(r1.cookie("aaaa") == "22aa"); +} diff --git a/tests/template.cpp b/tests/template.cpp new file mode 100644 index 0000000..60b31f3 --- /dev/null +++ b/tests/template.cpp @@ -0,0 +1,42 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "../utils.h" +#include "../config.h" +#include "../template.h" +#include "testconfigprovider.h" +class TemplateTest : public ::testing::Test +{ +public: + + Template *testTemplate; + TemplateTest() + { + Config config = TestConfigProvider::testConfig(); + + testTemplate = new Template { config }; + } + +}; diff --git a/tests/testconfigprovider.cpp b/tests/testconfigprovider.cpp new file mode 100644 index 0000000..86277a9 --- /dev/null +++ b/tests/testconfigprovider.cpp @@ -0,0 +1,33 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "testconfigprovider.h" + +TestConfigProvider::TestConfigProvider() +{ + +} + +Config TestConfigProvider::testConfig() +{ + ConfigReader configreader("config"); + Config config = configreader.readConfig(); + return config; +} diff --git a/tests/testconfigprovider.h b/tests/testconfigprovider.h new file mode 100644 index 0000000..7f382fd --- /dev/null +++ b/tests/testconfigprovider.h @@ -0,0 +1,13 @@ +#ifndef TESTCONFIGPROVIDER_H +#define TESTCONFIGPROVIDER_H +#include "../config.h" + +class TestConfigProvider +{ +public: + TestConfigProvider(); + + static Config testConfig(); +}; + +#endif // TESTCONFIGPROVIDER_H diff --git a/tests/utils.cpp b/tests/utils.cpp new file mode 100644 index 0000000..837127b --- /dev/null +++ b/tests/utils.cpp @@ -0,0 +1,101 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "../utils.h" +TEST(Utils, hasKey) { + std::map testmap; + testmap["test"] = 23; + testmap["test2"] = 24; + ASSERT_TRUE(utils::hasKey(testmap, std::string { "test" })); + ASSERT_TRUE(utils::hasKey(testmap, std::string { "test2"})); + ASSERT_FALSE(utils::hasKey(testmap, std::string { "testthere" })); + ASSERT_FALSE(utils::hasKey(testmap, std::string { })); + +} + +TEST(Utils, urldecode) +{ + std::string testr1 = "abc123=23"; + std::string decoded = utils::urldecode(testr1); + ASSERT_TRUE(testr1 == decoded); + + std::string testr2 = "a%20b"; + std::string decoded2 = utils::urldecode(testr2); + std::string expected2 = "a b"; + ASSERT_TRUE(decoded2 == expected2); + + std::string testr3 = "a%"; + std::string expected3 = "a%"; + + std::string decoded3 = utils::urldecode(testr3); + ASSERT_TRUE(testr3 == expected3); + +} + +TEST(UTILS, toUInt) +{ + EXPECT_NO_THROW({ + std::string number = "23"; + unsigned int expected = 23; + unsigned int actual = utils::toUInt(number); + ASSERT_EQ(expected, actual); + }); + + ASSERT_THROW(utils::toUInt("abc"), std::invalid_argument); + ASSERT_THROW(utils::toUInt("999999999999999999999"), std::out_of_range); + +} + +TEST(UTILS, html_xss) +{ + std::string input = ""; + std::string escaped = utils::html_xss(input); + + ASSERT_TRUE(escaped.find('<') == std::string::npos); +} + +TEST(UTILS, strreplace) +{ + + std::string input = "ABCHelloDEF"; + std::string output = utils::strreplace(input, "Hello", "Bye"); + ASSERT_TRUE("ABCByeDEF" == output); + + input = "XXLeaveUsYY"; + output = utils::strreplace(input, "NotFoundInString", "WithSomething"); + + ASSERT_TRUE(output == input); + + input = "AA2233"; + output = utils::strreplace(input, "A", "1"); + + ASSERT_TRUE(output == "112233"); + + input="someTESTtest"; + output = utils::strreplace(input, "TEST", "TEST"); + + ASSERT_TRUE(output == input); +} + diff --git a/urlprovider.cpp b/urlprovider.cpp new file mode 100644 index 0000000..bbb4303 --- /dev/null +++ b/urlprovider.cpp @@ -0,0 +1,116 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "urlprovider.h" + + +std::string replaceSingleVar(std::string where, std::string varname, std::string replacement) +{ + //TODO: Varreplacer is a bit of an overkill, isn't it? + + Varreplacer replacer("{"); + replacer.addKeyValue(varname, replacement); + return replacer.parse(where); +} +std::string UrlProvider::replaceOnlyPage(std::string templatepart, std::string page) +{ + return replaceSingleVar(templatepart, "page", page); +} + +std::string UrlProvider::index() +{ + return config->linkindex; +} + +std::string UrlProvider::recentSorted(unsigned int limit, unsigned int offset, unsigned int sort) +{ + Varreplacer replace("{"); + replace.addKeyValue("limit", std::to_string(limit)); + replace.addKeyValue("offset", std::to_string(offset)); + replace.addKeyValue("sort", std::to_string(sort)); + return replace.parse(config->linkrecentsort); +} + +std::string UrlProvider::allPages() +{ + return config->linkallpages; +} + +std::string UrlProvider::allCats() +{ + return config->linkallcats; +} + +std::string UrlProvider::page(std::string pagename) +{ + return replaceOnlyPage(config->linkpage, pagename); +} + +std::string UrlProvider::linksHere(std::string pagename) +{ + return replaceOnlyPage(config->linkshere, pagename); +} + +std::string UrlProvider::pageHistory(std::string pagename) +{ + return replaceOnlyPage(config->linkhistory, pagename); +} + +std::string UrlProvider::pageHistorySort(std::string pagename, unsigned int limit, unsigned int offset, unsigned int sort) +{ + Varreplacer replace("{"); + replace.addKeyValue("page", pagename); + replace.addKeyValue("limit", std::to_string(limit)); + replace.addKeyValue("offset", std::to_string(offset)); + replace.addKeyValue("sort", std::to_string(sort)); + return replace.parse(config->linkhistorysort); +} + +std::string UrlProvider::pageRevision(std::string pagename, unsigned int revision) +{ + Varreplacer replace("{"); + replace.addKeyValue("page", pagename); + replace.addKeyValue("revisionid", std::to_string(revision)); + return replace.parse(config->linkrevision); +} + +std::string UrlProvider::editPage(std::string pagename) +{ + return replaceOnlyPage(config->linkedit, pagename); +} + +std::string UrlProvider::pageSettings(std::string pagename) +{ + return replaceOnlyPage(config->linksettings, pagename); +} + +std::string UrlProvider::pageDelete(std::string pagename) +{ + return replaceOnlyPage(config->linkdelete, pagename); +} + +std::string UrlProvider::category(std::string catname) +{ + return replaceSingleVar(config->linkcategory, "category", catname); +} +std::string UrlProvider::login(std::string page) +{ + return replaceOnlyPage(config->loginurl, page); +} diff --git a/urlprovider.h b/urlprovider.h new file mode 100644 index 0000000..47bd298 --- /dev/null +++ b/urlprovider.h @@ -0,0 +1,51 @@ +#ifndef LINKCREATOR_H +#define LINKCREATOR_H +#include "config.h" +#include "varreplacer.h" +class UrlProvider +{ +private: + const Config *config; + + std::string replaceOnlyPage(std::string templatepart, std::string page); +public: + UrlProvider(const Config &config) { this->config = &config; } + + std::string index(); + + std::string recent(); + + std::string recentSorted(unsigned int limit, unsigned int offset, unsigned int sort); + + std::string allPages(); + + std::string allCats(); + + std::string page(std::string pagename); + + std::string linksHere(std::string pagename); + + std::string pageHistory(std::string pagename); + + std::string pageHistorySort(std::string pagename, unsigned int limit, unsigned int offset, unsigned int sort); + + std::string pageRevision(std::string pagename, unsigned int revision); + + std::string editPage(std::string pagename); + + std::string pageSettings(std::string pagename); + + + std::string pageDelete(std::string pagename); + + std::string userchangepw(); + + std::string refreshSession(); + + std::string category(std::string catname); + + std::string login(std::string page); + +}; + +#endif // LINKCREATOR_H diff --git a/user.cpp b/user.cpp new file mode 100644 index 0000000..c28ad7f --- /dev/null +++ b/user.cpp @@ -0,0 +1,35 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include "user.h" + +User User::anonUser; + +const User &User::Anonymous() +{ + return User::anonUser; +} + + +User::User() +{ + +} + diff --git a/user.h b/user.h new file mode 100644 index 0000000..57707bb --- /dev/null +++ b/user.h @@ -0,0 +1,24 @@ +#ifndef USER_H +#define USER_H +#include +#include +#include "permissions.h" +class User +{ +private: + static User anonUser; + public: + static const User &Anonymous(); + static void setAnon(User u) + { + User::anonUser = std::move(u); + } + std::string login; + std::vector password; + std::vector salt; + Permissions permissions; + User(); + +}; + +#endif diff --git a/utils.cpp b/utils.cpp new file mode 100644 index 0000000..01ccde0 --- /dev/null +++ b/utils.cpp @@ -0,0 +1,178 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include +#include +#include +#include "logger.h" +#include "utils.h" +//TODO: instead of returning vector maybe provide an iterator version too. + +//TODO: % may not be necessary (was in C version just to be sure against format string attacks +//TODO: hopefully not too slow looking up every character here: +const std::map replacements = { {'<', "<"}, {'>', "gt;"}, { '\"', """ }, { '%', "%" }}; +std::string utils::html_xss(std::string_view str) +{ + std::string result; + int size = str.length(); + for(int i = 0; i < size; i++) + { + char c = str[i]; + auto val = replacements.find(c); + if(val != replacements.end()) + { + result += val->second; + } + else + { + result += c; + } + } + return result; +} + +std::string utils::urldecode(std::string_view str) +{ + std::string result; + int size = str.length(); + for(int i = 0; i < size; i++) + { + char c = str[i]; + if(c == '%' && (size-i>1)) + { + char h[3]; + h[0] = str[i+1]; + h[1] = str[i+2]; + h[2] = 0; + if(std::isxdigit(h[0]) && std::isxdigit(h[1])) + { + c = std::stoi( h , 0, 16); + i+=2; + } + + } + result += c; + + } + return result; +} + +std::vector utils::splitByChar(const std::string &str, char delim) +{ + std::vector result; + std::stringstream stream(str); + std::string item; + while (std::getline(stream, item, delim)) { + result.push_back(item); + } + return result; +} + +//TODO: can easily break if we pass a regex here +std::vector utils::splitByString(const std::string &str, const std::string &delim) +{ + return splitByRegex(str, delim + "+"); +} +std::vector utils::splitByRegex(const std::string &str, const std::string ®ex) +{ + std::vector result; + std::regex reg(regex); + std::copy( std::sregex_token_iterator(str.begin(), str.end(), reg, -1),std::sregex_token_iterator(), + std::back_inserter(result)); + return result; +} + + +std::string utils::strreplace(const std::string &str, const std::string &search, const std::string &replace) +{ + std::string result = str; + auto searchlength = search.length(); + auto replacelength = replace.length(); + size_t pos=0; + while((pos = result.find(search, pos)) != std::string::npos) + { + result = result.replace(pos, searchlength, replace); + pos += replacelength; + } + return result; +} + + + + +std::string utils::getenv(const std::string &key) +{ + const char *result = ::getenv(key.c_str()); + if(result == nullptr) + return std::string(); + return std::string { result }; +} + +std::string utils::readCompleteFile(std::string_view filepath) +{ + + std::fstream stream(std::string { filepath }); + std::stringstream ss; + ss << stream.rdbuf(); + std::string content = ss.str(); + return content; +} + +std::string utils::regex_callback_replacer(std::regex regex, const std::string &input, std::function callback) +{ + std::string result; + auto tagsbegin = std::sregex_iterator(input.begin(), input.end(), regex); + auto tagsend = std::sregex_iterator(); + auto matchbegin = 0; + for (std::sregex_iterator i = tagsbegin; i != tagsend; ++i) { + std::smatch match = *i; + + auto matchlength = match.length(0); + auto matchpos = match.position(); + + result += input.substr(matchbegin, matchpos-matchbegin); + result += callback(match); + matchbegin = matchpos + matchlength; + + + } + result += input.substr(matchbegin); + return result; +} + +std::string utils::toISODate(time_t t) +{ + struct tm *lt = localtime(&t); + if(lt == nullptr) + { + return { }; + } + char result[20]; + size_t x = strftime(result, sizeof(result), "%Y-%m-%d %H:%M:%S", lt); + if(x == 0) + { + return { }; + } + return std::string { result }; + +} diff --git a/utils.h b/utils.h new file mode 100644 index 0000000..f159171 --- /dev/null +++ b/utils.h @@ -0,0 +1,105 @@ +#ifndef UTILS_H +#define UTILS_H +#include +#include +#include +#include +#include +#include +#include +#include +namespace utils +{ + + std::vector splitByChar(const std::string &str, char delim); + std::vector splitByString(const std::string &str, const std::string &delim); + std::vector splitByRegex(const std::string &str, const std::string ®ex); + std::string urldecode(std::string_view str); + std::string strreplace(const std::string &str, const std::string &search, const std::string &replace); + + std::string html_xss(std::string_view str); + std::string getenv(const std::string &key); + + template + bool hasKey(const std::map &map, T key) + { + auto k = map.find(key); + return k != map.end(); + } + + template + U getKeyOrEmpty(const std::map &map, T key) + { + auto k = map.find(key); + if(k != map.end()) + { + return k->second; + } + return U(); + } + + template + U getKeyOrEmpty(std::multimap map, T key) + { + auto k = map.find(key); + if(k != map.end()) + { + return k->second; + } + return U(); + } + + template + std::vector getAll(std::multimap map, T key) + { + std::vector result; + auto range = map.equal_range(key); + for(auto it = range.first; it != range.second; it++) + { + result.push_back(it->second); + } + return result; + } + + std::string regex_callback_replacer(std::regex regex, const std::string &input, std::function callback ); + + std::string readCompleteFile(std::string_view filepath); + + inline std::string nz(const char *s) { + if(s == nullptr) + { + return std::string { }; + } + return std::string { s }; + } + + //TODO: optional + inline unsigned int toUInt(const std::string &str) + { + if(str == "") + { + return 0; + } + auto result = std::stoul(str); + if(result > std::numeric_limits::max()) + { + throw std::out_of_range(str + " is too large for unsigned int "); + } + return result; + } + + + + std::string toISODate(time_t t); + + + template + inline std::string toString(const T &v) + { + return std::string(v.begin(), v.end()); + } + + + +} +#endif diff --git a/varreplacer.cpp b/varreplacer.cpp new file mode 100644 index 0000000..07bdfeb --- /dev/null +++ b/varreplacer.cpp @@ -0,0 +1,106 @@ +/* Copyright (c) 2018 Albert S. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include "varreplacer.h" +#include "utils.h" +#include "logger.h" +//TODO: postfix +Varreplacer::Varreplacer(std::string_view prefix) +{ + this->prefix = prefix; +} + + +std::tuple Varreplacer::extractKeyAndValue(std::string_view var) +{ + var.remove_prefix(prefix.length()); + size_t colonPos = var.find(':'); + //HACK + if(colonPos == std::string_view::npos) + { + std::string_view key = var; + std::string_view value = var; + return std::make_tuple(key, value); + } + std::string_view key = var.substr(0, colonPos); + std::string_view value = var.substr(colonPos+1); + + return std::make_tuple(key, value); +} +void Varreplacer::addResolver(std::string_view key, std::function resolver) +{ + this->resolverFunctionsMap.insert(std::make_pair(key, resolver)); +} + +void Varreplacer::addKeyValue(std::string_view key, std::string value) +{ + this->keyValues.insert(std::make_pair(key, value)); +} + +std::string Varreplacer::makeReplacement(std::string_view varkeyvalue) +{ + std::string_view key; + std::string_view value; + + std::tie(key, value) = extractKeyAndValue(varkeyvalue); + if(utils::hasKey(keyValues, key)) + { + std::string replacementContent = keyValues[key]; + return replacementContent; + } + else if(utils::hasKey(resolverFunctionsMap, key)) + { + + auto resolver = this->resolverFunctionsMap[key]; + std::string replacementContent = resolver(value); + return replacementContent; + } + + return std::string { varkeyvalue } + '}'; +} + +std::string Varreplacer::parse(std::string_view content) +{ + std::string result; + + size_t pos; + while( (pos = content.find(prefix)) != std::string_view::npos) + { + if(pos != 0) + { + auto part = content.substr(0, pos); + + result += part; + content.remove_prefix(pos); + } + auto endpos = content.find("}"); + if(endpos == std::string_view::npos) + { + throw "misformated"; + } + std::string_view varkeyvalue = content.substr(0, endpos); + result += makeReplacement(varkeyvalue); + content.remove_prefix(endpos+1); + } + result += content; + + return result; +} diff --git a/varreplacer.h b/varreplacer.h new file mode 100644 index 0000000..c9c0cd0 --- /dev/null +++ b/varreplacer.h @@ -0,0 +1,26 @@ +#ifndef VARPARSER_H +#define VARPARSER_H +#include +#include +#include +#include +#include +class Varreplacer +{ +private: + std::string_view content; + std::string_view prefix; + std::map> resolverFunctionsMap; + std::map keyValues; + std::tuple extractKeyAndValue(std::string_view var); + std::string makeReplacement(std::string_view varkeyvalue); +public: + Varreplacer(std::string_view prefix); + + void addKeyValue(std::string_view key, std::string value); + void addResolver(std::string_view key, std::function resolver); + std::string parse(std::string_view content); + +}; + +#endif // VARPARSER_H