From ef7ed9034bebe80a429112930ab0481c8aa66c95 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Tue, 21 Apr 2020 18:57:11 +0200 Subject: Weblog (WIP) --- plugins/weblog/weblog.cpp | 260 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 197 insertions(+), 63 deletions(-) (limited to 'plugins/weblog') diff --git a/plugins/weblog/weblog.cpp b/plugins/weblog/weblog.cpp index 1c58b73..0c0ffcd 100644 --- a/plugins/weblog/weblog.cpp +++ b/plugins/weblog/weblog.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -12,79 +13,203 @@ namespace fs = std::filesystem; namespace { -// Return a reasonable mime type based on the extension of a file. -std::string -mime_type(fs::path path) -{ - using boost::algorithm::iequals; - auto const ext = [&path] + const size_t number_of_articles_on_front_page {10}; + const std::string article_filename{"article.data"}; + + // Return a reasonable mime type based on the extension of a file. + std::string mime_type(fs::path path) { - size_t pos = path.string().rfind("."); - if (pos == std::string::npos) - return std::string{}; - return path.string().substr(pos); - }(); - if(iequals(ext, ".htm")) return "text/html"; // TODO: unordered_map - if(iequals(ext, ".html")) return "text/html"; - if(iequals(ext, ".php")) return "text/html"; - if(iequals(ext, ".css")) return "text/css"; - if(iequals(ext, ".txt")) return "text/plain"; - if(iequals(ext, ".js")) return "application/javascript"; - if(iequals(ext, ".json")) return "application/json"; - if(iequals(ext, ".xml")) return "application/xml"; - if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; - if(iequals(ext, ".flv")) return "video/x-flv"; - if(iequals(ext, ".png")) return "image/png"; - if(iequals(ext, ".jpe")) return "image/jpeg"; - if(iequals(ext, ".jpeg")) return "image/jpeg"; - if(iequals(ext, ".jpg")) return "image/jpeg"; - if(iequals(ext, ".gif")) return "image/gif"; - if(iequals(ext, ".bmp")) return "image/bmp"; - if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; - if(iequals(ext, ".tiff")) return "image/tiff"; - if(iequals(ext, ".tif")) return "image/tiff"; - if(iequals(ext, ".svg")) return "image/svg+xml"; - if(iequals(ext, ".svgz")) return "image/svg+xml"; - return "application/text"; -} + using boost::algorithm::iequals; + auto const ext = [&path] + { + size_t pos = path.string().rfind("."); + if (pos == std::string::npos) + return std::string{}; + return path.string().substr(pos); + }(); + if(iequals(ext, ".htm")) return "text/html"; // TODO: unordered_map + if(iequals(ext, ".html")) return "text/html"; + if(iequals(ext, ".php")) return "text/html"; + if(iequals(ext, ".css")) return "text/css"; + if(iequals(ext, ".txt")) return "text/plain"; + if(iequals(ext, ".js")) return "application/javascript"; + if(iequals(ext, ".json")) return "application/json"; + if(iequals(ext, ".xml")) return "application/xml"; + if(iequals(ext, ".swf")) return "application/x-shockwave-flash"; + if(iequals(ext, ".flv")) return "video/x-flv"; + if(iequals(ext, ".png")) return "image/png"; + if(iequals(ext, ".jpe")) return "image/jpeg"; + if(iequals(ext, ".jpeg")) return "image/jpeg"; + if(iequals(ext, ".jpg")) return "image/jpeg"; + if(iequals(ext, ".gif")) return "image/gif"; + if(iequals(ext, ".bmp")) return "image/bmp"; + if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon"; + if(iequals(ext, ".tiff")) return "image/tiff"; + if(iequals(ext, ".tif")) return "image/tiff"; + if(iequals(ext, ".svg")) return "image/svg+xml"; + if(iequals(ext, ".svgz")) return "image/svg+xml"; + return "application/text"; + } -std::string getFile(const fs::path& filename) -{ - std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate); + // Used to return errors by generating response page and HTTP status code + std::string HttpStatus(std::string status, std::string message, std::function& SetResponseHeader) + { + SetResponseHeader("status", status); + SetResponseHeader("content_type", "text/html"); + return status + " " + message; + } - if (file.is_open()) { - std::ifstream::pos_type fileSize = file.tellg(); - file.seekg(0, std::ios::beg); + std::string getFile(const fs::path& filename) + { + std::ifstream file(filename.string(), std::ios::in | std::ios::binary | std::ios::ate); + + if (file.is_open()) { + std::ifstream::pos_type fileSize = file.tellg(); + file.seekg(0, std::ios::beg); - std::string bytes(fileSize, ' '); - file.read(reinterpret_cast(bytes.data()), fileSize); + std::string bytes(fileSize, ' '); + file.read(reinterpret_cast(bytes.data()), fileSize); - return bytes; + return bytes; - } else { - throw std::runtime_error("Opening "s + filename.string() + " for reading"); + } else { + throw std::runtime_error("Opening "s + filename.string() + " for reading"); + } } -} -bool is_index_page(std::string& path) -{ - return (path.size() == 0 || path.back() == '/'); -} + bool is_index_page(std::string& rel_target) + { + return (rel_target.size() == 0 || rel_target == "/"); + } -std::string generateIndexPage(std::function& SetResponseHeader) -{ - return "

Blog

"; -} + bool is_article_page(std::string& rel_target, fs::path& path) + { + return (rel_target.size() >= 2 && rel_target.back() == '/' && fs::is_directory(path)); + } -// Used to return errors by generating response page and HTTP status code -std::string HttpStatus(std::string status, std::string message, std::function& SetResponseHeader) -{ - SetResponseHeader("status", status); - SetResponseHeader("content_type", "text/html"); - return status + " " + message; -} + bool is_article_file(std::string& rel_target, fs::path& path) + { + return (fs::is_regular_file(path) && path.filename().string() != article_filename); + } -} + struct ArticleInfo + { + fs::path path; + std::string subject; + std::string date; + }; + + // get article metadata from header lines + std::unordered_map getMetaData(fs::path path) + { + if (path.string().size() > 0 && path.string().back() == '/') { + std::string s {path.string()}; + path = s.substr(0, s.size() - 1); + } + std::unordered_map result; + + std::string pathname{path.filename().string()}; + // ISO date + std::string date{pathname.substr(0, 4) + "-"s + pathname.substr(4, 2) + "-"s + pathname.substr(6, 2)}; + std::string time{pathname.substr(9, 2) + ":"s + pathname.substr(11, 2)}; + if (time != "00:00") + date += " " + time; + + result["Date"] = date; + + fs::path filepath {path / article_filename}; + + std::ifstream file(filepath.string(), std::ios::in); + + if (file.is_open()) { + std::string line; + while (!file.eof()) { + std::getline(file, line); + if (line.empty()) // found header end + break; + size_t pos {line.find(": ")}; + if (pos == line.npos) { + std::cerr << "Warning: Found bad header line in " << filepath << ": " << line << std::endl; + continue; + } + result[line.substr(0, pos)] = line.substr(pos + 2); + } + return result; + + } else { + throw std::runtime_error("Opening "s + filepath.string() + " for reading"); + } + } + + std::vector getArticleList(fs::path& path) + { + std::vector result; + + for (auto& year_entry: fs::directory_iterator(path)) { + for (auto& entry: fs::directory_iterator(year_entry.path())) { + auto metaData{getMetaData(entry.path())}; + result.emplace_back(ArticleInfo{entry.path(), metaData.at("Subject"), metaData.at("Date")}); + } + } + + size_t size{std::min(number_of_articles_on_front_page, result.size())}; + // sort backwards + std::partial_sort(result.begin(), result.begin() + size, result.end(), [](const ArticleInfo& a0, const ArticleInfo& a1){ return a0.date > a1.date;}); + + return {result.begin(), result.begin() + size}; + } + + std::string generateIndexPage(fs::path& path, + std::function& GetRequestParam, + std::function& SetResponseHeader) + { + try { + std::string result{"

"s + GetRequestParam("WEBLOG_NAME") + "

"s}; + + fs::path link{ GetRequestParam("rel_target")}; + + auto list{getArticleList(path)}; + for (const auto& article: list) { + result += "

"s + article.subject + "

"s + article.date + "
"s; + } + result += ""; + return result; + } catch (const std::exception& ex) { + return HttpStatus("500", "Reading Index page: "s + ex.what(), SetResponseHeader); + } + } + + std::string generateArticlePage(fs::path& path, + std::function& SetResponseHeader) + { + try { + auto metaData{getMetaData(path)}; + + std::string data { getFile(path / article_filename)}; + + size_t pos {data.find("\n\n")}; + if (pos == data.npos) + throw std::runtime_error("Error parsing article"); + + std::string result { "

"s + metaData.at("Subject") + "

"s + metaData.at("Date") + "

"s + data.substr(pos + 2) + ""s}; + + return result; + } catch (const std::exception& ex) { + return HttpStatus("500", "Reading Article: "s + ex.what(), SetResponseHeader); + } + } + + std::string generateArticleFile(fs::path& path, std::function& SetResponseHeader) + { + try { + SetResponseHeader("content_type", mime_type(path)); + return getFile(path); + } catch (const std::exception& ex) { + return HttpStatus("500", "Reading Article file: "s + ex.what(), SetResponseHeader); + } + } + +} // anonymous namespace std::string weblog_plugin::name() { @@ -122,6 +247,9 @@ std::string weblog_plugin::generate_page( // Build the path to the requested file std::string doc_root{GetRequestParam("doc_root")}; + if (rel_target.size() >= 4 && std::for_each(rel_target.begin(), rel_target.begin() + 4, isdigit)) { + rel_target = rel_target.substr(0, 4) + "/" + rel_target; + } fs::path path {fs::path{doc_root} / rel_target}; if (target.size() && target.back() != '/' && fs::is_directory(path)) { std::string location{GetRequestParam("location") + "/"s}; @@ -132,7 +260,13 @@ std::string weblog_plugin::generate_page( SetResponseHeader("content_type", "text/html"); if (is_index_page(rel_target)) - return generateIndexPage(SetResponseHeader); + return generateIndexPage(path, GetRequestParam, SetResponseHeader); + + if (is_article_page(rel_target, path)) + return generateArticlePage(path, SetResponseHeader); + + if (is_article_file(rel_target, path)) + return generateArticleFile(path, SetResponseHeader); return HttpStatus("404", "Bad path specification: "s + rel_target, SetResponseHeader); -- cgit v1.2.3