43 Commits

Author SHA1 Message Date
a71c3da129 template: Enable syntax highlighting using highlight.js 2022-11-08 08:48:22 +01:00
fbfe5510a1 parser: Render <code> and <blockquote> preformated 2022-11-08 08:47:53 +01:00
78b9e5e043 README: update 2022-10-30 11:30:27 +01:00
ef8eebdbaa database: Add missing virtual destructors for some classes 2022-10-24 15:17:04 +02:00
7ef9d7f020 sandbox: Use exile_vows_from_str() for seccomp policy 2022-10-23 21:36:58 +02:00
d3bd5f79cc HandlerFeedGenerator: Don't escape title again 2022-08-20 12:57:54 +02:00
995a980d49 HandlerPageEdit: Add 'frompage' GET parameter to use a page as a template 2022-08-20 12:41:30 +02:00
2ee760d9ca submodules: cpp-httplib: Update 2022-08-20 12:31:15 +02:00
ffeea8cfd1 submodules: exile.h: Update 2022-08-20 12:31:15 +02:00
a81963181a RevisionDaoSqlite: Fix cases where we got pageid instead of the page name 2022-08-20 12:31:15 +02:00
d18c0669ce handlers: HandlerPageEdit: Use RevisionRenderer 2022-08-20 12:31:15 +02:00
ecd45a61c8 HandlerPageView: Use RevisionRenderer 2022-08-20 12:30:47 +02:00
2b1c3c71b7 HandlerFeedGenerator: Use RevisionRenderer 2022-08-20 12:30:47 +02:00
a1042720a7 Add RevisionRenderer 2022-08-20 12:30:47 +02:00
6dbe8d34dc Add DynamicContentFactory 2022-08-20 10:24:23 +02:00
51b259f385 HandlerPageView: Set 'pagetitle' dynamic variable 2022-08-17 22:41:15 +02:00
0cad11004f HandlerPageView: Drop partial caches
Anonymous access is the common case, this is already cached.

For other cases this logic cannot be justified as there
is hardly a practical gain for that extra complexity.
2022-08-17 21:57:19 +02:00
2102cf4e6b Add [cmd:allowinclude]
Pages must specify whether they want to be included or not.

Otherwise there too many surprises possibe, enforcing access
restrictions will get more complicated
2022-08-17 21:54:34 +02:00
86890660f4 HandlerPageView: Set 'createdon' dynamic variable 2022-08-17 19:35:52 +02:00
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
58 changed files with 686 additions and 187 deletions

View File

@ -1,23 +1,26 @@
# qswiki # qswiki
About ## About
==== qswiki is a wiki software, intended for my needs. Originally implemented in C, it's now written in C++.
qswiki is a wiki software, intended for small wikis. Originally
implemented in C, it's now written in C++.
History ## Dude... why?
====
A couple of years ago, I wanted to setup a personal wiki on my raspberry tl;dr: It was a playground, an experiment (taken too far). I guess at some point I couldn't stop, because I've already
started.
### History
Several 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 pi. However, the distribution I used back then did not have a PHP package
for ARM. So instead of switching distributions or searching for other for ARM. So instead of switching distributions or searching for other
wikis that I could use, I decided I would write one in C. Yes, wikis that I could use, I simply decided I would write one in C. Yes,
that's an odd way to approach the problem and indeed, I may have had too 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 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 skills a little bit. "web app" in C and wanted to sharpen my C skills a little bit.
Of course, it's pretty straightforward at first. No really: Just use CGI. Of course, it's pretty straightforward at first. No really: Just use CGI
And indeed, that would have been more than enough for my use cases. and print your HTML to stdout.And indeed, that would have been more than enough for my use cases.
Then I decided to play around and started using FastCGI (with the official
But 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. 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, It initially used a "pile of files database", but that became too painful,
so then I started using sqlite. so then I started using sqlite.
@ -25,50 +28,51 @@ so then I started using sqlite.
C++ C++
--- ---
Eventually, since it was mostly a playground for me, the code became Eventually, since it was mostly a playground for me, the code became
unmaintainable. Furthermore, I wanted something quick and given that unmaintainable. Furthermore, I initially wanted something quick and given that
it was CGI, I didn't bother taking care of memory leaks. it was CGI, I didn't bother taking care of memory leaks.
After initiating a FastCGI interface, they became an issue and then the After initiating a FastCGI interface, they became an issue and then the
task of avoiding memory leaks became too annoying. And of course, C does n task of avoiding memory leaks became too annoying. And of course, C does n
ot include any "batteries" and while I could manage, this too was another ot include any "batteries" and while I could manage, this too was another
good reason. good reason.
Overall, I am just continuing the experiment with C++17 now. It's not 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 nearly as bad as you would expect perhaps. Some things are surprisingly
convenient even. Still, the standard library is lacking and convenient even. Still, the standard library is lacking and
I would hope for a some better built-in Unicode support in future C++ I would hope for a some better built-in Unicode support in future C++
standards. standards.
Features
======== ## Features
To be fair, at this point it doesn't even have a "diff" between revisions Some essential features are lacking, such as a diff between revisions,
yet and does not have features that would make you prefer it over other user registration UI, etc.
wikis.
It doesn't compete with any other software anyway.
- CGI - CGI
- HTTP server using the header only library cpp-httplib. It's more - HTTP server using the header only library [cpp-httplib](https://github.com/yhirose/cpp-httplib). It's more
portable and more "future-proof" than FastCGI (since the official website portable and more "future-proof" than FastCGI (since the official website
disappeared, the library's future appears to be uncertain). disappeared, the library's future appears to be uncertain).
- Support for user accounts. Passwords are stored using PBKDF2. - Support for user accounts. Passwords are stored using PBKDF2.
sqlite database, but not too much of an effort to add other types of sqlite database, but not too much of an effort to add other types of
storage backends. sqlite is using the great header only library storage backends. sqlite is using the great header only library
sqlite_modern_cpp [sqlite_modern_cpp](https://github.com/SqliteModernCpp)
- Relatively fine-grained permission system. - Relatively fine-grained permission system.
- Categories - Categories
- Templates - Templates
- FTS search - FTS search
- Caching - Caching
- Blog-like functionality
- RSS/Atom feeds
Security ## Security
======== [exile.h](https://github.com/quitesimpleorg/exile.h) is used
On Linux namespaces are used to restrict the process to only access to restrict access to the files the wiki needs. It doesn't have access to other paths
files it needs. It doesn't have access to other paths in the system. in the system and the system calls that the qswiki process can make are restricted.
In addition, Seccomp is used to restrict the syscalls the qswiki process
can call. As for "web security", all POST requests are centrally As for "web security", all POST requests are centrally protected against CSRF attacks and all input is escaped against XSS
protected against CSRF attacks and all input is escaped against XSS
attacks. attacks.
Building ## 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

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

@ -78,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");

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;

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,8 @@ 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;
virtual ~CategoryDao() = default;
}; };
#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,6 +52,26 @@ 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;
@ -107,21 +127,28 @@ void PageDaoSqlite::save(const Page &page)
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)
{ {

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

@ -9,6 +9,7 @@ class PermissionsDao
PermissionsDao(); PermissionsDao();
virtual std::optional<Permissions> find(std::string pagename, std::string username) = 0; virtual std::optional<Permissions> find(std::string pagename, std::string username) = 0;
virtual void save(std::string pagename, std::string username, Permissions perms) = 0; virtual void save(std::string pagename, std::string username, Permissions perms) = 0;
virtual ~PermissionsDao() = default;
}; };
#endif // PERMISSIONSDAO_H #endif // PERMISSIONSDAO_H

View File

@ -129,9 +129,8 @@ std::optional<Revision> RevisionDaoSqlite::getCurrentForPage(std::string pagenam
try try
{ {
auto query = *db << "SELECT (SELECT username FROM user WHERE id = author), comment, content, " 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 " "strftime('%s',creationtime), page.name, revisionid FROM revision INNER JOIN page ON revision.page = page.id WHERE page.name = ? AND page.lastrevision = revision.revisionid";
"page WHERE name = ? ) AND revisionid = (SELECT lastrevision FROM page WHERE name = ?)"; query << pagename;
query << pagename << pagename;
query >> query >>
std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision); std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision);
} }
@ -155,7 +154,7 @@ std::optional<Revision> RevisionDaoSqlite::getRevisionForPage(std::string pagena
auto query = auto query =
*db *db
<< "SELECT (SELECT username FROM user WHERE id = author), comment, content, strftime('%s',creationtime), " << "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 = ?"; "page.name, revisionid FROM revision INNER JOIN page ON revision.page = page.id WHERE page.name = ? AND revisionid = ? ";
query << pagename << revision; query << pagename << revision;
query >> query >>
std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision); std::tie(result.author, result.comment, result.content, result.timestamp, result.page, result.revision);

View File

@ -37,6 +37,8 @@ class SqliteDao
bool execBool(sqlite::database_binder &binder) const; bool execBool(sqlite::database_binder &binder) const;
int execInt(sqlite::database_binder &binder) const; int execInt(sqlite::database_binder &binder) const;
virtual ~SqliteDao() = default;
}; };
#endif // SQLITEDAO_H #endif // SQLITEDAO_H

View File

@ -11,9 +11,15 @@ class DynamicContent
Database *database; Database *database;
UrlProvider *urlProvider; UrlProvider *urlProvider;
std::string argument;
public: public:
DynamicContent(Template &templ, Database &database, UrlProvider &urlProvider); DynamicContent(Template &templ, Database &database, UrlProvider &urlProvider);
virtual std::string render() = 0; virtual std::string render() = 0;
virtual void setArgument(std::string argument)
{
this->argument = argument;
}
virtual ~DynamicContent() virtual ~DynamicContent()
{ {
} }

View File

@ -0,0 +1,28 @@
#ifndef DYNAMICCONTENTFACTORY_H
#define DYNAMICCONTENTFACTORY_H
#include "dynamiccontent.h"
class DynamicContentFactory
{
private:
Template *templ;
Database *db;
UrlProvider *urlProvider;
public:
DynamicContentFactory(Template &templ, Database &db, UrlProvider &urlProvider)
{
this->templ = &templ;
this->db = &db;
this->urlProvider = &urlProvider;
}
template <class T> inline std::shared_ptr<T> createDynamicContent()
{
return std::make_shared<T>(*this->templ, *this->db, *this->urlProvider);
}
};
#endif // DYNAMICCONTENTFACTORY_H_INCLUDED

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,16 @@
#include "dynamiccontentincludepage.h"
#include "../parser.h"
std::string DynamicContentIncludePage::render()
{
auto revisionDao = this->database->createRevisionDao();
auto rev = revisionDao->getCurrentForPage(this->argument);
if(rev)
{
/* Quite dirty */
if(rev->content.find("[cmd:allowinclude]1[") != std::string::npos)
{
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

@ -1,11 +1,6 @@
#include <chrono> #include <chrono>
#include "dynamiccontentpostlist.h" #include "dynamiccontentpostlist.h"
void DynamicContentPostList::setCategory(std::string catname)
{
this->catname = catname;
}
std::string DynamicContentPostList::render() std::string DynamicContentPostList::render()
{ {
auto categoryDao = this->database->createCategoryDao(); auto categoryDao = this->database->createCategoryDao();
@ -13,12 +8,12 @@ std::string DynamicContentPostList::render()
auto revisionDao = this->database->createRevisionDao(); auto revisionDao = this->database->createRevisionDao();
QueryOption option; QueryOption option;
option.includeInvisible = false; option.includeInvisible = false;
auto members = categoryDao->fetchMembers(this->catname, option); auto members = categoryDao->fetchMembers(this->argument, option);
std::vector<std::pair<std::string, time_t>> pageList; std::vector<std::pair<std::string, time_t>> pageList;
for(std::string &member : members) for(const Page &member : members)
{ {
auto revision = revisionDao->getRevisionForPage(member, 1); auto revision = revisionDao->getRevisionForPage(member.name, 1);
pageList.push_back({member, revision->timestamp}); pageList.push_back({member.name, revision->timestamp});
} }
std::sort(pageList.begin(), pageList.end(), 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::pair<std::string, time_t> &a, std::pair<std::string, time_t> &b) { return a.second > b.second; });

View File

@ -4,12 +4,8 @@
#include "dynamiccontent.h" #include "dynamiccontent.h"
class DynamicContentPostList : public DynamicContent class DynamicContentPostList : public DynamicContent
{ {
private:
std::string catname;
public: public:
using DynamicContent::DynamicContent; using DynamicContent::DynamicContent;
void setCategory(std::string catname);
std::string render() override; std::string render() override;
}; };

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

@ -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

@ -1,5 +1,6 @@
#include "handlerfeedgenerator.h" #include "handlerfeedgenerator.h"
#include "../parser.h" #include "../parser.h"
#include "../revisionrenderer.h"
std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetchEntries( std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetchEntries(
std::vector<std::string> categories) std::vector<std::string> categories)
{ {
@ -10,7 +11,9 @@ std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetch
QueryOption option; QueryOption option;
option.includeInvisible = false; option.includeInvisible = false;
// option.limit = 20; // option.limit = 20;
std::set<std::string> members;
auto comppage = [](const Page &a, const Page &b) { return a.name < b.name; };
std::set<Page, decltype(comppage)> members (comppage);
if(categories.empty()) if(categories.empty())
{ {
auto pages = pageDao->getPageList(option); auto pages = pageDao->getPageList(option);
@ -21,15 +24,18 @@ std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetch
auto categoryDao = this->database->createCategoryDao(); auto categoryDao = this->database->createCategoryDao();
for(std::string cat : categories) for(std::string cat : categories)
{ {
if(!categoryDao->find(cat))
{
throw std::runtime_error("No such category");
}
auto catmembers = categoryDao->fetchMembers(cat, option); auto catmembers = categoryDao->fetchMembers(cat, option);
std::copy(catmembers.begin(), catmembers.end(), std::inserter(members, members.end())); std::copy(catmembers.begin(), catmembers.end(), std::inserter(members, members.end()));
} }
} }
for(const std::string &member : members) for(const Page &member : members)
{ {
auto page = pageDao->find(member).value(); auto revision = revisionDao->getRevisionForPage(member.name, 1).value();
auto revision = revisionDao->getRevisionForPage(page.name, 1).value(); result.push_back({member, revision});
result.push_back({page, revision});
} }
std::sort(result.begin(), result.end(), std::sort(result.begin(), result.end(),
[](EntryRevisionPair &a, EntryRevisionPair &b) { return a.second.timestamp > b.second.timestamp; }); [](EntryRevisionPair &a, EntryRevisionPair &b) { return a.second.timestamp > b.second.timestamp; });
@ -43,7 +49,7 @@ std::vector<HandlerFeedGenerator::EntryRevisionPair> HandlerFeedGenerator::fetch
return result; return result;
} }
Response HandlerFeedGenerator::generateAtom(const std::vector<HandlerFeedGenerator::EntryRevisionPair> &entries, std::string HandlerFeedGenerator::generateAtom(const std::vector<HandlerFeedGenerator::EntryRevisionPair> &entries,
std::string filter) std::string filter)
{ {
@ -56,11 +62,14 @@ Response HandlerFeedGenerator::generateAtom(const std::vector<HandlerFeedGenerat
auto revisionDao = this->database->createRevisionDao(); auto revisionDao = this->database->createRevisionDao();
auto pageDao = this->database->createPageDao(); auto pageDao = this->database->createPageDao();
std::string subtitle = filter;
if(utils::trim(filter).empty()) if(utils::trim(filter).empty())
{ {
filter = "All pages"; subtitle = "All pages";
} }
RevisionRenderer revisionRenderer { *this->templ, *this->database, *this->urlProvider };
for(const EntryRevisionPair &entry : entries) for(const EntryRevisionPair &entry : entries)
{ {
const Page &page = entry.first; const Page &page = entry.first;
@ -72,28 +81,26 @@ Response HandlerFeedGenerator::generateAtom(const std::vector<HandlerFeedGenerat
newestPublished = initialRevision.timestamp; newestPublished = initialRevision.timestamp;
} }
std::string entryPublished = utils::formatLocalDate(initialRevision.timestamp, dateformat) + "Z"; std::string entryPublished = utils::formatLocalDate(initialRevision.timestamp, dateformat) + "Z";
std::string entryUpdated = utils::formatLocalDate(current.timestamp, dateformat) + "Z";
std::string entryurl = std::string entryurl =
this->urlProvider->combine({this->urlProvider->rootUrl(), this->urlProvider->page(page.name)}); this->urlProvider->combine({this->urlProvider->rootUrl(), this->urlProvider->page(page.name)});
TemplatePage atomentry = this->templ->getPage("feeds/atomentry"); TemplatePage atomentry = this->templ->getPage("feeds/atomentry");
atomentry.setVar("entrytitle", utils::html_xss(page.title)); atomentry.setVar("entrytitle", page.title);
atomentry.setVar("entryurl", utils::html_xss(entryurl)); atomentry.setVar("entryurl", utils::html_xss(entryurl));
atomentry.setVar("entryid", utils::html_xss(entryurl)); atomentry.setVar("entryid", utils::html_xss(entryurl));
atomentry.setVar("entrypublished", entryPublished); atomentry.setVar("entrypublished", entryPublished);
Parser parser; atomentry.setVar("entryupdated", entryUpdated);
atomentry.setVar("entrycontent", utils::html_xss(parser.parse(*pageDao, *this->urlProvider, current.content))); atomentry.setVar("entrycontent", utils::html_xss(revisionRenderer.renderContent(current, page.title)));
stream << atomentry.render(); stream << atomentry.render();
} }
stream << atomfooter; stream << atomfooter;
TemplatePage atomheader = this->templ->getPage("feeds/atomheader"); TemplatePage atomheader = this->templ->getPage("feeds/atomheader");
atomheader.setVar("subtitle", filter); atomheader.setVar("subtitle", subtitle);
atomheader.setVar("atomfeeduniqueid", utils::html_xss(this->urlProvider->atomFeed(filter))); 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"); atomheader.setVar("atomfeedupdate", utils::formatLocalDate(newestPublished, dateformat) + "Z");
return atomheader.render() + stream.str();
Response result;
result.setStatus(200);
result.setContentType("application/atom+xml");
result.setBody(atomheader.render() + stream.str());
return result;
} }
Response HandlerFeedGenerator::handleRequest(const Request &r) Response HandlerFeedGenerator::handleRequest(const Request &r)
@ -103,12 +110,26 @@ Response HandlerFeedGenerator::handleRequest(const Request &r)
{ {
std::string type = r.get("type"); std::string type = r.get("type");
std::string categories = r.get("cats"); std::string categories = r.get("cats");
auto entries = fetchEntries(utils::split(categories, ','));
if(type == "atom") if(type == "atom")
{ {
std::string filter = categories; std::string filter = categories;
response = generateAtom(entries, filter); 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 else
{ {

View File

@ -9,7 +9,7 @@ class HandlerFeedGenerator : public Handler
protected: protected:
std::vector<EntryRevisionPair> fetchEntries(std::vector<std::string> categories); std::vector<EntryRevisionPair> fetchEntries(std::vector<std::string> categories);
Response generateAtom(const std::vector<EntryRevisionPair> &entries, std::string atomtitle); std::string generateAtom(const std::vector<EntryRevisionPair> &entries, std::string atomtitle);
Response generateRss(const std::vector<EntryRevisionPair> &entries); Response generateRss(const std::vector<EntryRevisionPair> &entries);
public: public:

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

@ -23,6 +23,7 @@ SOFTWARE.
#include "../request.h" #include "../request.h"
#include "../parser.h" #include "../parser.h"
#include "../revisionrenderer.h"
bool HandlerPageEdit::canAccess([[maybe_unused]] std::string page) bool HandlerPageEdit::canAccess([[maybe_unused]] std::string page)
{ {
return effectivePermissions(page).canEdit(); return effectivePermissions(page).canEdit();
@ -41,7 +42,7 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
return errorResponse("No permission", "You don't have permission to create new pages"); return errorResponse("No permission", "You don't have permission to create new pages");
} }
auto revisiondao = this->database->createRevisionDao(); auto revisiondao = this->database->createRevisionDao();
auto revision = this->database->createRevisionDao()->getCurrentForPage(pagename); auto revision = revisiondao->getCurrentForPage(pagename);
std::string body; std::string body;
unsigned int current_revision = 0; unsigned int current_revision = 0;
@ -50,6 +51,15 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
body = revision->content; body = revision->content;
current_revision = revision->revision; current_revision = revision->revision;
} }
std::string from = r.get("frompage");
if(!from.empty())
{
if(!effectivePermissions(from).canRead())
{
return this->errorResponse("Permission denied", "No access permissions, so you can't use this page as a template");
}
body = revisiondao->getCurrentForPage(from)->content;
}
if(r.getRequestMethod() == "POST") if(r.getRequestMethod() == "POST")
{ {
if(r.post("do") == "submit") if(r.post("do") == "submit")
@ -101,6 +111,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)
{ {
@ -114,12 +125,16 @@ Response HandlerPageEdit::handleRequest(PageDao &pageDao, std::string pagename,
{ {
std::string newContent = r.post("content"); std::string newContent = r.post("content");
Parser parser; Parser parser;
std::string title = parser.extractCommand("pagetitle", newContent);
TemplatePage templatePage = this->templ->getPage("page_creation_preview"); TemplatePage templatePage = this->templ->getPage("page_creation_preview");
templatePage.setVar("actionurl", urlProvider->editPage(pagename)); templatePage.setVar("actionurl", urlProvider->editPage(pagename));
templatePage.setVar("preview_content", parser.parse(pageDao, *this->urlProvider, newContent));
RevisionRenderer revisionRenderer { *this->templ, *this->database, *this->urlProvider };
templatePage.setVar("preview_content", revisionRenderer.renderContent(newContent));
templatePage.setVar("content", newContent); templatePage.setVar("content", newContent);
setPageVars(templatePage, pagename); setPageVars(templatePage, pagename);
templatePage.setVar("title", createPageTitle("Preview: " + pagename)); templatePage.setVar("title", createPageTitle("Preview: " + title));
templatePage.setVar("comment", r.post("comment")); templatePage.setVar("comment", r.post("comment"));
Response response; Response response;
response.setBody(templatePage.render()); response.setBody(templatePage.render());

View File

@ -24,6 +24,11 @@ SOFTWARE.
#include "../parser.h" #include "../parser.h"
#include "../htmllink.h" #include "../htmllink.h"
#include "../dynamic/dynamiccontentpostlist.h" #include "../dynamic/dynamiccontentpostlist.h"
#include "../dynamic/dynamiccontentincludepage.h"
#include "../dynamic/dynamiccontentsetvar.h"
#include "../dynamic/dynamiccontentgetvar.h"
#include "../revisionrenderer.h"
bool HandlerPageView::canAccess(std::string page) bool HandlerPageView::canAccess(std::string page)
{ {
return effectivePermissions(page).canRead(); return effectivePermissions(page).canRead();
@ -86,19 +91,20 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
std::optional<Revision> revision; std::optional<Revision> revision;
std::string templatepartname; std::string templatepartname;
auto revisionDao = this->database->createRevisionDao();
try try
{ {
if(revisionid > 0) if(revisionid > 0)
{ {
if(!effectivePermissions(pagename).canSeePageHistory()) if(!effectivePermissions(pagename).canSeePageHistory())
{ {
auto current = this->database->createRevisionDao()->getCurrentForPage(pagename); auto current = revisionDao->getCurrentForPage(pagename);
if(current && current->revision > revisionid) if(current && current->revision > revisionid)
{ {
return errorResponse("Error", "You are not allowed to view older revisions of this page"); return errorResponse("Error", "You are not allowed to view older revisions of this page");
} }
} }
revision = this->database->createRevisionDao()->getRevisionForPage(pagename, revisionid); revision = revisionDao->getRevisionForPage(pagename, revisionid);
if(!revision) if(!revision)
{ {
return errorResponse("Revision not found", "No such revision found"); return errorResponse("Revision not found", "No such revision found");
@ -118,7 +124,7 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
return r; return r;
} }
} }
revision = this->database->createRevisionDao()->getCurrentForPage(pagename); revision = revisionDao->getCurrentForPage(pagename);
templatepartname = "page_view"; templatepartname = "page_view";
} }
} }
@ -128,57 +134,20 @@ Response HandlerPageView::handleRequest(PageDao &pageDao, std::string pagename,
return errorResponse("Database error", "While trying to fetch revision, a database error occured"); return errorResponse("Database error", "While trying to fetch revision, a database error occured");
} }
TemplatePage page = this->templ->getPage(templatepartname);
Parser parser; Parser parser;
Response result; Response result;
result.setStatus(200); result.setStatus(200);
std::string indexcontent;
std::string parsedcontent;
std::function<std::string(std::string_view, std::string_view)> dynamicParseCallback = RevisionRenderer revisionRenderer { *this->templ, *this->database, *this->urlProvider };
[&](std::string_view key, std::string_view value) -> std::string
{
if(key == "dynamic:postlist")
{
std::shared_ptr<DynamicContentPostList> postlist = createDynamic<DynamicContentPostList>();
postlist->setCategory(std::string(value));
return postlist->render();
}
return std::string{};
};
if(revisionid > 0)
{
indexcontent = createIndexContent(parser, revision->content);
parsedcontent = parser.parse(pageDao, *this->urlProvider, revision->content, dynamicParseCallback);
}
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, dynamicParseCallback);
this->cache->put(cachekeyparsedcontent, parsedcontent);
}
}
std::string revisionstr = std::to_string(revision->revision);
std::string customtitle = parser.extractCommand("pagetitle", revision->content); std::string customtitle = parser.extractCommand("pagetitle", revision->content);
std::string parsedcontent = revisionRenderer.renderContent(revision.value(), customtitle);
/* TODO: Dynamic includes not considered, probably fine in practise */
std::string indexcontent = createIndexContent(parser, revision->content);
std::string revisionstr = std::to_string(revision->revision);
TemplatePage page = this->templ->getPage(templatepartname);
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);

View File

@ -23,6 +23,10 @@ class IParser
} }
virtual std::string parse(const PageDao &pagedao, UrlProvider &provider, const std::string &content, 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; 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 std::vector<std::string> extractCategories(const std::string &content) const = 0;
virtual ~IParser(){}; virtual ~IParser(){};

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

@ -30,7 +30,8 @@ SOFTWARE.
std::vector<Headline> Parser::extractHeadlines(const 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,7 +41,7 @@ std::vector<Headline> Parser::extractHeadlines(const 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;
@ -116,13 +117,38 @@ std::string Parser::processLink(const PageDao &pageDao, UrlProvider &urlProvider
return htmllink.render(); return htmllink.render();
} }
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, 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 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( std::regex tagfinder(
R"(\[(b|i|u|li||ul|ol|link|wikilink|h\d|cmd:rename|cmd:redirect|cmd:pagetitle|category|dynamic:postlist)*?\]((\s|\S)*?)\[/\1])"); R"(\[(b|i|u|li||ul|ol|code|blockquote|img|link|wikilink|h\d|cmd:visible|cmd:rename|cmd:redirect|cmd:pagetitle|cmd:allowinclude|category|dynamic:postlist|dynamic:includepage|dynamic:getvar|dynamic:setvar)*?\]((\s|\S)*?)\[/\1])");
result = utils::regex_callback_replacer( result = utils::regex_callback_replacer(
tagfinder, content, tagfinder, content,
[&](std::smatch &match) [&](std::smatch &match)
@ -130,7 +156,10 @@ std::string Parser::parse(const PageDao &pagedao, UrlProvider &provider, const s
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"};
if(tag != "code" && tag != "blockquote")
{
content = parse(pagedao, provider, content, callback); 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 + ">";
@ -141,12 +170,28 @@ std::string Parser::parse(const PageDao &pagedao, UrlProvider &provider, const s
pagedao, provider, 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 + ">";
} }
if(tag == "code" || tag == "blockquote")
{
return "<pre><" + tag + ">"+ utils::strreplace(content, "\r\n", "\n") + "</"+tag+"></pre>";
}
return callback(tag, content); 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,6 +6,7 @@ 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, const std::string &content) const; std::string extractCommand(std::string cmdname, const std::string &content) const;
@ -15,6 +16,9 @@ class Parser : public IParser
virtual std::string parse( virtual std::string parse(
const PageDao &pagedao, UrlProvider &provider, const std::string &content, const PageDao &pagedao, UrlProvider &provider, const std::string &content,
const std::function<std::string(std::string_view, std::string_view)> &callback) const override; 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;
}; };

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));
} }
} }
@ -75,7 +75,7 @@ void Request::initPostMap(const std::string &url)
void Request::initCookies(const std::string &cookiestr) void Request::initCookies(const std::string &cookiestr)
{ {
// TODO: find out what it really should be, ";" or "; "? // TODO: find out what it really should be, ";" or "; "?
std::regex regex { ";+\\s?" }; std::regex regex{";+\\s?"};
auto cookiesplitted = utils::split(cookiestr, regex); auto cookiesplitted = utils::split(cookiestr, regex);
for(const std::string &part : cookiesplitted) for(const std::string &part : cookiesplitted)
{ {

67
revisionrenderer.cpp Normal file
View File

@ -0,0 +1,67 @@
#include "revisionrenderer.h"
#include "templatepage.h"
#include "dynamic/dynamiccontentpostlist.h"
#include "dynamic/dynamiccontentincludepage.h"
#include "dynamic/dynamiccontentgetvar.h"
#include "dynamic/dynamiccontentsetvar.h"
#include "parser.h"
#include "htmllink.h"
std::string RevisionRenderer::dynamicCallback(std::string_view key, std::string_view value)
{
if(key == "dynamic:postlist")
{
auto postlist = this->dynamicContentFactory.createDynamicContent<DynamicContentPostList>();
postlist->setArgument(std::string(value));
return postlist->render();
}
if(key == "dynamic:includepage")
{
auto includePage = this->dynamicContentFactory.createDynamicContent<DynamicContentIncludePage>();
includePage->setArgument(std::string(value));
return parser.parseDynamics(includePage->render(), std::bind(&RevisionRenderer::dynamicCallback, this, std::placeholders::_1, std::placeholders::_2));
}
if(key == "dynamic:setvar")
{
auto setVar = this->dynamicContentFactory.createDynamicContent<DynamicContentSetVar>();
setVar->setMap(dynamicVarsMap);
setVar->setArgument(std::string(value));
return setVar->render();
}
if(key == "dynamic:getvar")
{
auto getVar = this->dynamicContentFactory.createDynamicContent<DynamicContentGetVar>();
getVar->setMap(dynamicVarsMap);
getVar->setArgument(std::string(value));
return getVar->render();
}
return std::string{};
}
std::string RevisionRenderer::renderContent(std::string content)
{
dynamicVarsMap["pagetitle"] = parser.extractCommand("pagetitle", content);
dynamicVarsMap["createdon"] = utils::toISODate(time(NULL));
std::string resolvedContent = parser.parseDynamics(content, std::bind(&RevisionRenderer::dynamicCallback, this, std::placeholders::_1, std::placeholders::_2));
return parser.parse(*this->db->createPageDao(), *this->urlProvider, resolvedContent);
}
std::string RevisionRenderer::renderContent(const Revision &r, std::string_view customTitle)
{
auto revisionDao = this->db->createRevisionDao();
auto firstRevision = revisionDao->getRevisionForPage(r.page, 1);
if(!firstRevision)
{
throw std::runtime_error("Could not get first revision for page, which is odd. Solar flares?");
}
dynamicVarsMap["createdon"] = utils::toISODate(firstRevision.value().timestamp);
dynamicVarsMap["pagetitle"] = customTitle;
std::string resolvedContent = parser.parseDynamics(r.content, std::bind(&RevisionRenderer::dynamicCallback, this, std::placeholders::_1, std::placeholders::_2));
return parser.parse(*this->db->createPageDao(), *this->urlProvider, resolvedContent);
}

29
revisionrenderer.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef REVISIONRENDERER_H
#define REVISIONRENDERER_H
#include "revision.h"
#include "templatepage.h"
#include "dynamic/dynamiccontentfactory.h"
#include "iparser.h"
#include "parser.h"
class RevisionRenderer
{
private:
DynamicContentFactory dynamicContentFactory;
Database *db;
UrlProvider *urlProvider;
std::map<std::string, std::string> dynamicVarsMap;
std::string dynamicCallback(std::string_view key, std::string_view value);
Parser parser;
public:
RevisionRenderer(Template &templ, Database &db, UrlProvider &urlProvider) :dynamicContentFactory(templ, db, urlProvider)
{
this->db = &db;
this->urlProvider = &urlProvider;
}
std::string renderContent(std::string content);
std::string renderContent(const Revision &r, std::string_view customTitle);
};
#endif // REVISIONRENDERER_H

View File

@ -55,10 +55,7 @@ bool SandboxLinux::enable(std::vector<std::string> fsPaths)
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;
policy->vow_promises = EXILE_SYSCALL_VOW_STDIO | EXILE_SYSCALL_VOW_WPATH | EXILE_SYSCALL_VOW_CPATH | policy->vow_promises = exile_vows_from_str("stdio wpath cpath rpath inet unix thread");
EXILE_SYSCALL_VOW_RPATH | EXILE_SYSCALL_VOW_INET | EXILE_SYSCALL_VOW_UNIX |
EXILE_SYSCALL_VOW_THREAD;
if(exile_enable_policy(policy) != 0) if(exile_enable_policy(policy) != 0)
{ {
Logger::error() << "Sandbox: Activation of exile failed!"; Logger::error() << "Sandbox: Activation of exile failed!";

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

@ -3,5 +3,6 @@
<link href="{qswiki:var:entryurl}"/> <link href="{qswiki:var:entryurl}"/>
<id>{qswiki:var:entryid}</id> <id>{qswiki:var:entryid}</id>
<published>{qswiki:var:entrypublished}</published> <published>{qswiki:var:entrypublished}</published>
<updated>{qswiki:var:entryupdated}</updated>
<content type="html">{qswiki:var:entrycontent}</content> <content type="html">{qswiki:var:entrycontent}</content>
</entry> </entry>

View File

@ -5,4 +5,5 @@
</author> </author>
<title>{qswiki:config:wikiname} - {qswiki:var:subtitle}</title> <title>{qswiki:config:wikiname} - {qswiki:var:subtitle}</title>
<id>{qswiki:var:atomfeeduniqueid}</id> <id>{qswiki:var:atomfeeduniqueid}</id>
<link rel="self" href="{qswiki:var:atomselflink}"/>
<updated>{qswiki:var:atomfeedupdate}</updated> <updated>{qswiki:var:atomfeedupdate}</updated>

View File

@ -5,8 +5,10 @@
<li style="font-size: 10pt">Powered by qswiki</li> <li style="font-size: 10pt">Powered by qswiki</li>
</ul> </ul>
</footer> </footer>
<script src="{qswiki:config:highlightjspath}"></script>
<script> <script>
{qswiki:include:js_session_refresh} {qswiki:include:js_session_refresh}
hljs.highlightAll();
</script> </script>
</body> </body>
</html> </html>

View File

@ -3,6 +3,8 @@
<meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="{qswiki:config:csspath}"> <link rel="stylesheet" type="text/css" href="{qswiki:config:csspath}">
<link rel="stylesheet" href="{qswiki:config:highlightjsstyle}">
<title>{qswiki:var:title}</title> <title>{qswiki:var:title}</title>
<body> <body>
<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

@ -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

@ -53,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;
@ -63,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);
@ -116,6 +126,16 @@ 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);

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,6 +49,7 @@ 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);

View File

@ -136,11 +136,10 @@ 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()) if(!stream.is_open())
{ {
throw std::runtime_error("stream is not open"); throw std::runtime_error("utils::readCompleteFile(): stream is not open");
} }
std::stringstream ss; std::stringstream ss;
ss << stream.rdbuf(); ss << stream.rdbuf();