commit 3bfebfe8a84d01c85fef5091d7a7e994b7e7510e 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..419a420 --- /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..39a7351 --- /dev/null +++ b/cache/fscache.h @@ -0,0 +1,23 @@ +#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..6bae4f2 --- /dev/null +++ b/cache/icache.h @@ -0,0 +1,20 @@ +#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..70f9036 --- /dev/null +++ b/category.cpp @@ -0,0 +1,25 @@ +/* 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..83a517f --- /dev/null +++ b/category.h @@ -0,0 +1,13 @@ +#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..f0c3fba --- /dev/null +++ b/config.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 "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..0f70606 --- /dev/null +++ b/config.h @@ -0,0 +1,72 @@ +#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..fafd35f --- /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..dba95c5 --- /dev/null +++ b/database/categorydao.cpp @@ -0,0 +1,25 @@ +/* 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..df2711a --- /dev/null +++ b/database/categorydao.h @@ -0,0 +1,20 @@ +#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..762fd6d --- /dev/null +++ b/database/categorydaosqlite.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 +#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(?)" + << c.name << c.name; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +void CategoryDaoSqlite::deleteCategory(std::string name) +{ + try + { + + *db << "BEGIN"; + *db << "DELETE FROM categorymember WHERE catid = (SELECT id FROM category WHERE name = ?)" << name; + *db << "DELETE FROM category WHERE name = ?" << name; + *db << "COMMIT;"; + } + catch(sqlite::sqlite_exception &e) + { + throwFrom(e); + } +} + +std::vector 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..186c8cf --- /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..c8e8bc1 --- /dev/null +++ b/database/database.h @@ -0,0 +1,40 @@ +#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..041752e --- /dev/null +++ b/database/exceptions.h @@ -0,0 +1,15 @@ +#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..238b9d8 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp.h @@ -0,0 +1,1160 @@ +#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::value || std::is_integral::value || + std::is_same::value || + std::is_same::value || + std::is_same::value> + { + }; + template + struct is_sqlite_value> + : public std::integral_constant::value || std::is_integral::value || + std::is_same::value> + { + }; +#ifdef MODERN_SQLITE_STD_VARIANT_SUPPORT + template + struct is_sqlite_value> : public std::integral_constant + { + }; +#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 +inline void step(sqlite3_context *db, int count, sqlite3_value **vals); + +template +inline typename std::enable_if<(sizeof...(Values) && sizeof...(Values) < Count), void>::type step(sqlite3_context *db, + int count, + sqlite3_value **vals, + Values &&...values); + +template +inline typename std::enable_if<(sizeof...(Values) == Count), void>::type step(sqlite3_context *db, int, + sqlite3_value **, Values &&...values); + +template inline void final(sqlite3_context *db); + +template +inline typename std::enable_if<(sizeof...(Values) < Count), void>::type scalar(sqlite3_context *db, int count, + sqlite3_value **vals, + Values &&...values); + +template +inline typename std::enable_if<(sizeof...(Values) == Count), void>::type scalar(sqlite3_context *db, int, + sqlite3_value **, Values &&...values); +} // namespace sql_function_binder + +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 + using nth_argument_type = typename utility::function_traits::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 + static typename std::enable_if<(sizeof...(Values) < Boundary), void>::type run(database_binder &db, + Function &&function, + Values &&...values) + { + typename std::remove_cv< + typename std::remove_reference>::type>::type value{}; + get_col_from_db(db, sizeof...(Values), value); + + run(db, function, std::forward(values)..., std::move(value)); + } + + template + 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 +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 +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::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 +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 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 +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::template argument>::type>::type value{}; + get_val_from_db(vals[sizeof...(Values)], value); + + scalar(db, count, vals, std::forward(values)..., std::move(value)); +} + +template +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); + } +} +} // namespace sql_function_binder +} // namespace sqlite diff --git a/database/hdr/sqlite_modern_cpp/errors.h b/database/hdr/sqlite_modern_cpp/errors.h new file mode 100644 index 0000000..a472bca --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/errors.h @@ -0,0 +1,100 @@ +#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 errors +namespace exceptions = errors; +} // namespace sqlite 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..3ac8e37 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/lists/error_codes.h @@ -0,0 +1,90 @@ +#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..c636076 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/log.h @@ -0,0 +1,124 @@ +#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)); +} +} // namespace detail +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)); +} +} // namespace sqlite diff --git a/database/hdr/sqlite_modern_cpp/sqlcipher.h b/database/hdr/sqlite_modern_cpp/sqlcipher.h new file mode 100644 index 0000000..f6eeb07 --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/sqlcipher.h @@ -0,0 +1,53 @@ +#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); + } +}; +} // namespace sqlite 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..770e9bb --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/function_traits.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace sqlite +{ +namespace utility +{ + +template struct function_traits; + +template +struct function_traits : public function_traits::type::operator())> +{ +}; + +template +struct function_traits : function_traits +{ +}; + +/* support the non-const operator () + * this will work with user defined functors */ +template +struct function_traits : function_traits +{ +}; + +template struct function_traits +{ + typedef ReturnType result_type; + + template using argument = typename std::tuple_element>::type; + + static const std::size_t arity = sizeof...(Arguments); +}; + +} // namespace utility +} // namespace sqlite 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..d980cfa --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/uncaught_exceptions.h @@ -0,0 +1,34 @@ +#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 +} // namespace utility +} // namespace sqlite 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..d12674c --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/utf16_utf8.h @@ -0,0 +1,45 @@ +#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..fec6c8f --- /dev/null +++ b/database/hdr/sqlite_modern_cpp/utility/variant.h @@ -0,0 +1,230 @@ +#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< + std::enable_if_t::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); */ + } + }; +} +} // namespace sqlite::utility diff --git a/database/pagedao.cpp b/database/pagedao.cpp new file mode 100644 index 0000000..ce2e891 --- /dev/null +++ b/database/pagedao.cpp @@ -0,0 +1,25 @@ +/* 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..6c71331 --- /dev/null +++ b/database/pagedao.h @@ -0,0 +1,30 @@ +#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..3467059 --- /dev/null +++ b/database/pagedaosqlite.cpp @@ -0,0 +1,207 @@ +/* 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..d4d7290 --- /dev/null +++ b/database/pagedaosqlite.h @@ -0,0 +1,29 @@ +#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..4fa2092 --- /dev/null +++ b/database/permissionsdao.cpp @@ -0,0 +1,25 @@ +/* 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..80b12ea --- /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..2d74cb2 --- /dev/null +++ b/database/permissionsdaosqlite.cpp @@ -0,0 +1,31 @@ +/* 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..42410f4 --- /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..0e0ef15 --- /dev/null +++ b/database/queryoption.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 "queryoption.h" diff --git a/database/queryoption.h b/database/queryoption.h new file mode 100644 index 0000000..12160ba --- /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..0774b36 --- /dev/null +++ b/database/revisiondao.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 "revisiondao.h" diff --git a/database/revisiondao.h b/database/revisiondao.h new file mode 100644 index 0000000..05fdc64 --- /dev/null +++ b/database/revisiondao.h @@ -0,0 +1,23 @@ +#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..d735e76 --- /dev/null +++ b/database/revisiondaosqlite.cpp @@ -0,0 +1,174 @@ +/* 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..6c38305 --- /dev/null +++ b/database/revisiondaosqlite.h @@ -0,0 +1,21 @@ +#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..95e4933 --- /dev/null +++ b/database/sessiondao.cpp @@ -0,0 +1,25 @@ +/* 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..d58e86b --- /dev/null +++ b/database/sessiondao.h @@ -0,0 +1,18 @@ +#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..57c9cbc --- /dev/null +++ b/database/sessiondaosqlite.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 "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..4b901a3 --- /dev/null +++ b/database/sessiondaosqlite.h @@ -0,0 +1,17 @@ +#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..cf74691 --- /dev/null +++ b/database/sqlite.cpp @@ -0,0 +1,86 @@ +/* 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..4af14d1 --- /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..5e26c36 --- /dev/null +++ b/database/sqlitedao.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 "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..0d30df5 --- /dev/null +++ b/database/sqlitedao.h @@ -0,0 +1,42 @@ +#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..2adbd30 --- /dev/null +++ b/database/sqlitequeryoption.cpp @@ -0,0 +1,72 @@ +/* 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..13d4184 --- /dev/null +++ b/database/sqlitequeryoption.h @@ -0,0 +1,27 @@ +#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..d907663 --- /dev/null +++ b/database/userdao.cpp @@ -0,0 +1,25 @@ +/* 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..20cd271 --- /dev/null +++ b/database/userdao.h @@ -0,0 +1,18 @@ +#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..1cd8d4b --- /dev/null +++ b/database/userdaosqlite.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 +#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..bcbc2df --- /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..cb2752f --- /dev/null +++ b/gateway/cgi.cpp @@ -0,0 +1,109 @@ +/* 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..70e75eb --- /dev/null +++ b/gateway/cgi.h @@ -0,0 +1,22 @@ +#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..3a8630f --- /dev/null +++ b/gateway/gatewayfactory.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 "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..dcd415b --- /dev/null +++ b/gateway/gatewayinterface.cpp @@ -0,0 +1,25 @@ +/* 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..56a736c --- /dev/null +++ b/gateway/gatewayinterface.h @@ -0,0 +1,18 @@ +#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..a8fce6a --- /dev/null +++ b/gateway/httpgateway.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 "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..50fb97d --- /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..6ed4a7b --- /dev/null +++ b/gateway/httplib.h @@ -0,0 +1,2665 @@ +// +// 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 ¶ms); + std::shared_ptr Post(const char *path, const Headers &headers, const Params ¶ms); + + 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 ¶ms) +{ + 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 ¶ms) +{ + return Post(path, Headers(), params); +} + +inline std::shared_ptr Client::Post(const char *path, const Headers &headers, const Params ¶ms) +{ + 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..dbc1f4a --- /dev/null +++ b/handlers/handler.cpp @@ -0,0 +1,76 @@ +/* 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..611d692 --- /dev/null +++ b/handlers/handler.h @@ -0,0 +1,40 @@ +#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..efe3b86 --- /dev/null +++ b/handlers/handlerallcategories.cpp @@ -0,0 +1,45 @@ +/* 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..0835977 --- /dev/null +++ b/handlers/handlerallcategories.h @@ -0,0 +1,14 @@ +#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..26c2609 --- /dev/null +++ b/handlers/handlerallpages.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 "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..cd68c26 --- /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..961f97d --- /dev/null +++ b/handlers/handlercategory.cpp @@ -0,0 +1,50 @@ +/* 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..5370004 --- /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..af53ebf --- /dev/null +++ b/handlers/handlerdefault.cpp @@ -0,0 +1,30 @@ +/* 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..3cb2ab6 --- /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..95e2eb5 --- /dev/null +++ b/handlers/handlerfactory.cpp @@ -0,0 +1,102 @@ +/* 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..f177076 --- /dev/null +++ b/handlers/handlerfactory.h @@ -0,0 +1,9 @@ +#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..2bd5f70 --- /dev/null +++ b/handlers/handlerhistory.cpp @@ -0,0 +1,104 @@ +/* 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..2988688 --- /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..281cf1d --- /dev/null +++ b/handlers/handlerinvalidaction.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 "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..b278724 --- /dev/null +++ b/handlers/handlerinvalidaction.h @@ -0,0 +1,15 @@ +#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..bc7e2c9 --- /dev/null +++ b/handlers/handlerlogin.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 "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..7772dd6 --- /dev/null +++ b/handlers/handlerlogin.h @@ -0,0 +1,22 @@ +#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..8604e2b --- /dev/null +++ b/handlers/handlerpage.cpp @@ -0,0 +1,90 @@ +/* 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..717a859 --- /dev/null +++ b/handlers/handlerpage.h @@ -0,0 +1,23 @@ +#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..23fcd96 --- /dev/null +++ b/handlers/handlerpagedelete.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 "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..2d5e664 --- /dev/null +++ b/handlers/handlerpagedelete.h @@ -0,0 +1,27 @@ +#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..9f606dd --- /dev/null +++ b/handlers/handlerpageedit.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 "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..da048dd --- /dev/null +++ b/handlers/handlerpageedit.h @@ -0,0 +1,22 @@ +#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..018089e --- /dev/null +++ b/handlers/handlerpageview.cpp @@ -0,0 +1,164 @@ +/* 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..8a970c1 --- /dev/null +++ b/handlers/handlerpageview.h @@ -0,0 +1,26 @@ +#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..e0bbf3e --- /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..43048d0 --- /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..7828644 --- /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..1895111 --- /dev/null +++ b/htmllink.cpp @@ -0,0 +1,25 @@ +/* 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..061217b --- /dev/null +++ b/htmllink.h @@ -0,0 +1,19 @@ +#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..2d815b8 --- /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..bd53f11 --- /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..26326b7 --- /dev/null +++ b/logger.h @@ -0,0 +1,71 @@ +#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..cf3ad63 --- /dev/null +++ b/page.cpp @@ -0,0 +1,25 @@ +/* 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..1cf9e6c --- /dev/null +++ b/page.h @@ -0,0 +1,14 @@ +#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..9b15375 --- /dev/null +++ b/parser.cpp @@ -0,0 +1,135 @@ +/* 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..6131325 --- /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..51cdebd --- /dev/null +++ b/permissions.cpp @@ -0,0 +1,37 @@ +/* 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..c683d59 --- /dev/null +++ b/permissions.h @@ -0,0 +1,107 @@ +#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..c14a584 --- /dev/null +++ b/qswiki.cpp @@ -0,0 +1,110 @@ +/* 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..79d4dff --- /dev/null +++ b/random.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 +#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..a817a1d --- /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..800b822 --- /dev/null +++ b/request.cpp @@ -0,0 +1,126 @@ +/* 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..d107325 --- /dev/null +++ b/request.h @@ -0,0 +1,112 @@ +#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..6870051 --- /dev/null +++ b/requestworker.cpp @@ -0,0 +1,90 @@ +/* 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..63529a9 --- /dev/null +++ b/requestworker.h @@ -0,0 +1,36 @@ +#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..3458d25 --- /dev/null +++ b/response.cpp @@ -0,0 +1,46 @@ +/* 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..55c639a --- /dev/null +++ b/response.h @@ -0,0 +1,70 @@ +#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..b664b8e --- /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..848c8b0 --- /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..55275cc --- /dev/null +++ b/session.cpp @@ -0,0 +1,45 @@ +/* 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..b1e5962 --- /dev/null +++ b/session.h @@ -0,0 +1,21 @@ +#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..9c6e202 --- /dev/null +++ b/template.cpp @@ -0,0 +1,166 @@ +/* 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..639cd7c --- /dev/null +++ b/template.h @@ -0,0 +1,38 @@ +#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..69321ef --- /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..a509b8d --- /dev/null +++ b/templatepage.h @@ -0,0 +1,21 @@ +#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..66450d6 --- /dev/null +++ b/tests/parser.h @@ -0,0 +1,10 @@ +#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..f843dc7 --- /dev/null +++ b/tests/request.cpp @@ -0,0 +1,57 @@ +/* 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..b139167 --- /dev/null +++ b/tests/template.cpp @@ -0,0 +1,40 @@ +/* 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..e62f614 --- /dev/null +++ b/tests/testconfigprovider.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 "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..245d10f --- /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..3c29081 --- /dev/null +++ b/tests/utils.cpp @@ -0,0 +1,98 @@ +/* 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..e79b6b1 --- /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..744e89a --- /dev/null +++ b/urlprovider.h @@ -0,0 +1,53 @@ +#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..cc95f78 --- /dev/null +++ b/user.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 "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..8b7efa9 --- /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..7114e29 --- /dev/null +++ b/utils.cpp @@ -0,0 +1,172 @@ +/* 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..fee00be --- /dev/null +++ b/utils.h @@ -0,0 +1,97 @@ +#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()); +} + +} // namespace utils +#endif diff --git a/varreplacer.cpp b/varreplacer.cpp new file mode 100644 index 0000000..beb2a44 --- /dev/null +++ b/varreplacer.cpp @@ -0,0 +1,105 @@ +/* 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..38e372b --- /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