imgurxp/src/utils.cpp

402 lines
13 KiB
C++

#include <string>
#include <optional>
#include <vector>
#include <charconv>
#include <system_error>
#include <kore/kore.h>
#include <kore/http.h>
#include "utils.hpp"
#define ERROR_PAGE_START "<!DOCTYPE html>\n" \
"<html>\n" \
" <head>\n" \
" <meta charset=\"utf-8\" />\n" \
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" \
" <title>ImgurXP</title>\n" \
" <link rel=\"stylesheet\" href=\"/style.css\" crossorigin=\"anonymous\" />\n" \
" </head>\n" \
" <body>\n" \
" <div><b>"
#define ERROR_PAGE_END "</b></div>\n </body>\n</html>"
#define ALBUM_PAGE_START "<!DOCTYPE html>\n" \
"<html>\n" \
" <head>\n" \
" <meta charset=\"utf-8\" />\n" \
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" \
" <title>"
#define ALBUM_PAGE_START_2 "</title>\n" \
" <link rel=\"stylesheet\" href=\"/style.css\" crossorigin=\"anonymous\" />\n" \
" </head>\n" \
" <body>"
#define ALBUM_PAGE_END "\n </body>\n</html>"
std::string build_error_page(const char* error, bool already_escaped) {
std::string output = ERROR_PAGE_START;
if (already_escaped) {
output.append(error);
} else {
std::string error_string = error;
escape_xml(&error_string);
output.append(std::move(error_string));
}
output.append(ERROR_PAGE_END);
return output;
}
// behold: the why
// oh and this is stolen from https://stackoverflow.com/a/3418285
void escape_xml(std::string* str) {
if (str->empty()) {
return;
}
size_t start = 0;
while ((start = str->find("&", start)) != std::string::npos) {
str->replace(start, 1, "&amp;");
start += 5;
}
start = 0;
while ((start = str->find("<", start)) != std::string::npos) {
str->replace(start, 1, "&lt;");
start += 4;
}
start = 0;
while ((start = str->find(">", start)) != std::string::npos) {
str->replace(start, 1, "&gt;");
start += 4;
}
start = 0;
while ((start = str->find("\"", start)) != std::string::npos) {
str->replace(start, 1, "&quot;");
start += 6;
}
start = 0;
while ((start = str->find("'", start)) != std::string::npos) {
str->replace(start, 1, "&#x27;");
start += 6;
}
}
std::optional<std::string> deserialize_album(kore_json json, Album* album) {
*album = {false, std::nullopt, {}};
kore_json_item* title_json = kore_json_find_string(json.root, "title");
if (title_json && *title_json->data.string) {
album->title = title_json->data.string;
}
kore_json_item* is_mature_json = kore_json_find_literal(json.root, "is_mature");
if (is_mature_json) {
album->is_mature = is_mature_json->data.literal == KORE_JSON_TRUE;
}
kore_json_item* medias_json = kore_json_find_array(json.root, "media");
if (!medias_json) {
return "missing media key in response";
}
kore_json_item* media_json;
TAILQ_FOREACH(media_json, &medias_json->data.items, list) {
kore_json_item* media_type_json = kore_json_find_string(media_json, "type");
kore_json_item* id_json = kore_json_find_string(media_json, "id");
kore_json_item* ext_json = kore_json_find_string(media_json, "ext");
kore_json_item* width_json = kore_json_find_integer_u64(media_json, "width");
kore_json_item* height_json = kore_json_find_integer_u64(media_json, "height");
if (!id_json) {
return "missing id key from response";
}
if (!media_type_json) {
std::string error = "missing type key from response (id: ";
error.append(id_json->data.string);
error.append(")");
return error;
}
if (!ext_json) {
std::string error = "missing ext key from response (id: ";
error.append(id_json->data.string);
error.append(")");
return error;
}
if (!width_json) {
std::string error = "missing width key from response (id: ";
error.append(id_json->data.string);
error.append(")");
return error;
}
if (!height_json) {
std::string error = "missing height key from response (id: ";
error.append(id_json->data.string);
error.append(")");
return error;
}
MediaType media_type = MediaType::Image;
if (!strcmp(media_type_json->data.string, "image")) {
// already set as image
} else if (!strcmp(media_type_json->data.string, "video")) {
media_type = MediaType::Video;
} else {
std::string error = "unknown media type (id: ";
error.append(id_json->data.string);
error.append(", type: ");
error.append(media_type_json->data.string);
error.append(")");
return error;
}
Media media = {std::nullopt, media_type, id_json->data.string, ext_json->data.string, width_json->data.u64, height_json->data.u64};
kore_json_item* metadata_json = kore_json_find_object(media_json, "metadata");
if (metadata_json) {
kore_json_item* description_json = kore_json_find_string(metadata_json, "description");
if (description_json && *description_json->data.string) {
media.description = description_json->data.string;
}
}
album->media.push_back(std::move(media));
}
return std::nullopt;
}
DeterminedKeys determine_keys(const char* id, bool album) {
std::string id_str;
if (album) {
id_str.push_back('+');
id_str.append(id);
} else {
id_str = id;
}
if (id_str.length() <= 2) {
return {"imgurxp:", std::move(id_str)};
}
char s1 = id_str.back();
id_str.pop_back();
char s0 = id_str.back();
id_str.pop_back();
id_str.insert(0, "imgurxp:");
return {std::move(id_str), {s0, s1}};
}
void escape_newline(std::string* str) {
if (str->empty()) {
return;
}
size_t start = 0;
while ((start = str->find("\\", start)) != std::string::npos) {
str->replace(start, 1, "\\\\");
start += 2;
}
start = 0;
while ((start = str->find("\n", start)) != std::string::npos) {
str->replace(start, 1, "\\n");
start += 2;
}
}
void unescape_newline(std::string* str) {
if (str->empty()) {
return;
}
size_t start = 0;
while ((start = str->find('\\', start)) != std::string::npos) {
if (str->length() - 1 >= start + 1) {
char c = str->at(start + 1);
if (c == 'n') {
c = '\n';
}
str->replace(start, 2, &c, 1);
}
start++;
}
}
std::string pack_album_into_string(Album album) {
std::string packed;
if (album.is_mature) {
packed.push_back('1');
} else {
packed.push_back('0');
}
if (album.title) {
std::string title = (*album.title).substr();
escape_newline(&title);
packed.append(std::move(title));
}
for (size_t i=0; i < album.media.size(); i++) {
packed.push_back('\n');
Media media = album.media[i];
switch (media.type) {
case MediaType::Image:
packed.push_back('0');
break;
case MediaType::Video:
packed.push_back('1');
break;
}
packed.push_back(' ');
packed.append(media.id);
packed.push_back(' ');
packed.append(media.ext);
packed.push_back(' ');
packed.append(std::to_string(media.width));
packed.push_back(' ');
packed.append(std::to_string(media.height));
if (media.description) {
packed.push_back(' ');
std::string description = (*media.description).substr();
escape_newline(&description);
packed.append(std::move(description));
}
}
return packed;
}
static std::optional<std::string> parse_packed_media_line(char* line, Media* media) {
switch (line[0]) {
case '0':
media->type = MediaType::Image;
break;
case '1':
media->type = MediaType::Video;
break;
default:
std::string error = "unknown media type (";
error.push_back(line[0]);
error.push_back(')');
return error;
}
media->id = std::string(&(line[2]), strchr(&(line[2]), ' ') - line - 2);
size_t offset = 2 + media->id.length() + 1;
media->ext = std::string(&(line[offset]), strchr(&(line[offset]), ' ') - line - offset);
offset += media->ext.length() + 1;
size_t width_offset = strchr(&(line[offset]), ' ') - line;
std::from_chars_result char_res = std::from_chars(&(line[offset]), &(line[width_offset]), media->width);
if (char_res.ec != std::errc()) {
std::string error = "failed to parse width: ";
error.append(std::make_error_code(char_res.ec).message());
return error;
}
offset = width_offset + 1;
size_t height_offset = strchr(&(line[offset]), ' ') - line;
if (!height_offset) {
height_offset = strlen(line);
}
char_res = std::from_chars(&(line[offset]), &(line[height_offset]), media->height);
if (char_res.ec != std::errc()) {
std::string error = "failed to parse height: ";
error.append(std::make_error_code(char_res.ec).message());
return error;
}
offset = height_offset + 1;
char* line_end = strchr(line, '\n');
if (!line_end) {
line_end = &(line[strlen(line)]);
}
if ((size_t)(line_end - line) > offset) {
offset++;
std::string description = std::string(&(line[offset]), line_end - line - offset);
unescape_newline(&description);
media->description = std::move(description);
}
return std::nullopt;
}
std::optional<std::string> unpack_album_from_string(Album* album, char* str) {
album->is_mature = str[0] != '0';
std::string title;
char* title_end = strchr(&(str[1]), '\n');
if (title_end) {
title = std::string(&(str[1]), title_end - str - 1);
} else {
title = &(str[1]);
}
if (!title.empty()) {
unescape_newline(&title);
album->title = std::move(title);
}
if (!title_end) {
return std::nullopt;
}
str = &(title_end[1]);
while (true) {
char spaces = 0;
for (size_t i=0; str[i] && str[i] != '\n'; i++) {
if (str[i] == ' ') {
spaces++;
if (spaces == 4) {
break;
}
}
}
if (spaces < 4) {
return "media line missing required spaces";
}
Media media = {std::nullopt, MediaType::Image, "", "", 0, 0};
std::optional<std::string> error = parse_packed_media_line(str, &media);
if (error) {
return error;
}
album->media.push_back(std::move(media));
char* line_end = strchr(str, '\n');
if (!line_end) {
break;
}
str = &(line_end[1]);
}
return std::nullopt;
}
int send_album_page(struct http_request* req, Album album) {
std::string response = ALBUM_PAGE_START;
std::optional<std::string> escaped_title;
if (album.title) {
escaped_title = album.title;
escape_xml(&(*escaped_title));
response.append((*escaped_title));
} else {
response.append("ImgurX");
}
response.append(ALBUM_PAGE_START_2);
if (album.is_mature) {
response.append("\n <div><b>Marked as Mature</b></div>");
}
if (escaped_title) {
response.append("\n <h1>");
response.append(std::move((*escaped_title)));
response.append("</h1>");
}
for (size_t i=0; i < album.media.size(); i++ ) {
Media* media = &(album.media[i]);
switch (media->type) {
case MediaType::Image:
response.append("\n <img src=\"/proxy/");
response.append(media->id);
response.append(".");
response.append(media->ext);
response.append("\" width=\"");
response.append(std::to_string(media->width));
response.append("\" height=\"");
response.append(std::to_string(media->height));
response.append("\" />");
break;
case MediaType::Video:
response.append("\n <video controls=\"1\" src=\"https://i.imgur.com/");
response.append(media->id);
response.append(".");
response.append(media->ext);
response.append("\" width=\"");
response.append(std::to_string(media->width));
response.append("\" height=\"");
response.append(std::to_string(media->height));
response.append("\" />");
break;
}
if (media->description) {
std::string escaped_description = *(media->description);
escape_xml(&escaped_description);
response.append("\n <p>");
response.append(std::move(escaped_description));
response.append("</p>");
}
}
response.append(ALBUM_PAGE_END);
http_response(req, 200, response.c_str(), response.length());
return HTTP_STATE_COMPLETE;
}