56 Commits

Author SHA1 Message Date
0325cdf936 Parser: Add code,blockquote and begin img tag 2022-04-19 19:50:22 +02:00
b0c715c4ea Parser: Add cmd:visible, it's also a tag 2022-04-03 14:35:14 +02:00
63a4437de7 HandlerFeedGenerator: Fix comparator condition 2022-04-03 12:07:43 +02:00
c88889b10b Parser: Fix headline extraction for the default case broken by fbca85e5 2022-04-03 11:48:16 +02:00
634cb2d7ee Handlers: HandlerAllPages / HandlerCategory: Use PageListRenderer 2022-04-03 11:15:02 +02:00
1c1416934b UrlProvider: Add Links to specify rendertype in allpages / category view 2022-04-03 11:15:02 +02:00
622ef5af6a Database: PageDao/CategoryDao: Return 'Page' object, not pagename string 2022-04-03 11:15:02 +02:00
5f83981d68 utils: readCompleteFile(): Fix error string which is too generic without context 2022-04-03 11:07:26 +02:00
b5b2a42839 Add PageListRenderer: Allow rendering pagelist by creationdate and A-Z as before 2022-04-03 11:06:19 +02:00
e217218a3f Add Grouper: Maps a key to a vectors 2022-04-03 11:05:13 +02:00
82c081385b Request: createPairFromVar(): Explicitly decode value
May not be the case on POST requests.
2022-03-30 22:59:20 +02:00
91951abe9c Revert "dynamic: DynamicContentPostList: Link using UrlProvider::pageByTitle()"
This reverts commit 9b35e43161.
2022-03-29 22:45:17 +02:00
9b35e43161 dynamic: DynamicContentPostList: Link using UrlProvider::pageByTitle() 2022-03-29 22:37:45 +02:00
73a4e4c10f UrlProvider: Add pageByTitle() 2022-03-29 22:37:20 +02:00
1e224fdac6 HandlerPageView: First resolve all dynamics before parsing tags
Should make more sense this way, especially to extract headlines.
2022-03-29 22:36:05 +02:00
fbca85e5ed Parser: Seperate parseDynamcis(), fix headline extraction with tags inside them 2022-03-29 22:35:45 +02:00
15e4f081cc HandlerPage: Support lookup by title 2022-03-29 22:34:22 +02:00
e876b15c5d dynamic: Add DynamicContent{Get,Set}Var 2022-03-29 22:33:32 +02:00
3e736db0ef database: pagedao: Add findByTitle() 2022-03-29 22:30:20 +02:00
03c5646858 HandlerPageView: Parse dynamically included pages recursively 2022-03-28 21:25:37 +02:00
ba06d04a08 HandlerFeedGenerator: Error when cat does not exists (instead of empty feed) 2022-03-28 20:24:57 +02:00
5bb3f55945 HandlerFeedGenerator: Improvements to make feed vlaid 2022-03-28 20:06:42 +02:00
1ae5495e61 Dynamic: Add DynamicContentIncludePage to allow including pages 2022-03-27 21:36:53 +02:00
7bb7600d39 HandlerFeedGenerator: Add caching 2022-03-27 21:22:00 +02:00
f5eb36e7bb DynamicContentPostList: Ignore invisible entries 2022-03-27 20:03:28 +02:00
c891b36339 Makefile: Build dynamic content generators, adjust for exile update 2022-03-27 20:00:21 +02:00
d17e596563 sandbox-linux: include exile.hpp 2022-03-27 19:59:52 +02:00
761471f243 template: Add template for atom feed 2022-03-27 19:54:07 +02:00
9ac0ad0ccd template: Add template for dynamic postlist 2022-03-27 19:53:48 +02:00
c30e09d44d HandlerFactory: Wire up HandlerFeedGenerator 2022-03-27 19:52:45 +02:00
bcc3737d88 UrlProvider: Introduce combine(), rootUrl(), atomFeed() 2022-03-27 19:51:53 +02:00
9520aabe5c Config: Require rooturl,atomurl 2022-03-27 19:50:51 +02:00
4854ea85f2 Begin HandlerFeedGenerator: Generates Atom feeds for categories (or all pages) 2022-03-27 19:48:57 +02:00
16c352c6af utils: readCompleteFile(): Throw exception if file can't be opened 2022-03-27 19:47:52 +02:00
f7cf06cdd5 Page: Add 'title' column, storing title of last revision 2022-03-27 09:23:35 +02:00
ac793c6d39 handlers: HandlerPageView: Add '[dynamic:postlist]' tag by callback 2022-03-27 08:37:55 +02:00
a524674149 Begin dynamic content generators 2022-03-27 08:36:25 +02:00
a4a45d9add Parser: Add callback support for unknown "tags" 2022-03-27 08:31:59 +02:00
44c27ed8b4 Template: Make loadResolvedPart() public 2022-03-27 08:30:51 +02:00
433b5da2bb template: Adjust after renaming: Use utils::toISODateTime() 2022-03-27 08:30:20 +02:00
c5435c52f4 utils: Rename/Add date functions 2022-03-27 08:29:13 +02:00
b2a7ea4031 Parser: Take 'content' by const reference. 2022-01-23 10:12:37 +01:00
1d5bf80710 HandlerPageView: Add [cmd:pagetitle] to set custom per-page titles 2022-01-23 10:02:46 +01:00
ca0c8a94fb sandbox: Use exile.h vow promises 2021-12-29 11:13:47 +01:00
5870102aa9 submodules: cpp-httplib: Update to v0.10.1 2021-12-29 11:13:05 +01:00
32544c8f68 submodules: cpp-httplib: Update module 2021-12-02 10:15:36 +01:00
d0e7ff0a8c sandbox: Switch to exile.h (former qssb.h) 2021-12-02 10:15:11 +01:00
696ff9b7e7 sandbox: Allow TIME group 2021-12-02 10:06:21 +01:00
5570154113 fscache: Fix starts_with() broken by b41a5f4e5b 2021-11-30 19:14:59 +01:00
4f6bcd27b4 sandbox: Sync iwth qssb.h upstream: Use whitelisting and groups 2021-11-14 21:54:08 +01:00
bbe74a2c50 handlers: HandlerSearch: Add missing call to setGeneralVars() 2021-11-14 21:54:08 +01:00
5db9305408 template: display headers inline (backport from production) 2021-11-14 21:54:08 +01:00
c90e26a374 template: Remove space between header links 2021-10-26 23:22:05 +02:00
b297498ca9 template: Don't render searchbar in portrait. Show link instead
Issue: #18
2021-10-26 23:07:37 +02:00
fdcef18861 HandlerSearch: Render a form when no q= given 2021-10-26 23:07:37 +02:00
75268e0073 sandbox: Disable Landlock due to qssb.h issue #19 2021-10-26 23:07:37 +02:00
71 changed files with 953 additions and 176 deletions

6
.gitmodules vendored
View File

@ -4,6 +4,6 @@
[submodule "submodules/cpp-httplib"] [submodule "submodules/cpp-httplib"]
path = submodules/cpp-httplib path = submodules/cpp-httplib
url = https://github.com/yhirose/cpp-httplib url = https://github.com/yhirose/cpp-httplib
[submodule "submodules/qssb.h"] [submodule "submodules/exile.h"]
path = submodules/qssb.h path = submodules/exile.h
url = https://gitea.quitesimple.org/crtxcr/qssb.h.git url = https://gitea.quitesimple.org/crtxcr/exile.h.git

View File

@ -3,7 +3,7 @@ CPPSTD=c++20
CXXFLAGS=-std=$(CPPSTD) -O0 -g -no-pie -pipe -MMD -Wall -Wextra CXXFLAGS=-std=$(CPPSTD) -O0 -g -no-pie -pipe -MMD -Wall -Wextra
RELEASE_CXXFLAGS=-std=$(CPPSTD) -O3 -pipe -MMD -Wall -Wextra RELEASE_CXXFLAGS=-std=$(CPPSTD) -O3 -pipe -MMD -Wall -Wextra
LDFLAGS=-lsqlite3 -lpthread -lcrypto -lstdc++fs LDFLAGS=-lsqlite3 -lpthread -lcrypto -lstdc++fs
INCLUDEFLAGS=-I submodules/sqlitemoderncpp/hdr -I submodules/cpp-httplib -I submodules/qssb.h INCLUDEFLAGS=-I submodules/sqlitemoderncpp/hdr -I submodules/cpp-httplib -I submodules/exile.h
CXX=g++ CXX=g++
@ -14,6 +14,7 @@ SOURCES+=$(wildcard handlers/*.cpp)
SOURCES+=$(wildcard database/*.cpp) SOURCES+=$(wildcard database/*.cpp)
SOURCES+=$(wildcard cache/*.cpp) SOURCES+=$(wildcard cache/*.cpp)
SOURCES+=$(wildcard sandbox/*.cpp) SOURCES+=$(wildcard sandbox/*.cpp)
SOURCES+=$(wildcard dynamic/*.cpp)
HEADERS=$(wildcard *.h) HEADERS=$(wildcard *.h)
HEADERS+=$(wildcard gateway/*.h) HEADERS+=$(wildcard gateway/*.h)
@ -21,7 +22,7 @@ HEADERS+=$(wildcard handlers/*.h)
HEADERS+=$(wildcard database/*.h) HEADERS+=$(wildcard database/*.h)
HEADERS+=$(wildcard cache/*.h) HEADERS+=$(wildcard cache/*.h)
HEADERS+=$(wildcard sandbox/*.h) HEADERS+=$(wildcard sandbox/*.h)
HEADERS+=$(wildcard dynamic/*.h)
OBJECTS=$(patsubst %.cpp, %.o, $(SOURCES)) OBJECTS=$(patsubst %.cpp, %.o, $(SOURCES))
WIKIOBJECTS=$(filter-out test.o, $(OBJECTS)) WIKIOBJECTS=$(filter-out test.o, $(OBJECTS))
@ -48,8 +49,12 @@ profile: LDFLAGS+= -pg
release: qswiki release: qswiki
profile: qswiki profile: qswiki
qswiki: $(WIKIOBJECTS)
$(CXX) $(WIKIOBJECTS) ${LDFLAGS} ${INCLUDEFLAGS} -o qswiki exile.o: submodules/exile.h/exile.c
$(CC) -std=c99 -DHAVE_LANDLOCK=0 -c submodules/exile.h/exile.c -o exile.o
qswiki: $(WIKIOBJECTS) exile.o
$(CXX) $(WIKIOBJECTS) exile.o ${LDFLAGS} ${INCLUDEFLAGS} -o qswiki
test: $(TESTOBJECTS) test: $(TESTOBJECTS)
$(CXX) $(TESTOBJECTS) ${LDFLAGS} -o test $(CXX) $(TESTOBJECTS) ${LDFLAGS} -o test
@ -63,6 +68,6 @@ gtest: $(GTESTS_TESTDIR)/*.cpp $(GTEST_OBJECTS)
version.o:version.cpp version.o:version.cpp
$(CXX) ${CXXFLAGS} ${INCLUDEFLAGS} -DGITCOMMIT=\"$(shell git rev-parse --short HEAD)\" -c -o $@ $< $(CXX) ${CXXFLAGS} ${INCLUDEFLAGS} -DGITCOMMIT=\"$(shell git rev-parse --short HEAD)\" -c -o $@ $<
clean: clean:
rm -f $(OBJECTS) $(DEPENDS) rm -f exile.o $(OBJECTS) $(DEPENDS)

View File

@ -72,8 +72,7 @@ Building
Dependencies: Dependencies:
- cpp-httplib: https://github.com/yhirose/cpp-httplib - cpp-httplib: https://github.com/yhirose/cpp-httplib
- SqliteModernCpp: https://github.com/SqliteModernCpp - SqliteModernCpp: https://github.com/SqliteModernCpp
- qssb.h: https://gitea.quitesimple.org/crtxcr/qssb.h - exile.h: https://gitea.quitesimple.org/crtxcr/exile.h
- libseccomp: https://github.com/seccomp/libseccomp
- sqlite3: https://sqlite.org/index.html - sqlite3: https://sqlite.org/index.html
The first three are header-only libraries that are included as a git submodule. The others must The first three are header-only libraries that are included as a git submodule. The others must

2
cache/fscache.cpp vendored
View File

@ -46,7 +46,7 @@ void FsCache::removePrefix(std::string_view prefix)
// TODO: lock dir // TODO: lock dir
for(auto &entry : std::filesystem::directory_iterator(std::filesystem::path{this->path})) for(auto &entry : std::filesystem::directory_iterator(std::filesystem::path{this->path}))
{ {
if(std::string_view(entry.path().filename().c_str()).starts_with(prefix) == 0) if(std::string_view(entry.path().filename().c_str()).starts_with(prefix))
{ {
std::filesystem::remove_all(entry); std::filesystem::remove_all(entry);
} }

View File

@ -141,10 +141,9 @@ std::pair<bool, std::string> CLIHandler::page_list([[maybe_unused]] const std::v
QueryOption o; QueryOption o;
auto result = pageDao->getPageList(o); auto result = pageDao->getPageList(o);
std::stringstream stream; std::stringstream stream;
for(std::string pagename : result) for(Page &page : result)
{ {
Page p = pageDao->find(pagename).value(); stream << page.name << " " << page.pageid << " " << std::string(page.listed ? "listed" : "unlisted") << std::endl;
stream << p.name << " " << p.pageid << " " << std::string(p.listed ? "listed" : "unlisted") << std::endl;
} }
return {true, stream.str()}; return {true, stream.str()};
} }
@ -271,9 +270,9 @@ std::pair<bool, std::string> CLIHandler::category_show(const std::vector<std::st
auto categoryDao = this->db->createCategoryDao(); auto categoryDao = this->db->createCategoryDao();
auto members = categoryDao->fetchMembers(args.at(0), QueryOption{}); auto members = categoryDao->fetchMembers(args.at(0), QueryOption{});
std::stringstream stream; std::stringstream stream;
for(std::string &member : members) for(Page &member : members)
{ {
stream << member << std::endl; stream << member.name << std::endl;
} }
return {true, stream.str()}; return {true, stream.str()};
} }

View File

@ -24,6 +24,7 @@ SOFTWARE.
#include "config.h" #include "config.h"
#include "permissions.h" #include "permissions.h"
#include "varreplacer.h" #include "varreplacer.h"
std::string Config::required(const std::string &key) std::string Config::required(const std::string &key)
{ {
auto it = this->configmap.find(key); auto it = this->configmap.find(key);
@ -77,13 +78,16 @@ Config::Config(const std::map<std::string, std::string> &map)
this->templatepath = required("templatepath"); this->templatepath = required("templatepath");
this->urls.linkallcats = required("linkallcats"); this->urls.linkallcats = required("linkallcats");
this->urls.linkallpages = required("linkallpages"); this->urls.linkallpages = required("linkallpages");
this->urls.linkallpagesrendertype = required ("linkallpagesrendertype");
this->urls.linkcategory = required("linkcategory"); this->urls.linkcategory = required("linkcategory");
this->urls.linkcategoryrendertype = required("linkcategoryrendertype");
this->urls.linkdelete = required("linkdelete"); this->urls.linkdelete = required("linkdelete");
this->urls.linkedit = required("linkedit"); this->urls.linkedit = required("linkedit");
this->urls.linkhistory = required("linkhistory"); this->urls.linkhistory = required("linkhistory");
this->urls.linkindex = required("linkindex"); this->urls.linkindex = required("linkindex");
this->urls.linklogout = required("linklogout"); this->urls.linklogout = required("linklogout");
this->urls.linkpage = required("linkpage"); this->urls.linkpage = required("linkpage");
this->urls.linkpagebytitle = required("linkpagebytitle");
this->urls.linkrecent = required("linkrecent"); this->urls.linkrecent = required("linkrecent");
this->urls.linkrevision = required("linkrevision"); this->urls.linkrevision = required("linkrevision");
this->urls.linksettings = required("linksettings"); this->urls.linksettings = required("linksettings");
@ -96,6 +100,8 @@ Config::Config(const std::map<std::string, std::string> &map)
this->urls.deletionurl = required("deletionurl"); this->urls.deletionurl = required("deletionurl");
this->urls.adminregisterurl = required("adminregisterurl"); this->urls.adminregisterurl = required("adminregisterurl");
this->urls.usersettingsurl = required("usersettingsurl"); this->urls.usersettingsurl = required("usersettingsurl");
this->urls.rooturl = required("rooturl");
this->urls.atomurl = required("atomurl");
this->connectionstring = required("connectionstring"); this->connectionstring = required("connectionstring");
this->handlersConfig.max_pagename_length = optional("max_pagename_length", 256); this->handlersConfig.max_pagename_length = optional("max_pagename_length", 256);

View File

@ -23,9 +23,11 @@ struct ConfigUrls
std::string linkindex; std::string linkindex;
std::string linkrecent; std::string linkrecent;
std::string linkallpages; std::string linkallpages;
std::string linkallpagesrendertype;
std::string linkallcats; std::string linkallcats;
std::string linkshere; std::string linkshere;
std::string linkpage; std::string linkpage;
std::string linkpagebytitle;
std::string linkrevision; std::string linkrevision;
std::string linkhistory; std::string linkhistory;
std::string linkedit; std::string linkedit;
@ -33,6 +35,7 @@ struct ConfigUrls
std::string linkdelete; std::string linkdelete;
std::string linklogout; std::string linklogout;
std::string linkcategory; std::string linkcategory;
std::string linkcategoryrendertype;
std::string loginurl; std::string loginurl;
std::string linkrecentsort; std::string linkrecentsort;
std::string actionurl; std::string actionurl;
@ -41,6 +44,8 @@ struct ConfigUrls
std::string linkhistorysort; std::string linkhistorysort;
std::string adminregisterurl; std::string adminregisterurl;
std::string usersettingsurl; std::string usersettingsurl;
std::string rooturl;
std::string atomurl;
}; };
class ConfigVariableResolver class ConfigVariableResolver

View File

@ -5,7 +5,7 @@
#include <optional> #include <optional>
#include "queryoption.h" #include "queryoption.h"
#include "../category.h" #include "../category.h"
#include "../page.h"
class CategoryDao class CategoryDao
{ {
public: public:
@ -14,7 +14,7 @@ class CategoryDao
virtual std::vector<std::string> fetchList(QueryOption o) = 0; virtual std::vector<std::string> fetchList(QueryOption o) = 0;
virtual std::optional<Category> find(std::string name) = 0; virtual std::optional<Category> find(std::string name) = 0;
virtual void deleteCategory(std::string name) = 0; virtual void deleteCategory(std::string name) = 0;
virtual std::vector<std::string> fetchMembers(std::string name, QueryOption o) = 0; virtual std::vector<Page> fetchMembers(std::string name, QueryOption o) = 0;
}; };
#endif // CATEGORYDAO_H #endif // CATEGORYDAO_H

View File

@ -94,9 +94,10 @@ std::vector<std::string> CategoryDaoSqlite::fetchList(QueryOption o)
} }
return result; return result;
} }
std::vector<std::string> CategoryDaoSqlite::fetchMembers(std::string name, QueryOption o)
std::vector<Page> CategoryDaoSqlite::fetchMembers(std::string name, QueryOption o)
{ {
std::vector<std::string> result; std::vector<Page> result;
SqliteQueryOption queryOption{o}; SqliteQueryOption queryOption{o};
std::string queryoptions = std::string queryoptions =
@ -104,11 +105,18 @@ std::vector<std::string> CategoryDaoSqlite::fetchMembers(std::string name, Query
try try
{ {
auto query = *db << "SELECT page.name AS name FROM categorymember INNER JOIN page ON page.id = " auto query = *db << "SELECT page.id, page.name AS name, page.title, page.lastrevision, page.visible FROM categorymember INNER JOIN page ON page.id = "
"categorymember.page WHERE category = (SELECT id FROM category WHERE name = ? ) AND " + "categorymember.page WHERE category = (SELECT id FROM category WHERE name = ? ) AND " +
queryoptions queryoptions
<< name; << name;
query >> [&](std::string p) { result.push_back(p); }; query >> [&](unsigned int id, std::string name, std::string title, unsigned int lastrevision, bool visible) {
Page p;
p.name = name;
p.pageid = id;
p.title = title;
p.current_revision = lastrevision;
p.listed = visible;
result.push_back(p); };
} }
catch(const sqlite::exceptions::no_rows &e) catch(const sqlite::exceptions::no_rows &e)
{ {

View File

@ -3,12 +3,13 @@
#include "categorydao.h" #include "categorydao.h"
#include "sqlitedao.h" #include "sqlitedao.h"
#include "../page.h"
class CategoryDaoSqlite : public CategoryDao, protected SqliteDao class CategoryDaoSqlite : public CategoryDao, protected SqliteDao
{ {
public: public:
CategoryDaoSqlite(); CategoryDaoSqlite();
std::vector<std::string> fetchList(QueryOption o) override; std::vector<std::string> fetchList(QueryOption o) override;
std::vector<std::string> fetchMembers(std::string name, QueryOption o) override; std::vector<Page> fetchMembers(std::string name, QueryOption o) override;
void save(const Category &c) override; void save(const Category &c) override;
void deleteCategory(std::string name) override; void deleteCategory(std::string name) override;
std::optional<Category> find(std::string name) override; std::optional<Category> find(std::string name) override;

View File

@ -13,8 +13,9 @@ class PageDao
virtual bool exists(std::string page) const = 0; virtual bool exists(std::string page) const = 0;
virtual bool exists(unsigned int id) const = 0; virtual bool exists(unsigned int id) const = 0;
virtual std::optional<Page> find(std::string name) = 0; virtual std::optional<Page> find(std::string name) = 0;
virtual std::optional<Page> findByTitle(std::string title) = 0;
virtual std::optional<Page> find(unsigned int id) = 0; virtual std::optional<Page> find(unsigned int id) = 0;
virtual std::vector<std::string> getPageList(QueryOption option) = 0; virtual std::vector<Page> getPageList(QueryOption option) = 0;
virtual std::vector<std::string> fetchCategories(std::string pagename, QueryOption option) = 0; virtual std::vector<std::string> fetchCategories(std::string pagename, QueryOption option) = 0;
virtual void deletePage(std::string page) = 0; virtual void deletePage(std::string page) = 0;
virtual void save(const Page &page) = 0; virtual void save(const Page &page) = 0;

View File

@ -52,15 +52,35 @@ std::optional<Page> PageDaoSqlite::find(std::string name)
} }
} }
std::optional<Page> PageDaoSqlite::findByTitle(std::string title)
{
Page result;
try
{
auto ps = *db << "SELECT id, name, title, lastrevision, visible FROM page WHERE title = ?";
ps << title >> std::tie(result.pageid, result.name, result.title, result.current_revision, result.listed);
}
catch(const sqlite::errors::no_rows &e)
{
return {};
}
catch(sqlite::sqlite_exception &e)
{
throwFrom(e);
}
return result;
}
std::optional<Page> PageDaoSqlite::find(unsigned int id) std::optional<Page> PageDaoSqlite::find(unsigned int id)
{ {
Page result; Page result;
result.pageid = id; result.pageid = id;
try try
{ {
auto ps = *db << "SELECT name, lastrevision, visible FROM page WHERE id = ?"; auto ps = *db << "SELECT name, title, lastrevision, visible FROM page WHERE id = ?";
ps << id >> std::tie(result.name, result.current_revision, result.listed); ps << id >> std::tie(result.name, result.title, result.current_revision, result.listed);
} }
catch(const sqlite::errors::no_rows &e) catch(const sqlite::errors::no_rows &e)
{ {
@ -97,30 +117,38 @@ void PageDaoSqlite::save(const Page &page)
{ {
try try
{ {
*db << "INSERT OR REPLACE INTO page (id, name, lastrevision, visible) VALUES((SELECT id FROM page WHERE name = " *db << "INSERT OR REPLACE INTO page (id, name, title, lastrevision, visible) VALUES((SELECT id FROM page WHERE "
"? OR id = ?), ?, ?, ?)" "name = "
<< page.name << page.pageid << page.name << page.current_revision << page.listed; "? OR id = ?), ?, ?, ?, ?)"
<< page.name << page.pageid << page.name << page.title << page.current_revision << page.listed;
} }
catch(sqlite::sqlite_exception &e) catch(sqlite::sqlite_exception &e)
{ {
throwFrom(e); throwFrom(e);
} }
} }
std::vector<std::string> PageDaoSqlite::getPageList(QueryOption option)
std::vector<Page> PageDaoSqlite::getPageList(QueryOption option)
{ {
std::vector<std::string> result; std::vector<Page> result;
try try
{ {
std::string queryOption = SqliteQueryOption(option) std::string queryOption = SqliteQueryOption(option)
.setOrderByColumn("lower(name)") .setOrderByColumn("lower(name)")
.setVisibleColumnName("visible") .setVisibleColumnName("visible")
.setPrependWhere(true) .setPrependWhere(true)
.build(); .build();
std::string query = "SELECT name FROM page " + queryOption; std::string query = "SELECT id, name, title, lastrevision, visible FROM page " + queryOption;
*db << query >> [&](unsigned int pageid, std::string name, std::string title,unsigned int current_revision, bool visible ) {
*db << query >> [&](std::string name) { result.push_back(name); }; Page tmp;
tmp.pageid = pageid;
tmp.name = name;
tmp.title = title;
tmp.current_revision = current_revision;
tmp.listed = visible;
result.push_back(tmp); };
} }
catch(const sqlite::errors::no_rows &e) catch(const sqlite::errors::no_rows &e)
{ {
@ -183,7 +211,8 @@ std::vector<SearchResult> PageDaoSqlite::search(std::string name, QueryOption op
auto query = auto query =
*db << "SELECT page.name FROM search INNER JOIN page ON search.page = page.id WHERE search MATCH ? " *db << "SELECT page.name FROM search INNER JOIN page ON search.page = page.id WHERE search MATCH ? "
<< ftsEscape(name); << ftsEscape(name);
query >> [&](std::string pagename) { query >> [&](std::string pagename)
{
SearchResult sresult; SearchResult sresult;
sresult.pagename = pagename; sresult.pagename = pagename;
sresult.query = name; sresult.query = name;

View File

@ -20,8 +20,9 @@ class PageDaoSqlite : public PageDao, protected SqliteDao
bool exists(std::string name) const override; bool exists(std::string name) const override;
void save(const Page &page) override; void save(const Page &page) override;
std::optional<Page> find(std::string name) override; std::optional<Page> find(std::string name) override;
std::optional<Page> findByTitle(std::string title) override;
std::optional<Page> find(unsigned int id) override; std::optional<Page> find(unsigned int id) override;
std::vector<std::string> getPageList(QueryOption option) override; std::vector<Page> getPageList(QueryOption option) override;
std::vector<std::string> fetchCategories(std::string pagename, QueryOption option) override; std::vector<std::string> fetchCategories(std::string pagename, QueryOption option) override;
using SqliteDao::SqliteDao; using SqliteDao::SqliteDao;
int fetchPageId(std::string pagename); int fetchPageId(std::string pagename);

View File

@ -0,0 +1,8 @@
#include "dynamiccontent.h"
DynamicContent::DynamicContent(Template &templ, Database &database, UrlProvider &provider)
{
this->templ = &templ;
this->database = &database;
this->urlProvider = &provider;
}

28
dynamic/dynamiccontent.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef DYNAMICCONTENT_H
#define DYNAMICCONTENT_H
#include <string>
#include "../database/database.h"
#include "../template.h"
#include "../urlprovider.h"
class DynamicContent
{
protected:
Template *templ;
Database *database;
UrlProvider *urlProvider;
std::string argument;
public:
DynamicContent(Template &templ, Database &database, UrlProvider &urlProvider);
virtual std::string render() = 0;
virtual void setArgument(std::string argument)
{
this->argument = argument;
}
virtual ~DynamicContent()
{
}
};
#endif // DYNAMICCONTENT_H

View File

@ -0,0 +1,11 @@
#include "dynamiccontentgetvar.h"
std::string DynamicContentGetVar::render()
{
return (*this->map)[this->argument];
}
void DynamicContentGetVar::setMap(std::map<std::string, std::string> &map)
{
this->map = &map;
}

View File

@ -0,0 +1,19 @@
#ifndef DYNAMICCONTENTGETVAR_H
#define DYNAMICCONTENTGETVAR_H
#include "dynamiccontent.h"
class DynamicContentGetVar : public DynamicContent
{
private:
std::map<std::string, std::string> *map;
public:
using DynamicContent::DynamicContent;
// DynamicContent interface
public:
std::string render();
void setMap(std::map<std::string, std::string> &map);
};
#endif // DYNAMICCONTENTGETVAR_H

View File

@ -0,0 +1,12 @@
#include "dynamiccontentincludepage.h"
#include "../parser.h"
std::string DynamicContentIncludePage::render()
{
auto revisionDao = this->database->createRevisionDao();
auto rev = revisionDao->getCurrentForPage(this->argument);
if(rev)
{
return rev->content;
}
return {};
}

View File

@ -0,0 +1,12 @@
#ifndef DYNAMICCONTENTINCLUDEPAGE_H
#define DYNAMICCONTENTINCLUDEPAGE_H
#include "dynamiccontent.h"
class DynamicContentIncludePage : public DynamicContent
{
public:
using DynamicContent::DynamicContent;
std::string render();
};
#endif // DYNAMICCONTENTINCLUDEPAGE_H

View File

@ -0,0 +1,39 @@
#include <chrono>
#include "dynamiccontentpostlist.h"
std::string DynamicContentPostList::render()
{
auto categoryDao = this->database->createCategoryDao();
auto pageDao = this->database->createPageDao();
auto revisionDao = this->database->createRevisionDao();
QueryOption option;
option.includeInvisible = false;
auto members = categoryDao->fetchMembers(this->argument, option);
std::vector<std::pair<std::string, time_t>> pageList;
for(const Page &member : members)
{
auto revision = revisionDao->getRevisionForPage(member.name, 1);
pageList.push_back({member.name, revision->timestamp});
}
std::sort(pageList.begin(), pageList.end(),
[](std::pair<std::string, time_t> &a, std::pair<std::string, time_t> &b) { return a.second > b.second; });
std::string postListBegin = this->templ->loadResolvedPart("dynamic/postlistbegin");
std::string postListEnd = this->templ->loadResolvedPart("dynamic/postlistend");
std::string postLink = this->templ->loadResolvedPart("dynamic/postlistlink");
std::stringstream stream;
stream << postListBegin;
for(auto &pair : pageList)
{
std::string link = this->urlProvider->page(pair.first);
std::string date = utils::toISODate(pair.second);
Varreplacer replacer{"{"};
replacer.addKeyValue("url", link);
replacer.addKeyValue("date", date);
replacer.addKeyValue("title", pageDao->find(pair.first)->title);
stream << replacer.parse(postLink);
}
stream << postListEnd;
return stream.str();
}

View File

@ -0,0 +1,12 @@
#ifndef DYNAMICCONTENTPOSTLIST_H
#define DYNAMICCONTENTPOSTLIST_H
#include "dynamiccontent.h"
class DynamicContentPostList : public DynamicContent
{
public:
using DynamicContent::DynamicContent;
std::string render() override;
};
#endif // DYNAMICCONTENTPOSTLIST_H

View File

@ -0,0 +1,21 @@
#include "dynamiccontentsetvar.h"
std::string DynamicContentSetVar::render()
{
auto result = utils::split(this->argument, '=');
if(result.size() == 2)
{
this->map->emplace(std::make_pair(result[0], result[1]));
}
return {};
}
void DynamicContentSetVar::setArgument(std::string argument)
{
this->argument = argument;
}
void DynamicContentSetVar::setMap(std::map<std::string, std::string> &map)
{
this->map = &map;
}

View File

@ -0,0 +1,17 @@
#ifndef DYNAMCCONTENTPUSHVAR_H
#define DYNAMCCONTENTPUSHVAR_H
#include "dynamiccontent.h"
class DynamicContentSetVar : public DynamicContent
{
private:
std::map<std::string, std::string> *map;
public:
using DynamicContent::DynamicContent;
std::string render();
void setArgument(std::string argument);
void setMap(std::map<std::string, std::string> &map);
};
#endif // DYNAMCCONTENTPUSHVAR_H

0
grouper.cpp Normal file
View File

26
grouper.h Normal file
View File

@ -0,0 +1,26 @@
#include "utils.h"
template<class G, class V, class C>
class Grouper
{
std::map<G, std::vector<const V*>, C> results;
public:
Grouper(C c)
{
results = std::map<G, std::vector<const V*>, C>(c);
}
void group(const std::function<G(const V&)> &map, const std::vector<V> &values)
{
for(const V &v : values)
{
results[map(v)].push_back(&v);
}
}
std::map<G, std::vector<const V*>, C> &getResults()
{
return this->results;
}
};

View File

@ -1,5 +1,6 @@
#ifndef HANDLER_H #ifndef HANDLER_H
#define HANDLER_H #define HANDLER_H
#include <memory>
#include "../config.h" #include "../config.h"
#include "../response.h" #include "../response.h"
#include "../request.h" #include "../request.h"
@ -9,6 +10,8 @@
#include "../database/queryoption.h" #include "../database/queryoption.h"
#include "../logger.h" #include "../logger.h"
#include "../cache/icache.h" #include "../cache/icache.h"
#include "../dynamic/dynamiccontent.h"
class Handler class Handler
{ {
protected: protected:
@ -53,6 +56,12 @@ class Handler
virtual ~Handler() virtual ~Handler()
{ {
} }
template <class T> inline std::shared_ptr<T> createDynamic()
{
return std::make_shared<T>(*this->templ, *this->database, *this->urlProvider);
}
Response errorResponse(std::string errortitle, std::string errormessage, int status = 200); Response errorResponse(std::string errortitle, std::string errormessage, int status = 200);
std::string createPageTitle(std::string append); std::string createPageTitle(std::string append);
}; };

View File

@ -19,6 +19,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
#include "handlerallpages.h" #include "handlerallpages.h"
#include "../grouper.h"
#include "../pagelistrenderer.h"
Response HandlerAllPages::handleRequest(const Request &r) Response HandlerAllPages::handleRequest(const Request &r)
{ {
@ -27,17 +29,21 @@ Response HandlerAllPages::handleRequest(const Request &r)
Response response; Response response;
auto pageDao = this->database->createPageDao(); auto pageDao = this->database->createPageDao();
QueryOption qo = queryOption(r); QueryOption qo = queryOption(r);
auto resultList = pageDao->getPageList(qo); std::vector<Page> pageList = pageDao->getPageList(qo);
if(resultList.size() == 0) if(pageList.size() == 0)
{ {
return errorResponse("No pages", "This wiki does not have any pages yet"); 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); PageListRenderer pagelistrender(*this->templ, *this->urlProvider, *this->database);
searchPage.setVar("pagelist", body); TemplatePage allPages = this->templ->getPage("allpages");
searchPage.setVar("title", createPageTitle("All pages")); allPages.setVar("pagelistcontent", pagelistrender.render(pageList, r.get("rendertype")));
setGeneralVars(searchPage); allPages.setVar("pagelistletterlink", this->urlProvider->allPages(PageListRenderer::RENDER_GROUP_BY_LETTER));
response.setBody(searchPage.render()); allPages.setVar("pagelistcreationdatelink", this->urlProvider->allPages(PageListRenderer::RENDER_GROUP_BY_CREATIONDATE));
allPages.setVar("title", createPageTitle("All pages"));
setGeneralVars(allPages);
response.setBody(allPages.render());
response.setStatus(200); response.setStatus(200);
return response; return response;
} }

View File

@ -19,6 +19,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
#include "handlercategory.h" #include "handlercategory.h"
#include "../pagelistrenderer.h"
Response HandlerCategory::handleRequest(const Request &r) Response HandlerCategory::handleRequest(const Request &r)
{ {
@ -34,8 +35,12 @@ Response HandlerCategory::handleRequest(const Request &r)
QueryOption qo = queryOption(r); QueryOption qo = queryOption(r);
auto resultList = categoryDao->fetchMembers(categoryname, qo); auto resultList = categoryDao->fetchMembers(categoryname, qo);
TemplatePage searchPage = this->templ->getPage("show_category"); TemplatePage searchPage = this->templ->getPage("show_category");
std::string body = this->templ->renderSearch(resultList); PageListRenderer pagelistrender(*this->templ, *this->urlProvider, *this->database);
searchPage.setVar("pagelist", body);
std::string body = pagelistrender.render(resultList, r.get("rendertype"));
searchPage.setVar("pagelistcontent", body);
searchPage.setVar("pagelistletterlink", this->urlProvider->category(categoryname, PageListRenderer::RENDER_GROUP_BY_LETTER));
searchPage.setVar("pagelistcreationdatelink", this->urlProvider->category(categoryname, PageListRenderer::RENDER_GROUP_BY_CREATIONDATE));
searchPage.setVar("categoryname", categoryname); searchPage.setVar("categoryname", categoryname);
setGeneralVars(searchPage); setGeneralVars(searchPage);
searchPage.setVar("title", createPageTitle("Category: " + categoryname)); searchPage.setVar("title", createPageTitle("Category: " + categoryname));

View File

@ -34,6 +34,7 @@ SOFTWARE.
#include "handlerpagedelete.h" #include "handlerpagedelete.h"
#include "handlerusersettings.h" #include "handlerusersettings.h"
#include "handlerversion.h" #include "handlerversion.h"
#include "handlerfeedgenerator.h"
std::unique_ptr<Handler> HandlerFactory::createHandler(const std::string &action, Session &userSession) std::unique_ptr<Handler> HandlerFactory::createHandler(const std::string &action, Session &userSession)
{ {
if(action == "" || action == "index") if(action == "" || action == "index")
@ -84,6 +85,10 @@ std::unique_ptr<Handler> HandlerFactory::createHandler(const std::string &action
{ {
return produce<HandlerVersion>(userSession); return produce<HandlerVersion>(userSession);
} }
if(action == "feed")
{
return produce<HandlerFeedGenerator>(userSession);
}
return produce<HandlerInvalidAction>(userSession); return produce<HandlerInvalidAction>(userSession);
} }

View File

@ -0,0 +1,148 @@
#include "handlerfeedgenerator.h"
#include "../parser.h"
std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetchEntries(
std::vector<std::string> categories)
{
auto revisionDao = this->database->createRevisionDao();
auto pageDao = this->database->createPageDao();
std::vector<EntryRevisionPair> result;
QueryOption option;
option.includeInvisible = false;
// option.limit = 20;
auto comppage = [](const Page &a, const Page &b) { return a.name < b.name; };
std::set<Page, decltype(comppage)> members (comppage);
if(categories.empty())
{
auto pages = pageDao->getPageList(option);
std::copy(pages.begin(), pages.end(), std::inserter(members, members.end()));
}
else
{
auto categoryDao = this->database->createCategoryDao();
for(std::string cat : categories)
{
if(!categoryDao->find(cat))
{
throw std::runtime_error("No such category");
}
auto catmembers = categoryDao->fetchMembers(cat, option);
std::copy(catmembers.begin(), catmembers.end(), std::inserter(members, members.end()));
}
}
for(const Page &member : members)
{
auto revision = revisionDao->getRevisionForPage(member.name, 1).value();
result.push_back({member, revision});
}
std::sort(result.begin(), result.end(),
[](EntryRevisionPair &a, EntryRevisionPair &b) { return a.second.timestamp > b.second.timestamp; });
const int maxResults = 20;
if(result.size() > maxResults)
{
result.erase(result.begin() + maxResults - 1, result.end());
}
return result;
}
std::string HandlerFeedGenerator::generateAtom(const std::vector<HandlerFeedGenerator::EntryRevisionPair> &entries,
std::string filter)
{
std::stringstream stream;
// don't care about offset for now especially since "%z" does not return what we need exactly (with ':')
const std::string dateformat = "%Y-%m-%dT%T";
time_t newestPublished = 0;
std::string atomfooter = this->templ->loadResolvedPart("feeds/atomfooter");
auto revisionDao = this->database->createRevisionDao();
auto pageDao = this->database->createPageDao();
std::string subtitle = filter;
if(utils::trim(filter).empty())
{
subtitle = "All pages";
}
for(const EntryRevisionPair &entry : entries)
{
const Page &page = entry.first;
const Revision &initialRevision = entry.second;
Revision current = revisionDao->getCurrentForPage(page.name).value();
std::string url = this->urlProvider->rootUrl() + this->urlProvider->page(page.name);
if(initialRevision.timestamp > newestPublished)
{
newestPublished = initialRevision.timestamp;
}
std::string entryPublished = utils::formatLocalDate(initialRevision.timestamp, dateformat) + "Z";
std::string entryUpdated = utils::formatLocalDate(current.timestamp, dateformat) + "Z";
std::string entryurl =
this->urlProvider->combine({this->urlProvider->rootUrl(), this->urlProvider->page(page.name)});
TemplatePage atomentry = this->templ->getPage("feeds/atomentry");
atomentry.setVar("entrytitle", utils::html_xss(page.title));
atomentry.setVar("entryurl", utils::html_xss(entryurl));
atomentry.setVar("entryid", utils::html_xss(entryurl));
atomentry.setVar("entrypublished", entryPublished);
atomentry.setVar("entryupdated", entryUpdated);
Parser parser;
atomentry.setVar("entrycontent", utils::html_xss(parser.parse(*pageDao, *this->urlProvider, current.content)));
stream << atomentry.render();
}
stream << atomfooter;
TemplatePage atomheader = this->templ->getPage("feeds/atomheader");
atomheader.setVar("subtitle", subtitle);
std::string selflink = utils::html_xss(this->urlProvider->atomFeed(filter));
atomheader.setVar("atomfeeduniqueid", selflink);
atomheader.setVar("atomselflink", selflink);
atomheader.setVar("atomfeedupdate", utils::formatLocalDate(newestPublished, dateformat) + "Z");
return atomheader.render() + stream.str();
}
Response HandlerFeedGenerator::handleRequest(const Request &r)
{
Response response;
try
{
std::string type = r.get("type");
std::string categories = r.get("cats");
if(type == "atom")
{
std::string filter = categories;
Response result;
result.setStatus(200);
result.setContentType("application/atom+xml");
std::string cacheKey = "feed:atom:" + filter;
auto cached = this->cache->get(cacheKey);
if(cached)
{
result.setBody(cached.value());
}
else
{
auto entries = fetchEntries(utils::split(categories, ','));
std::string feed = generateAtom(entries, filter);
result.setBody(feed);
this->cache->put(cacheKey, feed);
}
response = result;
}
else
{
return errorResponse("Invalid feed type", "Unknown feed type, try atom", 400);
}
}
catch(std::runtime_error &e)
{
Logger::error() << "Error while serving feed: " << e.what();
return errorResponse("Error", "An error occured while trying to serve the feed", 500);
}
return response;
}
bool HandlerFeedGenerator::canAccess(const Permissions &perms)
{
return true;
}

View File

@ -0,0 +1,21 @@
#ifndef HANDLERFEEDGENERATOR_H
#define HANDLERFEEDGENERATOR_H
#include "handler.h"
#include "../page.h"
#include "../revision.h"
class HandlerFeedGenerator : public Handler
{
typedef std::pair<Page, Revision> EntryRevisionPair;
protected:
std::vector<EntryRevisionPair> fetchEntries(std::vector<std::string> categories);
std::string generateAtom(const std::vector<EntryRevisionPair> &entries, std::string atomtitle);
Response generateRss(const std::vector<EntryRevisionPair> &entries);
public:
using Handler::Handler;
Response handleRequest(const Request &r) override;
bool canAccess(const Permissions &perms) override;
};
#endif // HANDLERFEEDGENERATOR_H

View File

@ -26,9 +26,20 @@ Response HandlerPage::handle(const Request &r)
std::string pagename = r.get("page"); std::string pagename = r.get("page");
auto pageDao = this->database->createPageDao(); auto pageDao = this->database->createPageDao();
if(pagename.empty()) if(pagename.empty())
{
std::string title = r.get("title");
if(title.empty())
{ {
return errorResponse("No page given", "No page given to request"); return errorResponse("No page given", "No page given to request");
} }
title = utils::strreplace(title, "-", " ");
auto page = pageDao->findByTitle(title);
if(!page)
{
return errorResponse("No page by such title", "No page with such title exists");
}
pagename = page->name;
}
if(pageMustExist() && !pageDao->exists(pagename)) if(pageMustExist() && !pageDao->exists(pagename))
{ {

View File

@ -45,6 +45,7 @@ Response HandlerPageDelete::handleRequest(PageDao &pageDao, std::string pagename
{ {
pageDao.deletePage(pagename); pageDao.deletePage(pagename);
this->cache->removePrefix("page:"); // TODO: overkill? this->cache->removePrefix("page:"); // TODO: overkill?
this->cache->removePrefix("feed:");
return Response::redirectTemporarily(this->urlProvider->index()); return Response::redirectTemporarily(this->urlProvider->index());
} }
TemplatePage delPage = this->templ->getPage("page_deletion"); TemplatePage delPage = this->templ->getPage("page_deletion");

View File

@ -66,6 +66,7 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
this->database->beginTransaction(); this->database->beginTransaction();
std::string visiblecmd = parser.extractCommand("visible", newContent); std::string visiblecmd = parser.extractCommand("visible", newContent);
std::string rename = parser.extractCommand("rename", newContent); std::string rename = parser.extractCommand("rename", newContent);
std::string customtitle = parser.extractCommand("pagetitle", newContent);
Page page; Page page;
std::optional<Page> currentPage = pageDao.find(pagename); std::optional<Page> currentPage = pageDao.find(pagename);
if(currentPage) if(currentPage)
@ -83,6 +84,11 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
page.current_revision = current_revision; page.current_revision = current_revision;
page.listed = !(visiblecmd == "0"); page.listed = !(visiblecmd == "0");
page.name = pagename; page.name = pagename;
page.title = customtitle;
if(page.title.empty())
{
page.title = page.name;
}
pageDao.save(page); pageDao.save(page);
Revision newRevision; Revision newRevision;
@ -95,6 +101,7 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
pageDao.setCategories(pagename, cats); pageDao.setCategories(pagename, cats);
this->database->commitTransaction(); this->database->commitTransaction();
this->cache->removePrefix("page:"); // TODO: overkill? this->cache->removePrefix("page:"); // TODO: overkill?
this->cache->removePrefix("feed:");
} }
catch(const DatabaseException &e) catch(const DatabaseException &e)
{ {

View File

@ -23,6 +23,10 @@ SOFTWARE.
#include "../logger.h" #include "../logger.h"
#include "../parser.h" #include "../parser.h"
#include "../htmllink.h" #include "../htmllink.h"
#include "../dynamic/dynamiccontentpostlist.h"
#include "../dynamic/dynamiccontentincludepage.h"
#include "../dynamic/dynamiccontentsetvar.h"
#include "../dynamic/dynamiccontentgetvar.h"
bool HandlerPageView::canAccess(std::string page) bool HandlerPageView::canAccess(std::string page)
{ {
@ -136,10 +140,46 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
std::string indexcontent; std::string indexcontent;
std::string parsedcontent; std::string parsedcontent;
std::map<std::string, std::string> dynamicVarsMap;
std::function<std::string(std::string_view, std::string_view)> dynamicParseCallback =
[&](std::string_view key, std::string_view value) -> std::string
{
if(key == "dynamic:postlist")
{
std::shared_ptr<DynamicContentPostList> postlist = createDynamic<DynamicContentPostList>();
postlist->setArgument(std::string(value));
return postlist->render();
}
if(key == "dynamic:includepage")
{
if((effectivePermissions(std::string(value)).canRead()))
{
std::shared_ptr<DynamicContentIncludePage> includePage = createDynamic<DynamicContentIncludePage>();
includePage->setArgument(std::string(value));
return parser.parseDynamics(includePage->render(), dynamicParseCallback);
}
}
if(key == "dynamic:setvar")
{
std::shared_ptr<DynamicContentSetVar> setVar = createDynamic<DynamicContentSetVar>();
setVar->setMap(dynamicVarsMap);
setVar->setArgument(std::string(value));
return setVar->render();
}
if(key == "dynamic:getvar")
{
std::shared_ptr<DynamicContentGetVar> getVar = createDynamic<DynamicContentGetVar>();
getVar->setMap(dynamicVarsMap);
getVar->setArgument(std::string(value));
return getVar->render();
}
return std::string{};
};
std::string resolvedContent = parser.parseDynamics(revision->content, dynamicParseCallback);
if(revisionid > 0) if(revisionid > 0)
{ {
indexcontent = createIndexContent(parser, revision->content); indexcontent = createIndexContent(parser, resolvedContent);
parsedcontent = parser.parse(pageDao, *this->urlProvider, revision->content); parsedcontent = parser.parse(pageDao, *this->urlProvider, resolvedContent);
} }
else else
{ {
@ -153,7 +193,7 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
} }
else else
{ {
indexcontent = createIndexContent(parser, revision->content); indexcontent = createIndexContent(parser, resolvedContent);
this->cache->put(cachekeyindexcontent, indexcontent); this->cache->put(cachekeyindexcontent, indexcontent);
} }
if(cachedparsedcontent) if(cachedparsedcontent)
@ -162,18 +202,23 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
} }
else else
{ {
parsedcontent = parser.parse(pageDao, *this->urlProvider, revision->content); parsedcontent = parser.parse(pageDao, *this->urlProvider, resolvedContent);
this->cache->put(cachekeyparsedcontent, parsedcontent); this->cache->put(cachekeyparsedcontent, parsedcontent);
} }
} }
std::string revisionstr = std::to_string(revision->revision); std::string revisionstr = std::to_string(revision->revision);
std::string customtitle = parser.extractCommand("pagetitle", revision->content);
page.setVar("content", parsedcontent); page.setVar("content", parsedcontent);
page.setVar("index", indexcontent); page.setVar("index", indexcontent);
page.setVar("editedby", revision->author); page.setVar("editedby", revision->author);
page.setVar("editedon", utils::toISODate(revision->timestamp)); page.setVar("editedon", utils::toISODateTime(revision->timestamp));
page.setVar("historyurl", this->urlProvider->pageHistory(pagename)); page.setVar("historyurl", this->urlProvider->pageHistory(pagename));
page.setVar("revision", revisionstr); page.setVar("revision", revisionstr);
setPageVars(page, pagename); setPageVars(page, pagename);
if(!customtitle.empty())
{
page.setVar("title", createPageTitle(customtitle));
}
std::string body = page.render(); std::string body = page.render();
if(revisionid == 0 && !this->userSession->loggedIn) if(revisionid == 0 && !this->userSession->loggedIn)
{ {

View File

@ -25,7 +25,11 @@ Response HandlerSearch::handleRequest(const Request &r)
std::string q = r.get("q"); std::string q = r.get("q");
if(q.empty()) if(q.empty())
{ {
return errorResponse("Missing search term", "No search term supplied"); TemplatePage searchForm = this->templ->getPage("searchform");
response.setBody(searchForm.render());
response.setStatus(200);
setGeneralVars(searchForm);
return response;
} }
auto pageDao = this->database->createPageDao(); auto pageDao = this->database->createPageDao();

View File

@ -2,16 +2,32 @@
#define IPARSER_H #define IPARSER_H
#include <vector> #include <vector>
#include <string_view> #include <string_view>
#include <functional>
#include "headline.h" #include "headline.h"
#include "database/pagedao.h" #include "database/pagedao.h"
#include "urlprovider.h" #include "urlprovider.h"
class IParser class IParser
{ {
protected:
static std::string empty(std::string_view key, std::string_view content)
{
return "";
}
public: public:
virtual std::string extractCommand(std::string cmdname, std::string content) const = 0; virtual std::string extractCommand(std::string cmdname, const std::string &content) const = 0;
virtual std::vector<Headline> extractHeadlines(std::string content) const = 0; virtual std::vector<Headline> extractHeadlines(const std::string &content) const = 0;
virtual std::string parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const = 0; virtual inline std::string parse(const PageDao &pagedao, UrlProvider &provider, const std::string &content) const
virtual std::vector<std::string> extractCategories(std::string content) const = 0; {
return parse(pagedao, provider, content, empty);
}
virtual std::string parse(const PageDao &pagedao, UrlProvider &provider, const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const = 0;
virtual std::string parseDynamics(
const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const = 0;
virtual std::vector<std::string> extractCategories(const std::string &content) const = 0;
virtual ~IParser(){}; virtual ~IParser(){};
}; };

1
page.h
View File

@ -7,6 +7,7 @@ class Page
public: public:
Page(); Page();
std::string name; std::string name;
std::string title;
bool listed; bool listed;
unsigned int current_revision; unsigned int current_revision;
unsigned int pageid; unsigned int pageid;

66
pagelistrenderer.cpp Normal file
View File

@ -0,0 +1,66 @@
#include "pagelistrenderer.h"
#include "grouper.h"
PageListRenderer::PageListRenderer(Template &templ, UrlProvider &provider, Database &database)
{
this->templ = &templ;
this->urlProvider = &provider;
this->database = &database;
}
std::string PageListRenderer::render(const std::vector<Page> &pagelist, std::string type) const
{
TemplatePage pagelistrendergroup = this->templ->loadResolvedPart("pagelistrender_group");
TemplatePage pagelistlink = this->templ->loadResolvedPart("pagelistrender_link");
std::function<bool(const std::string &, const std::string &)> lesser = [](const std::string &a, const std::string &b) -> bool {
return a < b;
};
std::function<bool(const std::string &, const std::string &)> greater = [](const std::string &a, const std::string &b) -> bool {
return a > b;
};
using Grouper = Grouper<std::string, Page, std::function<bool(const std::string &,const std::string &)>>;
Grouper grouper = (type == "letter") ? Grouper(lesser) : Grouper(greater);
if(type == "letter")
{
auto az = [&](const Page &p) -> std::string {
int upper = toupper(p.title[0]); // TODO: this is not unicode safe.
return { (char)upper };
};
grouper.group(az, pagelist);
}
else
{
auto revisionDao = this->database->createRevisionDao();
auto creationdate = [&revisionDao](const Page &p) -> std::string {
return utils::formatLocalDate(revisionDao->getRevisionForPage(p.name, 1).value().timestamp, "%Y-%m");
};
grouper.group(creationdate, pagelist);
}
std::stringstream stream;
std::string lastGroup = "";
for(auto &entry : grouper.getResults())
{
std::string groupname = entry.first;
const std::vector<const Page*> &pages = entry.second;
if(lastGroup != groupname)
{
lastGroup = groupname;
pagelistrendergroup.setVar("groupname", groupname);
stream << pagelistrendergroup.render();
}
for(const Page *p : pages)
{
pagelistlink.setVar("inner", p->title);
pagelistlink.setVar("href", this->urlProvider->page(p->name));
stream << pagelistlink.render();
}
}
return stream.str();
}

27
pagelistrenderer.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef PAGELISTRENDERER_H
#define PAGELISTRENDERER_H
#include "utils.h"
#include "page.h"
#include "template.h"
#include "htmllink.h"
#include "urlprovider.h"
#include "database/database.h"
class PageListRenderer
{
private:
Template *templ;
UrlProvider *urlProvider;
Database *database;
public:
PageListRenderer(Template &templ, UrlProvider &provider, Database &database);
std::string render(const std::vector<Page> &pages, std::string type) const;
inline static const std::string RENDER_GROUP_BY_LETTER { "letter" };
inline static const std::string RENDER_GROUP_BY_CREATIONDATE { "creationdate" };
};
#endif

View File

@ -27,10 +27,11 @@ SOFTWARE.
#include "parser.h" #include "parser.h"
#include "utils.h" #include "utils.h"
#include "htmllink.h" #include "htmllink.h"
std::vector<Headline> Parser::extractHeadlines(std::string content) const std::vector<Headline> Parser::extractHeadlines(const std::string &content) const
{ {
std::vector<Headline> result; std::vector<Headline> result;
std::string reg = R"(\[h(1|2|3)\](.*?)\[/h\1\])";
std::string reg = R"(\[h(1|2|3)\](\[.*?\])*(.*?)(\[.*?\])*\[\/h\1\])";
std::regex headerfinder(reg); std::regex headerfinder(reg);
auto begin = std::sregex_iterator(content.begin(), content.end(), headerfinder); auto begin = std::sregex_iterator(content.begin(), content.end(), headerfinder);
auto end = std::sregex_iterator(); auto end = std::sregex_iterator();
@ -40,13 +41,13 @@ std::vector<Headline> Parser::extractHeadlines(std::string content) const
auto smatch = *it; auto smatch = *it;
Headline h; Headline h;
h.level = utils::toUInt(smatch.str(1)); h.level = utils::toUInt(smatch.str(1));
h.title = smatch.str(2); h.title = smatch.str(3);
result.push_back(h); result.push_back(h);
} }
return result; return result;
} }
std::vector<std::string> Parser::extractCategories(std::string content) const std::vector<std::string> Parser::extractCategories(const std::string &content) const
{ {
std::vector<std::string> result; std::vector<std::string> result;
std::string reg = R"(\[category\](.*?)\[/category\])"; std::string reg = R"(\[category\](.*?)\[/category\])";
@ -62,7 +63,7 @@ std::vector<std::string> Parser::extractCategories(std::string content) const
return result; return result;
} }
std::string Parser::extractCommand(std::string cmdname, std::string content) const std::string Parser::extractCommand(std::string cmdname, const std::string &content) const
{ {
std::string cmd = "[cmd:" + cmdname + "]"; std::string cmd = "[cmd:" + cmdname + "]";
std::string cmdend = "[/cmd:" + cmdname + "]"; std::string cmdend = "[/cmd:" + cmdname + "]";
@ -116,31 +117,74 @@ std::string Parser::processLink(const PageDao &pageDao, UrlProvider &urlProvider
return htmllink.render(); return htmllink.render();
} }
std::string Parser::parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const std::string Parser::processImage(std::smatch &match) const
{
std::string tag = match.str(1);
std::string inside = match.str(2);
std::vector<std::string> splitted = utils::split(inside, '|');
std::string width;
std::string height;
std::string src;
if(splitted.size() == 3)
{
width = splitted[0];
height = splitted[1];
src = splitted[2];
}
else
{
src = splitted[0];
}
if(!width.empty() && !height.empty())
{
return "<img src=\"" + src + "\" width=\"" + width + "\" height=\"" + height + "\"/>";
}
return "<img src=\"" + src + "\"/>";
}
std::string Parser::parse(const PageDao &pagedao, UrlProvider &provider, const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const
{ {
std::string result; std::string result;
// we don't care about commands, but we nevertheless replace them with empty strings // we don't care about commands, but we nevertheless replace them with empty strings
std::regex tagfinder(R"(\[(b|i|u|li||ul|ol|link|wikilink|h\d|cmd:rename|cmd:redirect|category)*?\]((\s|\S)*?)\[/\1])"); std::regex tagfinder(
result = utils::regex_callback_replacer(tagfinder, content, [&](std::smatch &match) { R"(\[(b|i|u|li||ul|ol|code|blockquote|img|link|wikilink|h\d|cmd:visible|cmd:rename|cmd:redirect|cmd:pagetitle|category|dynamic:postlist|dynamic:includepage|dynamic:getvar|dynamic:setvar)*?\]((\s|\S)*?)\[/\1])");
result = utils::regex_callback_replacer(
tagfinder, content,
[&](std::smatch &match)
{
std::string tag = match.str(1); std::string tag = match.str(1);
std::string content = match.str(2); std::string content = match.str(2);
std::string justreplace[] = {"b", "i", "u", "ul", "li", "ol"}; std::string justreplace[] = {"b", "i", "u", "ul", "li", "ol", "code", "blockquote"};
content = parse(pagedao, provider, content); content = parse(pagedao, provider, content, callback);
if(std::find(std::begin(justreplace), std::end(justreplace), tag) != std::end(justreplace)) if(std::find(std::begin(justreplace), std::end(justreplace), tag) != std::end(justreplace))
{ {
return "<" + tag + ">" + content + "</" + tag + ">"; return "<" + tag + ">" + content + "</" + tag + ">";
} }
if(tag == "link" || tag == "wikilink") if(tag == "link" || tag == "wikilink")
{ {
return this->processLink(pagedao, provider, return this->processLink(
pagedao, provider,
match); // TODO: recreate this so we don't check inside the function stuff again match); // TODO: recreate this so we don't check inside the function stuff again
} }
if(tag == "img")
{
return this->processImage(match);
}
if(tag[0] == 'h') if(tag[0] == 'h')
{ {
return "<" + tag + " id='" + content + "'>" + content + "</" + tag + ">"; return "<" + tag + " id='" + content + "'>" + content + "</" + tag + ">";
} }
return std::string(""); return callback(tag, content);
}); });
result = utils::strreplace(result, "\r\n", "<br>"); result = utils::strreplace(result, "\r\n", "<br>");
return result; return result;
} }
std::string Parser::parseDynamics(const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const
{
std::regex tagfinder(R"(\[(dynamic:\w+)*?\]((\s|\S)*?)\[/\1])");
return utils::regex_callback_replacer(tagfinder, content,
[&](std::smatch &match) { return callback(match.str(1), match.str(2)); });
}

View File

@ -6,14 +6,21 @@ class Parser : public IParser
{ {
private: private:
std::string processLink(const PageDao &pageDao, UrlProvider &urlProvider, std::smatch &match) const; std::string processLink(const PageDao &pageDao, UrlProvider &urlProvider, std::smatch &match) const;
std::string processImage(std::smatch &match) const;
public: public:
std::string extractCommand(std::string cmdname, std::string content) const; std::string extractCommand(std::string cmdname, const std::string &content) const;
std::vector<Headline> extractHeadlines(std::string content) const override; std::vector<Headline> extractHeadlines(const std::string &content) const override;
std::vector<std::string> extractCategories(std::string content) const override; std::vector<std::string> extractCategories(const std::string &content) const override;
std::string parse(const PageDao &pagedao, UrlProvider &provider, std::string content) const override; using IParser::parse;
virtual std::string parse(
const PageDao &pagedao, UrlProvider &provider, const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const override;
std::string parseDynamics(
const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const override;
using IParser::IParser; using IParser::IParser;
~Parser(){};
}; };
#endif // PARSER_H #endif // PARSER_H

View File

@ -40,7 +40,7 @@ std::pair<std::string, std::string> Request::createPairFromVar(std::string var)
else else
{ {
std::string key = var.substr(0, equal); std::string key = var.substr(0, equal);
std::string val = utils::html_xss(var.substr(equal + 1)); std::string val = utils::html_xss(utils::urldecode(var.substr(equal + 1)));
return std::make_pair(std::move(key), std::move(val)); return std::make_pair(std::move(key), std::move(val));
} }
} }

View File

@ -12,17 +12,13 @@
#include <filesystem> #include <filesystem>
#include <sys/mount.h> #include <sys/mount.h>
#include <sys/capability.h> #include <sys/capability.h>
#include <qssb.h> #include <exile.hpp>
#include "../logger.h" #include "../logger.h"
#include "../utils.h" #include "../utils.h"
#include "../random.h" #include "../random.h"
#include "sandbox-linux.h" #include "sandbox-linux.h"
/* TODO: make a whitelist approach. So far we simply blacklist
* obvious systemcalls. To whitelist, we need to analyse our
* dependencies (http library, sqlite wrapper, sqlite lib etc.) */
bool SandboxLinux::supported() bool SandboxLinux::supported()
{ {
std::fstream stream; std::fstream stream;
@ -45,7 +41,7 @@ bool SandboxLinux::enable(std::vector<std::string> fsPaths)
std::sort(fsPaths.begin(), fsPaths.end(), std::sort(fsPaths.begin(), fsPaths.end(),
[](const std::string &a, const std::string &b) { return a.length() < b.length(); }); [](const std::string &a, const std::string &b) { return a.length() < b.length(); });
struct qssb_policy *policy = qssb_init_policy(); struct exile_policy *policy = exile_init_policy();
if(policy == NULL) if(policy == NULL)
{ {
Logger::error() << "Failed to init sandboxing policy (worker) "; Logger::error() << "Failed to init sandboxing policy (worker) ";
@ -53,30 +49,22 @@ bool SandboxLinux::enable(std::vector<std::string> fsPaths)
} }
for(unsigned int i = 0; i < fsPaths.size(); i++) for(unsigned int i = 0; i < fsPaths.size(); i++)
{ {
qssb_append_path_policy(policy, QSSB_FS_ALLOW_READ | QSSB_FS_ALLOW_WRITE, fsPaths[i].c_str()); exile_append_path_policies(policy, EXILE_FS_ALLOW_ALL_READ | EXILE_FS_ALLOW_ALL_WRITE, fsPaths[i].c_str());
} }
policy->drop_caps = 1; policy->drop_caps = 1;
policy->not_dumpable = 1; policy->not_dumpable = 1;
policy->no_new_privs = 1; policy->no_new_privs = 1;
policy->mount_path_policies_to_chroot = 1; policy->mount_path_policies_to_chroot = 1;
/* TODO: as said, a whitelist approach is better. As such, this list is bound to be incomplete in the policy->vow_promises = EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_WPATH | EXILE_SYSCALL_VOW_CPATH |
* sense that more could be listed here and some critical ones are probably missing */ EXILE_SYSCALL_VOW_RPATH | EXILE_SYSCALL_VOW_INET | EXILE_SYSCALL_VOW_UNIX |
EXILE_SYSCALL_VOW_THREAD;
/* TODO: use qssb groups */ if(exile_enable_policy(policy) != 0)
long blacklisted_syscalls[] = {QSSB_SYS(setuid), QSSB_SYS(connect), QSSB_SYS(chroot), QSSB_SYS(pivot_root),
QSSB_SYS(mount), QSSB_SYS(setns), QSSB_SYS(unshare), QSSB_SYS(ptrace),
QSSB_SYS(personality), QSSB_SYS(prctl), QSSB_SYS(execveat), QSSB_SYS(execve),
QSSB_SYS(fork)};
qssb_append_syscalls_policy(policy, QSSB_SYSCALL_DENY_KILL_PROCESS, blacklisted_syscalls,
sizeof(blacklisted_syscalls) / sizeof(blacklisted_syscalls[0]));
qssb_append_syscall_default_policy(policy, QSSB_SYSCALL_ALLOW);
if(qssb_enable_policy(policy) != 0)
{ {
Logger::error() << "Sandbox: Activation of seccomp blacklist failed!"; Logger::error() << "Sandbox: Activation of exile failed!";
qssb_free_policy(policy); exile_free_policy(policy);
return false; return false;
} }
qssb_free_policy(policy); exile_free_policy(policy);
return true; return true;
} }

View File

@ -1,4 +1,4 @@
CREATE TABLE page(id INTEGER PRIMARY KEY, name varchar(256), lastrevision integer, visible integer DEFAULT 1); CREATE TABLE page(id INTEGER PRIMARY KEY, name varchar(256), title varchar(1024), lastrevision integer, visible integer DEFAULT 1);
CREATE TABLE user(id INTEGER PRIMARY KEY,username varchar(64), CREATE TABLE user(id INTEGER PRIMARY KEY,username varchar(64),
password blob, salt blob, permissions integer, enabled integer DEFAULT 1); password blob, salt blob, permissions integer, enabled integer DEFAULT 1);
CREATE TABLE session(id INTEGER PRIMARY KEY, csrf_token varchar(32), CREATE TABLE session(id INTEGER PRIMARY KEY, csrf_token varchar(32),

1
submodules/exile.h Submodule

Submodule submodules/exile.h added at f2ca26010a

View File

@ -141,7 +141,7 @@ std::string Template::renderRevisionList(const std::vector<Revision> &revisions,
<< revision.revision << "</a></td>" << revision.revision << "</a></td>"
<< "<td>" << revision.author << "</td>" << "<td>" << revision.author << "</td>"
<< "<td>" << revision.comment << "</td>" << "<td>" << revision.comment << "</td>"
<< "<td>" << utils::toISODate(revision.timestamp) << "</td></tr>"; << "<td>" << utils::toISODateTime(revision.timestamp) << "</td></tr>";
} }
}; };
@ -155,7 +155,7 @@ std::string Template::renderRevisionList(const std::vector<Revision> &revisions,
<< "<td>" << revision.revision << "</td>" << "<td>" << revision.revision << "</td>"
<< "<td>" << revision.author << "</td>" << "<td>" << revision.author << "</td>"
<< "<td>" << revision.comment << "</td>" << "<td>" << revision.comment << "</td>"
<< "<td>" << utils::toISODate(revision.timestamp) << "</td></tr>"; << "<td>" << utils::toISODateTime(revision.timestamp) << "</td></tr>";
} }
}; };

View File

@ -22,7 +22,6 @@ class Template
std::string resolveIncludes(std::string_view content); std::string resolveIncludes(std::string_view content);
std::string getPartPath(std::string_view partname); std::string getPartPath(std::string_view partname);
std::string loadResolvedPart(std::string_view partname);
std::string loadPartContent(std::string_view partname); std::string loadPartContent(std::string_view partname);
TemplatePage createPage(std::string_view name); TemplatePage createPage(std::string_view name);
@ -31,6 +30,7 @@ class Template
ConfigVariableResolver &configVarsResolver, MapCache<TemplatePage> &pageCache); ConfigVariableResolver &configVarsResolver, MapCache<TemplatePage> &pageCache);
TemplatePage getPage(const std::string &pagename); TemplatePage getPage(const std::string &pagename);
std::string loadResolvedPart(std::string_view partname);
std::string renderSearch(const std::vector<std::string> &results, std::string renderSearch(const std::vector<std::string> &results,
std::function<std::string(std::string)> callback) const; std::function<std::string(std::string)> callback) const;

View File

@ -1,6 +1,6 @@
{qswiki:include:general_header} {qswiki:include:general_header}
<div id="content" style="margin-top: 10px;"> <div id="content" style="margin-top: 10px;">
<h2>All pages</h2> <h2>All pages</h2>
{qswiki:var:pagelist} {qswiki:include:pagelistrender}
</div> </div>
{qswiki:include:general_footer} {qswiki:include:general_footer}

View File

@ -0,0 +1 @@
<ul>

View File

@ -0,0 +1 @@
</ul>

View File

@ -0,0 +1 @@
<li>{date}: <a href="{url}">{title}</a></li>

View File

@ -0,0 +1,8 @@
<entry>
<title>{qswiki:var:entrytitle}</title>
<link href="{qswiki:var:entryurl}"/>
<id>{qswiki:var:entryid}</id>
<published>{qswiki:var:entrypublished}</published>
<updated>{qswiki:var:entryupdated}</updated>
<content type="html">{qswiki:var:entrycontent}</content>
</entry>

View File

@ -0,0 +1 @@
</feed>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<author>
<name>{qswiki:config:wikiownername}</name>
</author>
<title>{qswiki:config:wikiname} - {qswiki:var:subtitle}</title>
<id>{qswiki:var:atomfeeduniqueid}</id>
<link rel="self" href="{qswiki:var:atomselflink}"/>
<updated>{qswiki:var:atomfeedupdate}</updated>

View File

@ -6,16 +6,15 @@
<title>{qswiki:var:title}</title> <title>{qswiki:var:title}</title>
<body> <body>
<nav> <nav>
<ul>
<li><a href="{qswiki:config:linkindex}"><h2>{qswiki:config:wikiname}</h2></a></li>
</ul>
<ul id="nav"> <ul id="nav">
<li><a href="{qswiki:config:linkindex}"><h2>{qswiki:config:wikiname}</h2></a></li>
<li><a href="{qswiki:config:linkrecent}">Recent changes</a></li> <li><a href="{qswiki:config:linkrecent}">Recent changes</a></li>
<li><a href="{qswiki:config:linkallpages}">All pages</a></li> <li><a href="{qswiki:config:linkallpages}">All pages</a></li>
<li><a href="{qswiki:config:linkallcats}">All categories</a></li> <li><a href="{qswiki:config:linkallcats}">All categories</a></li>
<li id="searchlink"><a href="{qswiki:config:linksearch}">Search</a></li>
</ul> </ul>
<ul id="right" class="search"> <ul id="right" class="search">
<li><div><form action="{qswiki:config:wikipath}" method="GET"><input type="hidden" name="action" value="search"/><input type="text" name="q" value="search here" onfocus="this.value=''"></form></div></li> <li><div id="searchbar"><form action="{qswiki:config:wikipath}" method="GET"><input type="hidden" name="action" value="search"/><input type="text" name="q" value="search here" onfocus="this.value=''"></form></div></li>
</ul> </ul>
</nav> </nav>

View File

@ -6,20 +6,15 @@
<title>{qswiki:var:title}</title> <title>{qswiki:var:title}</title>
<body> <body>
<nav> <nav>
<ul>
<li><a href="{qswiki:config:linkindex}"><h2>{qswiki:config:wikiname}</h2></a></li>
</ul>
<ul id="nav"> <ul id="nav">
<li><a href="{qswiki:config:linkindex}"><h2>{qswiki:config:wikiname}</h2></a></li>
<li><a href="{qswiki:config:linkrecent}">Recent changes</a></li> <li><a href="{qswiki:config:linkrecent}">Recent changes</a></li>
<li><a href="{qswiki:config:linkallpages}">All pages</a></li> <li><a href="{qswiki:config:linkallpages}">All pages</a></li>
<li><a href="{qswiki:config:linkallcats}">All categories</a></li> <li><a href="{qswiki:config:linkallcats}">All categories</a></li>
</ul> <li id="searchlink"><a href="{qswiki:config:linksearch}">Search</a></li>
<ul>
{qswiki:var:headerlinks} {qswiki:var:headerlinks}
</ul> </ul>
<ul id="right" class="search"> <ul id="right" class="search">
<li><div><form action="{qswiki:config:wikipath}" method="GET"><input type="hidden" name="action" value="search"/><input type="text" value="search here" onfocus="this.value=''" name="q"/></form></div></li> <li><div id="searchbar"><form action="{qswiki:config:wikipath}" method="GET"><input type="hidden" name="action" value="search"/><input type="text" value="search here" onfocus="this.value=''" name="q"/></form></div></li>
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1,3 @@
{qswiki:include:pagelistrender_header}
{qswiki:var:pagelistcontent}
{qswiki:include:pagelistrender_footer}

View File

@ -0,0 +1 @@
<div class="letter_search_result">{qswiki:var:groupname}</div>

View File

@ -0,0 +1 @@
Sort by: <a href="{qswiki:var:pagelistletterlink}">A-Z</a> - <a href="{qswiki:var:pagelistcreationdatelink}">Creation date</a>

View File

@ -0,0 +1 @@
<a href="{qswiki:var:href}">{qswiki:var:inner}</a><br>

View File

@ -0,0 +1,7 @@
{qswiki:include:general_header}
<main id="content">
<h2>Search</h2><br>
Search content of pages:
<form action="{qswiki:config:wikipath}" method="GET"><input type="hidden" name="action" value="search"/><input type="text" name="q" value="search here" onfocus="this.value=''"></form>
</main>
{qswiki:include:general_footer}

View File

@ -1,6 +1,6 @@
{qswiki:include:general_header} {qswiki:include:general_header}
<main id="content"> <main id="content">
<h2>Category: {qswiki:var:categoryname}</h2> <h2>Category: {qswiki:var:categoryname}</h2>
{qswiki:var:pagelist} {qswiki:include:pagelistrender}
</main> </main>
{qswiki:include:general_footer} {qswiki:include:general_footer}

View File

@ -37,6 +37,7 @@ nav
grid-area: nav; grid-area: nav;
} }
nav ul nav ul
{ {
background-color: #062463; background-color: #062463;
@ -47,16 +48,12 @@ nav ul
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
} }
nav li nav li
{ {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
nav a, nav a:visited nav a, nav a:visited
@ -68,7 +65,6 @@ nav a, nav a:visited
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
line-height: 100%; line-height: 100%;
} }
nav a:hover, nav a:focus nav a:hover, nav a:focus
@ -81,8 +77,6 @@ nav a:hover, nav a:focus
font-weight: bold; font-weight: bold;
} }
a, a:visited a, a:visited
{ {
color: #062463;; color: #062463;;
@ -92,7 +86,6 @@ a:hover
{ {
background-color: #062463; background-color: #062463;
color: white; color: white;
} }
#content #content
@ -107,19 +100,16 @@ grid-area: main
#sidebar #sidebar
{ {
grid-area: side; grid-area: side;
} }
#sidebar ul #sidebar ul
{ {
list-style-type: none; list-style-type: none;
} }
#sidebar a, a:visited #sidebar a, a:visited
{ {
color: #062463; color: #062463;
} }
#sidebar a:hover #sidebar a:hover
@ -137,9 +127,8 @@ list-style-type: none;
{ {
background-color: #062463; background-color: #062463;
color: white; color: white;
} }
footer footer
{ {
width: 100%; width: 100%;
@ -160,6 +149,7 @@ footer ul
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
} }
footer li footer li
{ {
margin: 0; margin: 0;
@ -168,14 +158,12 @@ footer li
line-height: 45px; line-height: 45px;
color: white; color: white;
font-weight: bold; font-weight: bold;
//flex: 1 1 0;
text-align: center; text-align: center;
} }
footer a, a:visited footer a, a:visited
{ {
text-decoration: none; text-decoration: none;
color: white; color: white;
display: inline-block; display: inline-block;
} }
@ -198,20 +186,27 @@ background-color: #062463;
text-decoration: underline; text-decoration: underline;
font-weight: bold; font-weight: bold;
} }
ol ol
{ {
counter-reset: item; counter-reset: item;
} }
.indexlink .indexlink
{ {
display: block; display: block;
} }
.notexists .notexists
{ {
color: red !important; color: red !important;
font-weight: bold; font-weight: bold;
} }
#searchlink
{
display: none;
}
@media screen and (orientation: portrait) @media screen and (orientation: portrait)
{ {
@ -219,13 +214,23 @@ display: block;
{ {
display: none; display: none;
} }
#footer li:nth-child(-n+2) #footer li:nth-child(-n+2)
{ {
display: none; display: none;
} }
#footer li:nth-of-type(3) #footer li:nth-of-type(3)
{ {
text-align: center; text-align: center;
width: 100%; width: 100%;
} }
#searchlink {
display: inline;
}
#searchbar {
display: none;
}
} }

View File

@ -18,6 +18,7 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
*/ */
#include <string_view>
#include "urlprovider.h" #include "urlprovider.h"
std::string replaceSingleVar(std::string where, std::string varname, std::string replacement) std::string replaceSingleVar(std::string where, std::string varname, std::string replacement)
@ -52,6 +53,11 @@ std::string UrlProvider::allPages()
return config->linkallpages; return config->linkallpages;
} }
std::string UrlProvider::allPages(std::string rendertype)
{
return replaceSingleVar(config->linkallpagesrendertype, "type", rendertype);
}
std::string UrlProvider::allCats() std::string UrlProvider::allCats()
{ {
return config->linkallcats; return config->linkallcats;
@ -62,6 +68,11 @@ std::string UrlProvider::page(std::string pagename)
return replaceOnlyPage(config->linkpage, pagename); return replaceOnlyPage(config->linkpage, pagename);
} }
std::string UrlProvider::pageByTitle(std::string title)
{
return replaceSingleVar(config->linkpagebytitle, "title", utils::strreplace(title, " ", "-"));
}
std::string UrlProvider::linksHere(std::string pagename) std::string UrlProvider::linksHere(std::string pagename)
{ {
return replaceOnlyPage(config->linkshere, pagename); return replaceOnlyPage(config->linkshere, pagename);
@ -115,7 +126,42 @@ std::string UrlProvider::category(std::string catname)
{ {
return replaceSingleVar(config->linkcategory, "category", catname); return replaceSingleVar(config->linkcategory, "category", catname);
} }
std::string UrlProvider::category(std::string catname, std::string rendertype)
{
Varreplacer replace("{");
replace.addKeyValue("category", catname);
replace.addKeyValue("type", rendertype);
return replace.parse(config->linkcategoryrendertype);
}
std::string UrlProvider::login(std::string page) std::string UrlProvider::login(std::string page)
{ {
return replaceOnlyPage(config->loginurl, page); return replaceOnlyPage(config->loginurl, page);
} }
std::string UrlProvider::rootUrl()
{
return config->rooturl;
}
std::string UrlProvider::atomFeed(std::string filter)
{
return combine({config->rooturl, replaceSingleVar(config->atomurl, "filter", filter)});
}
std::string UrlProvider::combine(std::initializer_list<std::string> urls)
{
std::string result;
for(const std::string &url : urls)
{
std::string_view urlview{url};
if(result.back() == '/' && urlview.front() == '/')
{
urlview.remove_prefix(1);
}
result += urlview;
}
return result;
}

View File

@ -22,11 +22,14 @@ class UrlProvider
std::string recentSorted(unsigned int limit, unsigned int offset, unsigned int sort); std::string recentSorted(unsigned int limit, unsigned int offset, unsigned int sort);
std::string allPages(); std::string allPages();
std::string allPages(std::string rendertype);
std::string allCats(); std::string allCats();
std::string page(std::string pagename); std::string page(std::string pagename);
std::string pageByTitle(std::string title);
std::string linksHere(std::string pagename); std::string linksHere(std::string pagename);
std::string pageHistory(std::string pagename); std::string pageHistory(std::string pagename);
@ -46,8 +49,15 @@ class UrlProvider
std::string refreshSession(); std::string refreshSession();
std::string category(std::string catname); std::string category(std::string catname);
std::string category(std::string catname, std::string rendertype);
std::string login(std::string page); std::string login(std::string page);
std::string rootUrl();
std::string atomFeed(std::string filter);
std::string combine(std::initializer_list<std::string> urls);
}; };
#endif // LINKCREATOR_H #endif // LINKCREATOR_H

View File

@ -136,8 +136,11 @@ std::string utils::getenv(const std::string &key)
std::string utils::readCompleteFile(std::string_view filepath) std::string utils::readCompleteFile(std::string_view filepath)
{ {
std::fstream stream(std::string{filepath}); std::fstream stream(std::string{filepath});
if(!stream.is_open())
{
throw std::runtime_error("utils::readCompleteFile(): stream is not open");
}
std::stringstream ss; std::stringstream ss;
ss << stream.rdbuf(); ss << stream.rdbuf();
std::string content = ss.str(); std::string content = ss.str();
@ -166,7 +169,10 @@ std::string utils::regex_callback_replacer(std::regex regex, const std::string &
return result; return result;
} }
std::string utils::toISODate(time_t t) /* TODO: Convert to C++20, but currently the state is rather poor and would
* require workarounds, so keep it this way for now, and do it properly
* once compiler support gets there */
std::string utils::formatLocalDate(time_t t, std::string format)
{ {
struct tm lt; struct tm lt;
if(localtime_r(&t, &lt) == nullptr) if(localtime_r(&t, &lt) == nullptr)
@ -174,7 +180,7 @@ std::string utils::toISODate(time_t t)
return {}; return {};
} }
char result[20]; char result[20];
size_t x = strftime(result, sizeof(result), "%Y-%m-%d %H:%M:%S", &lt); size_t x = strftime(result, sizeof(result), format.c_str(), &lt);
if(x == 0) if(x == 0)
{ {
return {}; return {};
@ -182,6 +188,16 @@ std::string utils::toISODate(time_t t)
return std::string{result}; return std::string{result};
} }
std::string utils::toISODateTime(time_t t)
{
return utils::formatLocalDate(t, "%Y-%m-%d %H:%M:%S");
}
std::string utils::toISODate(time_t t)
{
return utils::formatLocalDate(t, "%Y-%m-%d");
}
std::string utils::trim(std::string_view view) std::string utils::trim(std::string_view view)
{ {
std::string_view chars = " \t\n\r"; std::string_view chars = " \t\n\r";

View File

@ -81,7 +81,9 @@ inline unsigned int toUInt(const std::string &str)
return result; return result;
} }
std::string formatLocalDate(time_t t, std::string format);
std::string toISODate(time_t t); std::string toISODate(time_t t);
std::string toISODateTime(time_t t);
template <class T> inline std::string toString(const T &v) template <class T> inline std::string toString(const T &v)
{ {