403 lines
13 KiB
C++
403 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, "&");
|
|
start += 5;
|
|
}
|
|
start = 0;
|
|
while ((start = str->find("<", start)) != std::string::npos) {
|
|
str->replace(start, 1, "<");
|
|
start += 4;
|
|
}
|
|
start = 0;
|
|
while ((start = str->find(">", start)) != std::string::npos) {
|
|
str->replace(start, 1, ">");
|
|
start += 4;
|
|
}
|
|
start = 0;
|
|
while ((start = str->find("\"", start)) != std::string::npos) {
|
|
str->replace(start, 1, """);
|
|
start += 6;
|
|
}
|
|
start = 0;
|
|
while ((start = str->find("'", start)) != std::string::npos) {
|
|
str->replace(start, 1, "'");
|
|
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_header(req, "Content-Type", "text/html");
|
|
http_response(req, 200, response.c_str(), response.length());
|
|
return HTTP_STATE_COMPLETE;
|
|
}
|