#include "weblog.h" #include #include #include #include #include #include using namespace std::string_literals; namespace fs = std::filesystem; namespace { 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) { 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"; } // 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; } 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); return bytes; } else { throw std::runtime_error("Opening "s + filename.string() + " for reading"); } } bool is_index_page(std::string& rel_target) { return (rel_target.size() == 0 || rel_target == "/"); } bool is_article_page(std::string& rel_target, fs::path& path) { return (rel_target.size() >= 2 && rel_target.back() == '/' && fs::is_directory(path)); } 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() { return "weblog"; } weblog_plugin::weblog_plugin() { //std::cout << "Plugin constructor" << std::endl; } weblog_plugin::~weblog_plugin() { //std::cout << "Plugin destructor" << std::endl; } std::string weblog_plugin::generate_page( std::function& GetServerParam, std::function& GetRequestParam, // request including body (POST...) std::function& SetResponseHeader // to be added to result string ) { try { // Make sure we can handle the method std::string method {GetRequestParam("method")}; if (method != "GET" && method != "HEAD") return HttpStatus("400", "Unknown HTTP method", SetResponseHeader); // Request path must not contain "..". std::string rel_target{GetRequestParam("rel_target")}; std::string target{GetRequestParam("target")}; if (rel_target.find("..") != std::string::npos) { return HttpStatus("400", "Illegal request: "s + target, SetResponseHeader); } // 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}; SetResponseHeader("location", location); return HttpStatus("301", "Correcting directory path", SetResponseHeader); } SetResponseHeader("content_type", "text/html"); if (is_index_page(rel_target)) 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); } catch (const std::exception& ex) { return HttpStatus("500", "Unknown Error: "s + ex.what(), SetResponseHeader); } }