commit 076106ecb9d23ee9d6a412244fd0c4aa379539ba Author: Albert S Date: Sat Nov 3 17:12:20 2018 +0100 Let's make (git) history! 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